第23章 Swift与Objective-C混合编程

在苹果公司的Swift语言出现之前,开发iOS或macOS应用主要使用Objective-C语言;此外还可以使用C和C++语言。

23.1 选择语言

Swift语言出现后,iOS程序员有了更多的选择。在苹果社区里,有很多人在讨论Swift语言以及Objective-C语言的未来,人们关注的重点是Swift语言是否能够完全取代Objective-C语言。然而在我看来,苹果公司为了给程序员提供更多的选择,会让这两种语言并存。既然是并存,我们就有4种方式可以选择:

 采用纯Swift的改革派方式;

 采用纯Objective-C的保守派方式;

 采用Swift调用Objective-C的左倾改良派方式;

 采用Objective-C调用Swift的右倾改良派方式。

本章之前一直介绍纯Swift的方式,而纯Objective-C的方式超出了本书的讨论范围,后两种方式则是本章的重点。无论是Swift调用Objective-C,还是Objective-C调用Swift,我们都需要做一些工作。

23.2 文件扩展名

在用Xcode等工具开发iOS或macOS应用时,我们可以编写多种形式的源文件;原本就可以使用Objective-C、C和C++语言,Swift语言出现后源文件的形式更加多样了。表23-1所示为开发iOS或macOS应用所有可能的文件扩展名。

图像说明文字 图像说明文字

23.3 Swift 与Objective-C API 映射

在混合编程过程中,Swift与Objective-C的调用是双向的,由于不同语言对于相同API的表述不同,它们之间具有某种映射规律,这种API映射规律主要体现在构造函数和方法两个方面。

23.3.1 构造函数映射

在用Swift与Objective-C语言进行混合编程时,首先涉及调用构造函数实例化对象的问题,不同语言下构造函数的表述形式不同。图23-1所示为苹果公司官方API文档,描述了NSString类的init(format:locale:arguments:)构造函数,Objective-C语言表示形式是initWithFormat:locale: arguments:。

图像说明文字

从图23-1所示的两种语言声明构造函数中可以找到什么规律吗?如果在Swift语言中实例化NSString对象,从Objective-C构造函数映射为Swift构造函数的规律如图23-2所示,Swift构造函数除了第一个参数,其他参数都一一对应,如果参数名与参数标签名相同,则在Swift中省略参数标签。规律的其他细节图中已经解释的很清楚了,这个规律反之亦然,这里不再赘述。

图像说明文字

这种映射规律不仅仅适用于苹果公司官方提供的Objective-C类,也适用于我们自己编写的Objective-C类。下面来看一个示例,其中自己编写的Objective-C类代码如下:

// ObjCObject.h文件代码
#import <Foundation/Foundation.h>

@interface ObjCObject : NSObject

@property(strong, nonatomic, nonnull) NSString* greeting;
@property(strong, nonatomic, nonnull) NSString* name;

-(nonnull instancetype)initWithGreeting:(nonnull NSString*)aGreeting
                                name:(nonnull NSString*)aName;    ①

@end

// ObjCObject.m文件代码
#import "ObjCObject.h"

@implementation ObjCObject

-(nonnull instancetype)initWithGreeting:(nonnull NSString*)aGreeting 
                               name:(nonnull NSString*)aName {
    self = [super init];
    if (self) {
        self.greeting = aGreeting;
        self.name = aName;
    }
    return self;
}

@end


代码第①行是声明Objective-C构造函数,其中nonnull NSString*表示非nil字符串类型,nonnull instancetype表示非nil的当前实例类型,instancetype可以使用id类型替换。从这个构造函数可以推断出Swift语言中ObjCObject构造函数形式如下:

init(greeting: String, name: String)

在Swift语言中调用ObjCObject代码如下:

let obj = ObjCObject(greeting: "Good morning.", name: "Tony")

print("Hi,\(obj.name)! \(obj.greeting) ")

ObjCObject构造函数中的greeting和name参数是非可选类型,不能为nil。

提示

Objective-C构造函数中nonnull声明表示该参数是非nil的,对应的Swift参数是非可选类型。与nonnull相反的声明是nullable,表示可以为nil,对应的Swift参数是可选类型。nonnull和nullable声明是WWDC 2015推出的Objective-C语言Nullability新特性,这也是为了与Swift协同工作。

23.3.2 方法名映射

在用Swift与Objective-C语言进行混合编程时,不同语言下方法名的表述形式也不同。图23-3所示为苹果公司的官方API文档,描述了NSString类的range(of:options:range:)方法,Objective-C语言表示形式是rangeOfString:options:range:。

图像说明文字

从图23-3所示的两种语言声明的方法中可以找到什么规律吗?如图23-4所示,Objective-C方法第一个参数标签名作为Swift方法名,但是需要注意如果参数标签名中间有介词(Of、With和By等),那么介词之前的部分作为Swift方法名,例如:rangeOfString的range作为Swift方法名;介词Of小写第一个字母后,作为第一个参数标签名。其他的参数一一对应下来,包括:参数名、参数标签和参数类型。

图像说明文字

Swift 2.0之后方法可以声明抛出错误,这些能抛出错误的方法在不同语言下的方法名表述形式如图23-5所示,描述了NSString类的write(toFile:atomically:encoding:)方法,Objective-C语言表示形式是writeToFile:atomically:encoding:error:。

图像说明文字

比较两种语言,我们会发现error参数在Swift语言中不再被使用,而是在方法后添加了throws关键字。

这种映射规律不仅仅适用于苹果公司官方提供的Objective-C类,也适用于自己编写的Objective-C类。下面我们看一个示例,其中自己编写的Objective-C类代码如下:

// ObjCObject.h文件代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN    ①

@interface ObjCObject : NSObject

@property(strong, nonatomic) NSString* greeting;
@property(strong, nonatomic) NSString* name;

-(instancetype)initWithGreeting:(NSString*)aGreeting name:(NSString*)aName;

-(NSString*)sayHello:(NSString*)greeting name: (NSString*)name;    ②

-(nullable NSString *)write:(NSString *)fileName 
                                error:(NSError **)error;    ③

@end

NS_ASSUME_NONNULL_END        ④

// ObjCObject.m文件代码
#import "ObjCObject.h"

@implementation ObjCObject

-(instancetype)initWithGreeting:(NSString*)aGreeting name:(NSString*)aName {
    self = [super init];
    if (self) {
        self.greeting = aGreeting;
        self.name = aName;
    }
    return self;
}

-(NSString*)sayHello:(NSString*)greeting name: (NSString*)name {
    NSString *string = [NSString stringWithFormat:@"Hi,%@ %@.", name, greeting];
    return string;
}


-(NSString *)write:(NSString *)fileName 
               error:(NSError *__autoreleasing *)error {
    if (error) {
        *error = [NSError errorWithDomain:@"ObjCObject Error" 
                                                  code:0 userInfo:nil];
    }
    return nil;
}

@end


代码第①行和第④行是两个宏,它们之间的成员变量、参数、属性和返回值等类型都标记为nonnull。代码第②行声明了一个普通的方法,而代码第③行是一个可能抛出错误的方法。

在Swift语言中调用ObjCObject代码如下:

import Foundation

// init(greeting: String, name: String)
let obj = ObjCObject(greeting: "Good morning.", name: "Tony")

print("Hi,\(obj.name)! \(obj.greeting) ")

let hello = obj.sayHello("Good morning.", name: "Tom")    ①
print(hello)
do {
    print(try obj.write("a.plist"))    ②
} catch let error {
    print(error)
}


上述代码第①行是调用ObjCObject的sayHello:name:方法,其中第一个参数的参数名省略了。代码第②行是调用ObjCObject的write:error: 方法,由于可能抛出错误,需要do-try-catch等错误处理语句。

23.4 同一应用目标中的混合编程

使用Xcode可以创建应用(application)、静态库(static library)、框架(framework)或工程(project),每一个工程都可以创建多个目标(target)。我们可以在同一应用中混合编程,也可以在同一静态库或同一框架中混合编程,本书重点介绍框架。

23.4.1 什么是目标

我们首先解释一下前文提到的目标概念。一个目标就是一个编译后的产品。

图23-6所示的界面是我们之前使用Xcode创建的iOS工程,一个工程中可以包含多个目标。一个目标包含了一些源程序文件、资源文件和编译说明文件等内容,编译说明文件是通过“编译参数设置”(build setting)和“编译阶段”(build phase)进行设置的。

图像说明文字

目标列表上面还有一个工程,工程也包含了一些“编译参数设置”和“编译阶段”设置项目。目标继承了工程的设置,而且可以覆盖工程的设置。

图23-6所示的Xcode工程有两个目标,可以根据需要添加新的目标。首先,请依次选择File→New→Target菜单项,此时会看到一个选择模板对话框。如图23-7所示,选择Application中的Single View Application,接下来点击Next按钮,将看到如图23-8所示的对话框。然后根据情况逐一设定(其中在Language中可以选择Swift或Objective-C),然后点击Finish按钮(如图23-9所示),这样就成功地新增了一个目标。

图像说明文字

图像说明文字

提示

在图23-9所示的左边导航面板中可以发现,每一个目标对应一组源文件和资源文件,但是并不意味着某个源文件或资源文件只能属于一个特定的目标。事实上,这些源文件或资源文件隶属于哪个目标成员是可以设定的。如下图所示,选择文件,打开右边的文件检查器,在下面的Target Membership下选中具体的目标,这样一来该文件就成为这个目标的成员了。

图像说明文字

23.4.2 Swift调用Objective-C

这一节我们来介绍同一应用目标中Swift调用Objective-C的混合编程。打开23.3.1节的HelloWorld示例工程会发现一个HelloWorld-Bridging-Header.h文件,这个文件在此之前没有提及过,它称为Objective-C桥接头文件(Objective-C bridging header)。

1 . 桥接头文件

当Swift调用Objective-C时,我们需要一个桥接头文件,它的命名规则为“<产品模块名>-Bridging-Header.h”。如图23-10所示,桥接头文件的作用是为Swift语言调用Objective-C对象搭建一个“桥”,在桥接头文件中引入所需要的Objective-C Public头文件。这些Public头文件会暴露给同一应用目标的Swift文件,这样在Swift文件中就可以访问这些Public头文件所声明的Objective-C类等内容了。

图像说明文字

要把一个桥接头文件添加到工程中,我们有两种方式,一种是自动添加,另一种是手动添加。

自动添加桥接头文件的场景是:试图在一个Swift应用中添加Objective-C文件,或者试图在一个Objective-C应用中添加Swift文件。这两种情况都会弹出一个是否创建桥接头文件对话框,如图23-11所示,这里点击Yes就会创建一个桥接头文件,然后你在Build Settings中配置好;如果点击No则不会创建桥接头文件。

图像说明文字

要手动添加桥接头文件,先是在工程中新建头文件。具体过程:右键选择HelloWorld组,然后选择菜单中的New File…,此时弹出新建文件模板对话框;如图23-12所示,选择iOS→Source→ Header File;点击Next按钮,进入保存文件界面,根据提示输入文件名并选择存放文件的位置,然后点击Create按钮创建头文件。

图像说明文字

桥接头文件创建成功之后还要进行配置,这才是区分一般头文件的关键。如图23-13所示,选择TARGETS→Build Settings→All→Swift Compiler - Code Generation,修改Objective-C Bridging Header之后的配置内容“HelloWorld/HelloWorld-Bridging-Header.h”,其中HelloWorld是Xcode工程文件下的目录。本例的目录结构如下:

<HelloWorld工程目录>
|____HelloWorld
|    |____HelloWorld-Bridging-Header.h
|    |____main.swift
|    |____ObjCObject.h
|    |____ObjCObject.m
|____HelloWorld.xcodeproj

提示

若采用手动方式,你可以自己配置桥接头文件。所有桥接头文件不必一定命名为“<产品模块名>- Bridging-Header.h”,只要是在Build Settings中配置好编译器,让其能够找到桥接头文件就可以了。问题的关键在于配置。

图像说明文字

2 . 产品名和产品模块名

前面介绍桥接头文件时提到过产品模块名,事实上后面的学习过程中还涉及另外一个类似的名字——产品名。它们有什么区别呢?

默认情况下,产品名和产品模块名是相同的,但这不能说明它俩是相同的概念。如图23-14所示,请选择TARGETS→Build Settings→All→Packaging下的 Product Name(产品名)或Product Module Name(产品模块名),修改默认的产品名或产品模块名。

Swift调用Objective-C的具体示例我们在23.3节介绍过了,调用代码不再赘述。我们看看桥接头文件HelloWorld-Bridging-Header.h的内容:

#import "ObjCObject.h"

由于Swift代码要访问Objective-C的ObjCObject类,所以引入ObjCObject.h头文件就可以了。

图像说明文字

23.4.3 Objective-C调用Swift

如果已经有了一个使用Objective-C编写的iOS或macOS应用,而它的一些新功能需要采用Swift来编写,这时就可以从Objective-C调用Swift。

Objective-C调用Swift时不需要桥接头文件,而是需要Xcode生成头文件(Xcode-generated header)。这种文件由Xcode生成,不需要我们维护,对于开发人员也是不可见的。如图23-15所示,它能够将Swift中的类暴露给Objective-C,它的命名是“<产品模块名>-Swift.h”。我们需要将该头文件引入到Objective-C文件中,而且Swift中的类需要继承NSObject或NSObject子类,还要使用@objc注释属性声明。

图像说明文字

下面我们通过一个示例介绍一下同一应用目标中如何通过Objective-C调用Swift。

1 . 创建Objective-C的macOS工程

为了能够更好地介绍混合搭配调用,我们首先创建一个Objective-C的macOS工程,并参考2.3节创建macOS的Command Line Tool工程。注意,在选择编程语言时要选择Objective-C。创建成功后的界面如图23-16所示。

图像说明文字

2 . 在Objective-C工程中添加Swift类

我们刚刚创建了Objective-C的工程,需要添加Swift类到工程中。具体过程:右键选择HelloWorld组,选择菜单中的New File…,此时弹出新建文件模板对话框。如图23-17所示,请选择macOS→Source→Cocoa Class。

提示

这里我们并没有选择Swift File,因为需要创建的Swift类是基于Cocoa框架的,选择次模板可以自动添加父类。

图像说明文字

接着点击Next按钮,随即可看到如图23-18所示的界面。此时在Class中输入SwiftObject,在Subclass of中选择NSObject,这个选项可以让生成的Swift类继承NSObject。另外,请在Language中选择Swift。

图像说明文字

相关选项设置完成后请点击Next按钮,进入保存文件界面,根据提示选择存放文件的位置,然后点击Create按钮创建Swift类。如果工程中没有桥接头文件,在创建过程中,Xcode也会提示并询问我们是否添加桥接头文件,此时需要选择添加。

以上操作成功后,Xcode工程中就生成了SwiftObject.swift文件。

3 . 调用代码

Swift的SwiftObject创建完成后,我们会在Xcode工程中看到新增加的SwiftObject.swift文件。在SwiftObject.swift中编写如下代码:

import Foundation                 ①

@objc class SwiftObject: NSObject {    ②

    private var name: String
    private var greeting: String

    init(greeting aGreeting: String, name aName: String) {    ③
        self.greeting = aGreeting
        self.name = aName
    }

    override var description: String {    ④
        let string = String(format: "desc -> name:%@, 
        greeting:%@", name, greeting)
        return string
    }    

    func sayHello(_ aGreeting: String, name aName: String) -> String {    ⑤
        var string = "Hi," + aName + " "
        string += aGreeting + "."
        return string
    }
}


上述代码第①行引入了Foundation框架的头文件。第②行代码定义SwiftObject类,Swift中的类要与Objective-C兼容,必须继承NSObject类或NSObject子类。另外,我们在类前面声明@objc;@objc是一种属性注释,也可以注释属性和方法。

代码第③行定义构造函数init(greeting:name:)。它有两个参数,构造函数中参数标签名一般都是要指定的。

代码第④行重写description属性,description是NSObject提供的只读计算属性。

代码第⑤行定义了sayHello(:name:)方法。它有两个参数,第一个参数标签为“”调用时省略标签,第二个参数标签,需要调用时指定标签name。

下面看Objective-C端的代码,main.m文件代码如下:

#import <Foundation/Foundation.h>
#import "HelloWorld-Swift.h"    ①

int main(int argc, const char * argv[]) {

    SwiftObject *sobj = [[SwiftObject alloc] initWithGreeting:@"Good morning" 
    name:@"Tom"];            ②

    NSLog(@"%@", sobj.description);     ③

    NSString* hello = [sobj sayHello:@"Good morning" name:@"Tony"];    ④
    NSLog(@"%@",hello);    ⑤

    return 0;
}


上述代码第①行引入头文件HelloWorld-Swift.h。它是Xcode生成的头文件,命名规则是 “<产品模块名>- Swift.h”。

代码第②行实例化SwiftObject对象。SwiftObject是Swift中定义的类,它的构造函数是init(greeting:name:),调用时符合23.3.1节讨论的映射规律。代码第③行是打印输出SwiftObject的description属性。

代码第④行调用SwiftObject的sayHello(_:name:)方法,调用时符合23.3.1节讨论的映射规律。代码第⑤行NSLog(@"%@",hello)用于输出结果。

这样就实现了在Objective-C中调用Swift代码的情况,我们可以借助这样的调用充分利用已有的Swift文件,减少重复编码,提高工作效率。

23.5 同一框架目标中的混合编程

我们不仅可以在应用(application)工程中混合编程,还可以在静态链接库(static library)或框架(framework)工程中进行混合编程。

23.5.1 链接库和框架

我们首先了解一下什么是链接库,以及什么是框架。有时候,我们需要将某些类复用给其他的团队、公司或者个人,但由于某些原因不能提供源代码,此时就可以将这些类编写成链接库或框架。

库是一些没有main函数的程序代码的集合。链接库分静态链接库和动态链接库,它们的区别是:静态链接库可以编译到你的执行代码中,应用程序可以在没有静态链接库的环境下运行;动态链接库不能编译到你的执行代码中,应用程序必须在有链接库文件的环境下运行。

提示

静态链接库中不能有Swift代码模块,只能是Objective-C代码模块。

静态链接库比较麻烦,使用时需要给使用者提供.a和.h文件,还要配置很多环境变量。而框架是将.a和.h等文件打包在一起以方便使用,需要配置的环境变量简单且非常少。事实上我们已经介绍并使用了苹果公司提供的一些框架,如Foundation、UIKit、QuartzCore和CoreFoundation。

如图23-19所示是iOS的Framework & Library工程模板,可以创建基于iOS的:Cocoa Touch Framework(框架)、Cocoa Touch Static Library(静态链接库)和Metal Library(Metal① 库)。

图像说明文字

① Metal 是一个兼顾图形与计算功能的,面向底层、低开销的硬件加速应用程序接口(API),其类似于将 OpenGL 与 OpenCL 的功能集成到了同一个API上,最初支持它的系统是 iOS 8。 ——引自于维基百科:https://zh.wikipedia.org/wiki/Metal_(API)

如图23-20所示是macOS的Framework & Library工程模板。

图像说明文字

23.5.2 Swift调用Objective-C

这一节我们先来介绍同一框架目标中如何通过Swift调用Objective-C进行混合编程。首先创建一个iOS框架工程,打开图23-19所示的Cocoa Touch Framework工程模板,点击Next按钮,进入如图23-21所示的对话框;我们输入名字MyFramework,选择语言Swift,然后再点击Next按钮,进入保存文件对话框;点击Create按钮创建工程,如果成功地创建工程,则如图23-22所示。

图像说明文字

图像说明文字

从图23-22所示的框架工程中可看到一个MyFramework.h头文件,它被称为Objective-C“保护伞头文件”(Objective-C umbrella header)。保护伞头文件中可以引入框架的Public头文件。

保护伞头文件的命名规则为“<产品模块名>.h”。保护伞头文件的作用与桥接头文件类似,如图23-23所示,保护伞头文件为Swift语言调用Objective-C对象搭建一个“桥”,在保护伞头文件中引入所需要的Objective-C Public头文件。这些Public头文件会暴露给同一框架目标的Swift文件,这样在Swift文件中就可以访问这些头文件所声明的Objective-C类等内容。

图像说明文字

下面我们通过一个示例介绍一下同一框架目标中如何通过Swift调用Objective-C。该示例中Swift对象SwiftObject调用Objective-C对象ObjCObject(ObjCObject的代码与23.3.2节一样,不再赘述)。SwiftObject.swift文件代码如下:

import Foundation

public class SwiftObject {

    public func callFrameworkMethod() -> String {

        // init(greeting: String, name: String)
        let obj = ObjCObject(greeting: "Good morning.", name: "Tony")

        print("Hi,\(obj.name)! \(obj.greeting) ")

        let hello = obj.sayHello("Good morning.", name: "Tom")
        print(hello)

        do {
            print( try obj.write("a.plist"))
        } catch let error {
            print(error)
        }

        return hello
    }
}


上述代码也与23.3.2节Swift语言中调用ObjCObject的代码类似,只不过本例是将这些代码封装到callFrameworkMethod()方法中,这个方法是暴露给一个应用工程,通过应用工程目标调用的。

图23-24所示为测试调用时序图,测试应用调用MyFramework框架SwiftObject对象的callFrameworkMethod()方法。callFrameworkMethod()方法中先是实例化MyFramework框架中的ObjCObject对象,接着调用ObjCObject对象的sayHello:name:方法。

图像说明文字

为了能够在Swift中调用Objective-C对象,我们需要修改保护伞头文件MyFramework.h,代码如下:

#import <UIKit/UIKit.h>

// 定义框架项目版本号
FOUNDATION_EXPORT double MyFrameworkVersionNumber;

// 定义框架项目版本
FOUNDATION_EXPORT const unsigned char MyFrameworkVersionString[];

// 框架中要暴露的Public头文件
#import <MyFramework/ObjCObject.h>    ①
// #import "ObjCObject.h"    ②


要暴露给Swift的Objective-C头文件,还需要在保护伞头文件MyFramework.h中引入。代码第①行是引入语句#import <MyFramework/ObjCObject.h>,MyFramework是产品模块名。这条语句还可以写成代码第②行的#import "ObjCObject.h"语句,这两种引入方式在本例中的效果相同。

提示

<MyFramework/ObjCObject.h>中的尖括号表示在环境变量中搜索头文件。"ObjCObject.h"中的双引号表示系统路径目录搜索。

默认情况下要暴露给Swift的Objective-C头文件(如ObjCObject.h等)都是非Public头文件,我们需要把它们设置为Public头文件。具体设置如下:选择 TARGETS→MyFramework→Build Phases→Headers (2 items)。拖曳需要暴露的头文件从Project栏到Public栏,如图23-25所示。

图像说明文字

23.5.3 测试框架目标

链接库和框架与应用的最大区别是,应用编译的结果是可以独立运行的文件,而链接库和框架编译的结果不能独立,它们为应用提供服务。所以我们开发完成一个框架目标,则需要另外的应用目标来测试它。有两种方法可以实现这种测试:

 基于同一工程不同目标;

 基于同一工作空间(workspace)不同工程。

1 . 基于同一工程不同目标

这种测试方法就是在当前的框架工程中创建另外一个应用目标,通过这个应用目标测试框架目标。这种方法两个目标都在同一个工程中,耦合度很高,适合同一个团队开发。

我们来具体介绍一下,首先参考23.4.1节添加SwiftApp目标。请选择模板iOS→Application→Single View Application,如图23-26所示将语言选择为Swift。

图像说明文字

提示

选择Swift语言的目的是便于测试,因为我们要测试MyFramework中的SwiftObject类也是使用Swift语言编写的。毕竟相同语言的沟通是完全“无障碍”的。

接下来需要一些配置,首先便是为SwiftApp目标和MyFramework目标建立依赖关系。因为SwiftApp目标依赖于MyFramework目标,所以选中SwiftApp目标中的General中的Embedded Binaries。如图23-27所示,选择Embedded Binaries左下角的+按钮,然后从弹出界面中选择MyFramework.framework,再点击Add按钮,这样依赖关系就添加好了。

提示

Embedded Binaries会将MyFramework.framework文件嵌入到SwiftApp应用包中,只要是自定义框架就需要这样配置。一般,官方提供的框架需要在Linked Frameworks and Libraries中配置,如图23-27所示在Linked Frameworks and Libraries中选择MyFramework.framework文件。如果自定义框架在Embedded Binaries会中进行了配置,而且还有编译错误,那么还需要同时配置Linked Frameworks and Libraries。

图像说明文字

建立依赖关系之后我们来看看SwiftApp目标中的ViewController.swift相关代码:

import UIKit
import MyFramework            ①

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let sobj = SwiftObject()    ②
        let message = sobj.callFrameworkMethod()    ③

        print(message)    

    }
    ...
}


上述代码第①行是引入框架产品模块名MyFramework,代码第②行是实例化SwiftObject对象,代码第③行是调用callFrameworkMethod()。输出结果不再赘述。

提示

在运行测试时,请一定选择运行目标为SwiftApp。如图所示,选择目标为SwiftApp,然后选择运行设备或模拟器。

图像说明文字

2 . 基于同一工作空间的不同工程

这种测试方法就是在当前的框架工程中创建另外一个应用目标,通过这个应用目标测试框架目标。这种方法两个目标都在同一个工程中,耦合度很高,适合同一个团队开发。

我们还可以把多个相关的Xcode工程放到一个Xcode工作空间(workspace)中,工作空间是多个工程的集合,在Xcode中工程文件名后缀为.xcodeproj,工作空间文件名后缀是.xcworkspace。一个工作空间中包含应用工程和框架工程,我们可以使用应用工程测试和访问框架工程。这种方法两个目标都在不同工程中,耦合度低,适合不同团队之间的协同开发。

创建工作空间可以通过Xcode菜单File→New→Workspace实现,此时创建的工作空间是空的,没有工程,我们可以添加现有的工程到工作空间中,也可以在工作空间中创建工程。

1) 添加现有的工程到工作空间

添加现有的工程到工作空间与添加一个文件到工程中类似。例如,我们将MyFramework框架工程添加到MyWorkspace工作空间,具体步骤为:打开Xcode的导航面板,在右键菜单中选择Add File to “MyWorkspace”,然后在对话框中选择MyFramework框架工程文件MyFramework.xcodeproj,这样就可以将工程添加到工作空间了。

2) 在工作空间中创建工程

在工作空间中创建工程与一般情况下创建工程稍微有点儿区别。例如,我们创建一个SwiftApp应用工程,具体步骤为:在MyWorkspace工作空间中,选择菜单File→New→Project…,在打开的对话框中选择iOS→ Application→Single View Application,并将语言选择为Swift。创建过程中要选择工作空间(如图23-28所示),在Add to和Group中都选择MyWorkspace,然后点击Create按钮创建工程。

图像说明文字

接下来需要为应用SwiftApp工程SwiftApp目标和框架MyFramework工程MyFramework目标建立依赖关系,具体过程参考本节中的“1. 基于同一工程不同目标”部分。ViewController.swift调用代码也与“1. 基于同一工程不同目标”中完全一样,也不再赘述。

23.5.4 Objective-C调用Swift

这一节我们来介绍同一框架目标中如何通过Objective-C调用Swift来混合编程。首先参考23.5.2节创建iOS框架工程,工程名还是MyFramework,但是这次语言选择为Objective-C。

Objective-C调用Swift时不需要保护伞头文件了,而是需要Xcode生成头文件(Xcode-generated header)。这种文件也是由Xcode生成,不需要我们维护,对于开发人员也是不可见的。如图23-29所示,它能够将Swift中的类暴露给Objective-C,它的命名是“产品名/产品模块名-Swift.h”。我们需要将该头文件引入到Objective-C文件中,而且Swift中的类需要继承NSObject或NSObject子类,还要使用@objc注释属性声明。

图像说明文字

下面我们通过一个示例介绍一下同一框架目标的Objective-C如何调用Swift。

该示例调用的SwiftObject类代码如下:

import Foundation

@objc public class SwiftObject: NSObject {

    private var name: String
    private var greeting: String

    public init(greeting aGreeting: String, name aName: String) {
        self.greeting = aGreeting
        self.name = aName
    }

    override open var description: String {
        let string = String(format: "desc -> name:%@, 
        →greeting:%@", name, greeting)
        return string
    }    
    public func sayHello(_ aGreeting: String, name aName: String) -> String {
        var string = "Hi," + aName + " "
        string += aGreeting + "."
        return string
    }
}


注意,需要暴露的构造函数、方法和属性都要声明为public,而类也要声明为public。

该示例调用了ObjCObject类,代码如下:


    // ObjCObject.h文件
    #import <Foundation/Foundation.h>

    @interface ObjCObject : NSObject

    -(NSString*)callFrameworkMethod;

    @end

    // ObjCObject.m文件
    #import "ObjCObject.h"
    #import <MyFramework/MyFramework-Swift.h>    ①

    @implementation ObjCObject

    -(NSString*)callFrameworkMethod {

        SwiftObject *sobj = [[SwiftObject alloc] 
        initWithGreeting:@"Good morning" name:@"Tom"];

        NSLog(@"%@", sobj.description);

        NSString* hello = [sobj sayHello:@"Good morning" name:@"Tony"];
        NSLog(@"%@",hello);

        return hello;
    }

    @end

callFrameworkMethod()方法暴露给一个应用工程,通过应用工程目标调用。代码第①行#import <MyFramework/MyFramework-Swift.h>语句是引入Xcode生成头文件(注意它的命名规则)。

图23-30所示为测试调用时序图,测试应用调用MyFramework框架ObjCObject对象的-callFrameworkMethod方法。-callFrameworkMethod方法先是实例化MyFramework框架中的SwiftObject对象,接着调用SwiftObject对象的sayHello(_: name:)方法。

图像说明文字

测试同一框架目标中Objective-C调用Swift,与同一框架目标中Swift调用Objective-C类似,也可以用“基于同一工程不同目标”和“基于同一工作空间不同工程”两种方法实现。

我们重点介绍“基于同一工程不同目标”实现。首先参考23.5.3节添加ObjCApp目标。请选择模板iOS→Application→Single View Application,如图23-31所示将语言选择为Objective-C。

图像说明文字

提示

选择Objective-C语言的目的也是便于测试,因为我们要测试MyFramework中的ObjCObject类也是使用Objective-C语言编写的。

接下来需要进行一些配置,首先需要为ObjCApp目标和MyFramework目标建立依赖关系。由于是ObjCApp目标依赖于MyFramework目标,参考23.5.3节选中ObjCApp,选择添加MyFramework. framework,从而建立依赖关系。

建立依赖关系之后,我们来看看ObjCApp目标中的ViewController.m相关代码:


    #import "ViewController.h"

    #import "ObjCObject.h"        ①

    ...

    @implementation ViewController

    - (void)viewDidLoad {
        [super viewDidLoad];

        ObjCObject* obj = [[ObjCObject alloc] init];     ②
        NSString* message = [obj callFrameworkMethod];     ③

        NSLog(@"%@", message);

    }

    ...

    @end

上述代码第①行是引入ObjCObject.h头文件,代码第②行是实例化ObjCObject对象,代码第③行是调用callFrameworkMethod。输出结果不再介绍。

23.6 本章小结

通过对本章内容的学习,广大读者可以了解Swift与Objective-C的混合编程,其中包括:同一应用目标中的混合编程和同一框架目标中的混合编程。

目录

  • 前言
  • 第1 章 准备起航
  • 第2章 第一个Swift程序
  • 第3章 Swift语法基础
  • 第4章 运算符 
  • 第5章 Swift原生数据类型
  • 第6章 Swift简介
  • 第7章 控制语句
  • 第8章 Swift原生集合类型
  • 第9章 函数
  • 第10章 闭包
  • 第11章 Swift语言中的面向对象特性
  • 第12章 属性与下标
  • 第13章 方法
  • 第14章 构造与析构
  • 第15章 类继承
  • 第16章 扩展
  • 第17章 协议
  • 第18章 泛型
  • 第19章 Swift编码规范
  • 第20章 Swift内存管理
  • 第21章 错误处理
  • 第22章 Foundation框架
  • 第23章 Swift与Objective-C混合编程
  • 第24章 Swift与C/C++混合编程
  • 第25章 SpriteKit游戏引擎
  • 第26章 游戏App实战——迷失航线