首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》附录A C、Objective-C与Swift

关灯直达底部

你是一名iOS程序员,并且已经选择使用了Apple的全新语言Swift。这意味着你再也不会关心Apple过去的语言Objective-C了吗?当然不是这样。

Objective-C不死。你可以使用Swift,但Cocoa不行。编写iOS程序涉及与Cocoa及其补充框架的通信。这些框架的API是用Objective-C或其底层语言C编写的。使用Swift向Cocoa发送的消息会被转换为Objective-C。跨越Swift/Objective-C桥所发送或接收的对象都是Objective-C对象。从Swift向Objective-C所发送的一些对象甚至会被转换为其他对象类型或非对象类型。

在跨越语言之间的桥接发送消息时,你需要知道Objective-C期望的到底是什么、Objective-C会如何处理这些消息、Objective-C会返回什么结果,这些结果在Swift中会是什么样子的。应用可能需要包含一些Objective-C代码和Swift代码,因此你需要知道应用内部之间的通信方式。

本附录总结了C与Objective-C的一些语言特性,并介绍了Swift会如何使用这些特性。这里并不会讲述如何编写Objective-C代码!比如,我会谈及Objective-C方法与方法声明,因为你需要知道如何从Swift中调用Objective-C方法;不过,我并不会介绍如何在Objective-C中调用Objective-C方法。本书的上一版系统且详尽地介绍了C与Objective-C,因此我建议你参考它以了解关于这些语言的信息。

A.1 C语言

Objective-C是C的超集;换句话说,C构成了Objective-C的语言基础。C中的一切均可用在Objective-C中。我们可以(通常也是必要的)编写本质上就是纯C的长长的Objective-C代码。一些Cocoa API是用C编写的。因此,为了掌握Objective-C,我们有必要先了解C。

C语句(包括声明)必须以分号结尾。变量在使用前需要先声明。变量声明的语法是:数据类型名后跟变量名,然后跟着初始值的赋值(此为可选):


int i;double d = 3.14159;  

C typedef语句以现有的类型名开始,并为其定义了一个新的同义词:


typedef double NSTimeInterval;  

A.1.1 C数据类型

C并不是面向对象的语言;其数据类型不是对象(它们是标量)。C中基本的内建数据类型都是数字:char(1个字节)、int(4个字节)、float与double(浮点数)及各种变种,如short(短整型)、long(长整型)、unsigned short等。Objective-C增加了NSInteger、NSUInteger(无符号)与CGFloat。C中的布尔类型实际上是个数字,0表示false;Objective-C增加了BOOL,它也是个数字。C中的原生文本类型(字符串)实际上是个以null结尾的字符数组。

Swift显式提供了可以与C数字类型直接交互的数字类型,不过Swift的类型是对象,而C的类型则不是。Swift类型别名提供了与C类型名字相对应的名字:Swift CBool是个C bool、Swift CChar是个C char(一个Swift Int8)、Swift CInt是个C int(一个Swift Int32)、Swift CFloat是个C float(一个Swift Float),诸如此类。Swift Int可以与NSInteger交换使用、Swift UInt可以与NSUInteger交换使用、Swift Bool可以与Swift ObjCBool交换使用,后者表示Objective-C BOOL。CGFloat被Swift作为一个类型名。

C与Swift之间的一个主要差别在于,当对不同的数字类型进行赋值、传递或比较时,C(以及Objective-C)会隐式进行转换;但Swift不会,因此你需要进行显式转换来匹配类型。

原生的C字符串类型(以null结尾的字符数组)在Swift中的类型为UnsafePointer<Int8>(回忆一下,Int8就是个CChar),稍后将会介绍这么做的原因。我们无法在Swift中构造C字符串字面值,不过在需要C字符串时,你可以传递一个Swift String:


let q = dispatch_queue_create("MyQueue", nil)  

如果需要创建C字符串变量,那么可以使用NSString的UTF8String属性与cString-UsingEncoding:方法来构造C字符串。此外,还可以使用Swift String的withCString实例方法,不过其语法有点麻烦。在该示例中,我遍历了C字符串的“字符”,直到遇到null终止符(稍后将会介绍memory属性):


let _ : Void = "hello".withCString {    var cs = $0    while cs.memory != 0 {        print(cs.memory)        cs = cs.successor    }}  

此外,可以通过Swift String的静态方法fromCString将C字符串转换为Swift String(包装在Optional中)。

A.1.2 C枚举

C枚举是个数字;其值是某种形式的整型,可以隐式(从0开始)或显式指定其值。C枚举可以通过各种形式转换为Swift,这取决于其声明方式。下面就从最简单的形式开始:


enum State {     kDead,     kAlive};typedef enum State State;  

(最后一行的typedef可以让C程序使用State代替冗长的enum State作为类型名)。C枚举名kDead与kAlive并不是任何东西的“case”;它们并没有命名空间。它们是常量,由于并未对其进行显式初始化,因此它们分别代表0和1。枚举声明可以进一步指定整型类型;但该示例没有这么做,因此其值在Swift中是UInt32类型。

在Swift 2.0中,老式的C枚举会成为使用了RawRepresentable协议的Swift结构体。当从C传递过来或向C传递了State枚举时,该结构体会自动作为交换的媒介。这样,如果C函数setState接收一个State枚举参数时,你可以通过一个State名字调用它:


setState(kDead)  

通过这种方式,Swift会尽可能以更加方便的方式导入这些名字,将State表示为一种类型,不过在C中它并不是类型。如果想知道名字kDead到底表示什么整数,那只能使用其rawValue。还可以通过调用init(rawValue:)初始化器创建任意的State值,并没有编译器或运行期检查来确保该值是一个预定义的常量。不过也不建议你这么做。

Xcode 4.4引入了一个全新的C枚举符号,它基于NS_ENUM宏:


typedef NS_ENUM(NSInteger, UIStatusBarAnimation) {    UIStatusBarAnimationNone,    UIStatusBarAnimationFade,    UIStatusBarAnimationSlide,};  

该符号显式指定了整型类型并将一个类型名关联到了这个枚举。Swift会将以这种方式声明的枚举用Swift枚举的形式导入,保持其名字与类型不变。此外,Swift会自动将导入的case名前面的共有前缀去掉:


enum UIStatusBarAnimation : Int {    case None    case Fade    case Slide}  

此外,带有Int原生值类型的Swift枚举可以通过@objc特性公开给Objective-C。比如:


@objc enum Star : Int {    case Blue    case White    case Yellow    case Red}  

Objective-C会将其看作一个NSInteger类型的枚举,枚举名分别是StarBlue、StarWhite等。

还有另外一个C枚举符号的变种,它基于NS_OPTIONS宏,适合于位掩码:


typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {    UIViewAutoresizingNone = 0,    UIViewAutoresizingFlexibleLeftMargin = 1 << 0,    UIViewAutoresizingFlexibleWidth = 1 << 1,    UIViewAutoresizingFlexibleRightMargin = 1 << 2,    UIViewAutoresizingFlexibleTopMargin = 1 << 3,    UIViewAutoresizingFlexibleHeight = 1 << 4,    UIViewAutoresizingFlexibleBottomMargin = 1 << 5};  

这种方式声明的枚举会以使用了OptionSetType协议的结构体的形式进入Swift中。OptionSetType协议使用了RawRepresentable协议,因此该结构体有一个rawValue实例属性,它持有底层的整型值。C枚举的case名是通过静态属性表示的,其每个值都是该结构体的实例;在导入这些静态属性名时会去掉其共有前缀:


struct UIViewAutoresizing : OptionSetType {    init(rawValue: UInt)    static var None: UIViewAutoresizing { get }    static var FlexibleLeftMargin: UIViewAutoresizing { get }    static var FlexibleWidth: UIViewAutoresizing { get }    static var FlexibleRightMargin: UIViewAutoresizing { get }    static var FlexibleTopMargin: UIViewAutoresizing { get }    static var FlexibleHeight: UIViewAutoresizing { get }    static var FlexibleBottomMargin: UIViewAutoresizing { get }}  

这样,调用UIViewAutoresizing.FlexibleLeftMargin时就好像在初始化Swift枚举的一个case,不过实际上,它是UIViewAutoresizing结构体的一个实例,其rawValue属性会被设为原来的C枚举所声明的值,对于.FlexibleLeftMargin来说就是1<<0。由于该结构体的静态属性是相同结构体的一个实例;因此像枚举一样,在需要结构体时,可以提供一个静态属性名并省略结构体名:


self.view.autoresizingMask = .FlexibleWidth  

此外,由于这是个OptionSetType结构体,因此可以使用集合相关操作。这样就可以通过实例来操纵位掩码了,就好像它是个Set一样:


self.view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]   

如果Objective-C中需要一个NS_OPTIONS枚举,那么可以通过传递0来表示没有提供任何选项。在Swift 2.0中,如果需要相应的结构体,那么可以传递(一个空集合)。

但遗憾的是,很多常见的替代方案一开始并没有以枚举的形式实现出来。这不是什么问题,但却很不方便。比如,AVFoundation音频会话类别名只不过是NSString常量而已:


NSString *const AVAudioSessionCategoryAmbient;NSString *const AVAudioSessionCategorySoloAmbient;NSString *const AVAudioSessionCategoryPlayback;// ... and so on ...  

虽然这个列表还有着显而易见的共有前缀,但Swift却不能通过缩略名将其转换为AVAudioSessionCategory枚举或结构体。如果想要指定Playback类别,你需要使用全名AVAudioSessionCategoryPlayback。

A.1.3 C结构体

C结构体是个复合类型,其元素可以通过名字来访问,方式是在对结构体的引用后使用点符号。比如:


struct CGPoint {   CGFloat x;   CGFloat y;};typedef struct CGPoint CGPoint;  

声明之后,在C中就可以这样使用了:


CGPoint p;p.x = 100;p.y = 200;  

C结构体进入Swift中后就会变成Swift结构体,然后就拥有了Swift结构体的特性。比如,Swift中的CGPoint会拥有x与y CGPoint实例属性;不过,它还会神奇地拥有隐式的成员初始化器!此外,还会注入一个不带参数的初始化器;因此,CGPoint()会创建一个x与y值都为0的CGPoint。扩展可以提供额外的特性,Swift CoreGraphics头会向CGPoint添加一些:


extension CGPoint {    static var zeroPoint: CGPoint { get }    init(x: Int, y: Int)    init(x: Double, y: Double)}  

如你所见,Swift CGPoint拥有一些额外的初始化器,接收Int或Double参数,还有另外一种创建0值CGPoint的方式CGPoint.zeroPoint。CGSize与之类似。特别地,Swift中的CGRect还会拥有额外的方法与属性;如果无法通过Core Graphics框架提供的内建C函数来操纵CGRect,那么你也不能通过这些额外的方法实现;不过,你可以以Swift的方式来做到这一点。

Swift结构体是对象,C结构体却不是,不过这一点并不会对通信造成任何影响。比如,你可以在需要C CGPoint时赋值或传递一个Swift CGPoint,因为CGPoint首先来自于C。Swift为CGPoint添加了对象方法与属性,不过这也没关系;C看不到它们。C所关心的只是该CGPoint的x与y元素,而它们可以轻松从Swift传递给C。

A.1.4 C指针

C指针是个整型,它指向了内存中真实数据的位置(地址)。分配与回收内存是分开进行的。指向某个数据类型的指针声明是通过在数据类型名后加上一个星号实现的;星号左侧、右侧或两侧可以添加空格。如下代码声明了一个指向int的指针,它们是等价的:


int *intPtr1;int* intPtr2;int * intPtr3;  

类型名本身是int*(或加上一个空格,即int*)。如前所述,Objective-C重度使用了C指针,查看Objective-C代码就会发现很多地方都会出现星号。

C指针转换为Swift后会变成UnsafePointer,如果可写则会变成UnsafeMutablePointer;这是个泛型,并且特定于所指向的实际数据类型(指针是“不安全的”,因为Swift并不会管理其内存,甚至都不会保证所指数据的完整性)。

比如,下面是个C函数声明;之前并没有介绍过C函数语法,不过请重点关注每个参数名前的类型即可:


void CGRectDivide(CGRect rect,    CGRect *slice,    CGRect *remainder,    CGFloat amount,    CGRectEdge edge)  

关键字void表示该函数不返回值。CGRect与CGRectEdge都是C结构体;CGFloat则是个基本的数字类型。CGRect*slice与CGRect*remainder(空格的位置不同,不过没关系)表示slice与remainder都是CGRect*,即指向CGRect的指针。上述声明转换为Swift后将如下所示:


func CGRectDivide(rect: CGRect,    _ slice: UnsafeMutablePointer<CGRect>,    _ remainder: UnsafeMutablePointer<CGRect>,    _ amount: CGFloat,    _ edge: CGRectEdge)  

该上下文中的UnsafeMutablePointer类似于Swift inout参数:你提前声明并初始化了一个恰当类型的var,然后通过&前缀运算符将其地址作为参数进行传递。当以这种方式传递引用地址时,实际上会创建并传递一个指针:


var arrow = CGRectZerovar body = CGRectZeroCGRectDivide(rect, &arrow, &body, Arrow.ARHEIGHT, .MinYEdge)  

在C中,要想访问指针所指向的内存,可以在指针名前使用一个星号:*intPtr表示“指针intPtr所指向的东西”。在Swift中,你可以使用指针的memory属性。

在该示例中,我们会接收到一个stop参数,其原始类型为BOOL*,即一个指向BOOL的指针;在Swift中,它是个UnsafeMutablePointer<ObjCBool>。要想设置指针所指向的这个BOOL,我们需要设置指针的memory(mas是个NSMutableAttributedString):


mas.enumerateAttribute("HERE", inRange: r, options: ) {    value, r, stop in    if let value = value as? Int where value == 1 {        // ...        stop.memory = true    }}  

最通用的C指针类型是指向void的指针(void*),也叫作通用指针。这里的void表示没有指定类型;在C中,如果需要具体类型的指针,那么我们是可以使用通用指针的,反之亦然。实际上,指向void的指针不会再对指针所指向的东西进行类型检查。在Swift中,这是个特定于Void的指针,一般来说就是UnsafeMutablePointer<Void>或相应的UnsafeMutablePointer<()>。一般来说,当遇到这种类型的指针时,如果需要访问底层数据,那么首先要将UnsafeMutablePointer泛型转换为底层数据的类型。

A.1.5 C数组

C数组包含了某种数据类型固定数目的元素。在底层,其所占据的内存大小等于该数据类型的这些固定数目的元素所占据的内存大小总和。由于这一点,C中的数组名就是指针名,它指向了数组中的首个元素。比如,如果将arr声明为一个int数组,那么arr就可以用在需要int*(指向int的指针)类型值的地方。C语言通过对引用使用方括号或使用指针来表示数组类型。

(这也说明了为何Swift中涉及C字符串的字符串方法会将这些字符串当作指向Int8的不安全的指针:C字符串是个字符数组,而Int8是个字符。)

比如,C函数CGContextStrokeLineSegments的声明如下所示:


void CGContextStrokeLineSegments(CGContextRef c,   const CGPoint points,   size_t count);  

第2个参数是个CGPoint类型的C数组;这是根据方括号得出的结论。C数组并不会表示出其中所包含的元素个数,因此要想将该C数组传递给这个函数,你还需要告诉函数数组中所包含的元素个数;这正是第3个参数的意义。CGPoint类型的C数组是个指向CGPoint的指针,因此该函数声明转换为Swift后如下所示:


func CGContextStrokeLineSegments(c: CGContext?,    _ points: UnsafePointer<CGPoint>,    _ count: Int)  

要想调用该函数并将其传递给CGPoint类型的C数组,你需要创建一个CGPoint类型的C数组。C数组并不是Swift数组;那么该如何做到这一点呢?其实你什么都不用做。虽然Swift数组不是C数组,但你可以传递一个指向Swift数组的指针。实际上,你甚至都不需要传递指针,你可以传递一个对Swift数组本身的引用。由于这并不是一个可变指针,因此可以通过let声明数组;事实上,你甚至可以传递一个Swift数组字面值!无论选择哪种方式,Swift都会帮你转换为C数组,并将其作为参数跨越从Swift到C的桥梁:


let c = UIGraphicsGetCurrentContext!let arr = [CGPoint(x:0,y:0),    CGPoint(x:50,y:50),    CGPoint(x:50,y:50),    CGPoint(x:0,y:100),]CGContextStrokeLineSegments(c, arr, 4)  

不过,如果需要,你还是可以构造出一个C数组。要想做到这一点,首先要留出内存块:声明一个所需类型的UnsafeMutablePointer,调用静态方法alloc并传递所需的元素数量。接下来通过下标将元素直接写进去来初始化内存。最后,由于UnsafeMutablePointer是个指针,你将其(而不是指向它的指针)作为参数进行传递:


let c = UIGraphicsGetCurrentContext!let arr = UnsafeMutablePointer<CGPoint>.alloc(4)arr[0] = CGPoint(x:0,y:0)arr[1] = CGPoint(x:50,y:50)arr[2] = CGPoint(x:50,y:50)arr[3] = CGPoint(x:0,y:100)CGContextStrokeLineSegments(c, arr, 4)  

接收到C数组时也可以使用同样便捷的下标。比如:


let col = UIColor(red: 0.5, green: 0.6, blue: 0.7, alpha: 1.0)let comp = CGColorGetComponents(col.CGColor)  

上述代码执行完毕后,comp的类型就是个指向CGFloat的UnsafePointer。这实际上意味着它是个CGFloat类型的C数组,你可以通过下标访问其元素:


if let sp = CGColorGetColorSpace(col.CGColor) {   if CGColorSpaceGetModel(sp) == .RGB {       let red = comp[0]       let green = comp[1]       let blue = comp[2]       let alpha = comp[3]       // ...    }}  

A.1.6 C函数

C函数声明以返回类型开始(返回类型可能为void,表示没有返回值),后跟函数名,然后是一对圆括号,括号里面是逗号分隔的参数列表,列表中的每一项都是由类型与参数名构成的。参数名都是内部使用的,调用C函数时不会用到它们。

下面是CGPointMake的C声明,它返回一个初始化过的CGPoint:


CGPoint CGPointMake (    CGFloat x,    CGFloat y);  

下面展示了如何在Swift中调用它:


let p = CGPointMake(50,50)  

在Objective-C中,CGPoint并不是对象,CGPointMake是创建CGPoint的主要方式。如前所述,Swift提供了初始化器,不过我个人仍然倾向于使用CGPointMake。

在C中,函数有一个类型,这是根据其签名得来的,函数名就是对函数的引用,因此在需要某个类型的函数时,我们可以通过函数名来传递这个函数(这有时也叫作指向函数的指针)。在声明中,指向函数的指针可以通过在圆括号中使用星号来表示。

比如,下面是Audio Toolbox框架的一个C函数的声明:


extern OSStatusAudioServicesAddSystemSoundCompletion(SystemSoundID inSystemSoundID,    CFRunLoopRef __nullable inRunLoop,    CFStringRef __nullable inRunLoopMode,    AudioServicesSystemSoundCompletionProc inCompletionRoutine,    void * __nullable inClientData)  

(现在请忽略__nullable,稍后将会对其进行介绍;extern也不用管,后面也不会介绍它)。SystemSoundID仅仅是个UInt32而已。不过,AudioServicesSystemSoundCompletionProc是什么呢?它是:


typedef void (*AudioServicesSystemSoundCompletionProc)(SystemSoundID ssID,    void* __nullable clientData);  

SystemSoundID是个UInt32,它告诉你在C用于表示这个含义的令人费解的语法中,AudioServicesSystemSoundCompletionProc是个指向函数的指针,该函数接收两个参数(类型为UInt32以及指向void的指针),不返回结果。

在Swift 1.2及之前版本中,调用AudioServicesAddSystemSoundCompletion的唯一方式是在Objective-C中构造AudioServicesSystemSoundCompletionProc。这个C函数的参数类型为CFunctionPointer,这是个细节未知的结构体,你无法在Swift中创建。

不过在Swift 2.0中,你可以在需要C中指向函数的指针的情况下传递一个Swift函数!与往常一样,在传递函数时,你可以单独定义函数并传递其名字,或是以内联的方式将函数构造为匿名函数。如果准备单独定义函数,那么它必须要是个函数,这意味着它不能是方法。定义在文件顶部的函数是可以的;定义在函数内部的函数也是没问题的。

如下是我编写的AudioServicesSystemSoundCompletionProc,它声明在文件顶部:


func soundFinished(snd:UInt32, _ c:UnsafeMutablePointer<Void>) -> Void {    AudioServicesRemoveSystemSoundCompletion(snd)    AudioServicesDisposeSystemSoundID(snd)}  

这是用于播放音频文件(作为系统声音)的代码,包含了对AudioServicesAddSystemSoundCompletion的调用:


let sndurl =    NSBundle.mainBundle.URLForResource("test", withExtension: "aif")!var snd : SystemSoundID = 0AudioServicesCreateSystemSoundID(sndurl, &snd)AudioServicesAddSystemSoundCompletion(snd, nil, nil, soundFinished, nil)AudioServicesPlaySystemSound(snd)  

A.2 Objective-C

Objective-C构建在C之上。它添加了一些语法与特性,不过继续使用着C语法与数据类型,其底层依然是C。

与Swift不同,Objective-C没有命名空间。出于这个原因,不同框架通过不同的前缀作为名字的开始以进行区分。“CGFloat”中的“CG”表示Core Graphics,因为它声明在Core Graphics框架中。“NSString”中的“NS”表示NeXTStep,这是Cocoa框架过去的名字,诸如此类。

A.2.1 Objective-C对象与C指针

所有的C数据类型与语法都是Objective-C的一部分。不过,Objective-C还是面向对象的,因此它需要通过某种方式向C中添加对象。它是通过C指针做到这一点的。C指针可以指向任何东西;对所指向的目标的管理是另外一件事,这正是Objective-C所关注的。这样,Objective-C对象类型都是通过C指针语法来表达的。

比如,下面是addSubview:方法的Objective-C声明:


- (void)addSubview:(UIView *)view;  

目前还没有介绍过Objective-C方法声明语法,不过请将注意力放在圆括号中view参数的类型声明上:它是个UIView*。看起来表示的好像是“指向UIView的指针”。说是也是,说不是也不是。所有的Objective-C对象引用都是指针。这样,说它是指针只是表示它是个对象而已。指针的另一边则是个UIView实例。

不过,将该方法转换为Swift后就看不到任何指针的影子了:


func addSubview(view: UIView)  

一般来说,当Objective-C需要一个类实例时,在Swift中只需传递一个指向类实例的引用即可;Objective-C声明中会通过星号来表示对象,不过你不用管这些。从Swift中调用addSubview:方法时作为参数传递的是个UIView实例。你会有这样一种感觉,当传递类实例时,实际传递的是一个指针,因为类实例是引用类型。这样,Swift与Objective-C看待类实例的方式其实是一样的。区别在于Swift不会使用指针符号。

Objective-C的id类型是个指向对象的通用指针,相当于指向void的指针对象。任何对象类型都可以赋值给id,也可以转换为id,还可以通过id来构造(Swift的AnyObject与之类似)。由于id本身就是个指针,因此声明为id的引用并不会使用星号;你可能永远都看不到id*这种写法。

A.2.2 Objective-C对象与Swift对象

Objective-C对象是类与类的实例,进入Swift中后基本上都是原封不动的。你在子类化Objective-C类或使用Objective-C类实例时不会遇到任何问题。

反之亦然。如果Objective-C需要一个对象,那么它实际上需要的是一个类,Swift则可以提供。对于最一般的情况来说,即Objective-C需要一个id,你可以传递任何一个类型使用了AnyObject的实例,也就是说,其类型是个类。此外,Swift还会将某些非类的类型转换为Objective-C类的等价物。如下结构体可以转换为AnyObject,并且在Objective-C需要对象时能够自动桥接为Objective-C类类型:

·String到NSString

·Int、UInt、Double、Float与Bool到NSNumber

·Array到NSArray

·Dictionary到NSDictionary

·Set到NSSet

Swift的自动桥接使得数字类型的处理要比在Objective-C中容易得多。Swift Int可用在需要Objective-C对象的地方,因为Swift会将其包装为一个NSNumber;在Objective-C中,你就得记得将一个整型包装到NSNumber中。

只要元素类型是类类型或是可以桥接为类类型(可以转换为AnyObject),并且不是Optional(因为Objective-C集合中不能包含nil),那么Swift集合(Array、Dictionary与Set)就可以桥接为Objective-C集合。

Swift可以看到Objective-C类类型的方方面面(请参考第10章了解Swift是如何看到Objective-C属性的)。不过,很多Swift类型是Objective-C所看不到的(也没什么问题)。Objective-C无法看到如下类型:

·Swift枚举,除了拥有Int原生值的@objc枚举。

·Swift结构体,除了可以桥接的或是最终来自于C的那些结构体。

·没有从NSObject继承的Swift类。

·嵌套类型、泛型与元组。

虽然Objective-C可以看到Swift类型,但却无法看到类型里面的某些属性(属性的类型是Swift所无法看到的),如果方法的参数或返回值类型是Swift所看不到的,那么这些方法也是看不到的。你可以自由使用这些属性与方法,甚至在Objective-C类类型的子类或扩展中;Objective-C对此没有什么问题,因为对于Objective-C来说,它们根本就不存在。

如果Objective-C能够看到某个类型,那么它就能看到包装该类型的Optional,除了数字类型。比如,Objective-C无法看到类型为Int?的属性。推测起来这是因为Int无法直接桥接到Objective-C;需要将其包装到NSNumber中,但仅仅通过类型声明是做不到这一点的。

obj特性会向Objective-C公开一些通常情况下Objective-C无法看到的东西,前提是满足合法性要求。它还有另外一个目的:在将某个东西标记为@obj时,你可以添加一对圆括号,里面包含着你希望Objective-C看到的该成员的名字。你甚至可以自由对Objective-C所能看到的类或类成员这么做,比如:


@objc(ViewController) class ViewController : UIViewController { // ...  

上述代码演示了在实际开发中颇具价值的一件事。在默认情况下,Objective-C会认为你的类名是根据模块名(一般来说就是项目名)划分了命名空间(前缀)的。这样,该ViewController类就会被Objective-C当作MyCoolApp.ViewController。这会破坏类名与其他东西之间的关联关系。比如,在将现有的Objective-C项目转换为Swift时,你可能会使用@objc(...)语法防止nib对象或NSCoding归档失去与其关联类的关联关系。

A.2.3 Objective-C方法

在Objective-C中,方法参数可以有自己的名字,整体来看,方法名与参数名是一样的。参数名是方法名的一部分,每个参数名后都有一个冒号。比如,UIViewController类有一个名为presentViewController:animated:completion:的实例方法。方法名中包含了3个冒号,因此这个方法会接收3个参数。如下代码展示了如何在Objective-C中调用它:


SecondViewController* svc = [SecondViewController new];[self presentViewController:svc animated:YES completion:nil];  

一个Objective-C方法的声明包含如下3部分:

·一个+或-,分别表示方法是类方法还是实例方法。

·一对圆括号,里面是返回值的数据类型。它可能是void,表示没有返回值。

·方法名,通过冒号分隔以便为参数留出位置。每个冒号后是个圆括号,里面是参数的数据类型,后跟参数的占位符名。

比如,UIViewController实例方法presentViewController:animated:completion:的Objective-C声明如以下代码所示:


- (void)presentViewController: (UIViewController *)viewControllerToPresent    animated: (BOOL)flag    completion: (void (^ __nullable)(void))completion;  

(看起来比较奇怪的第3个参数类型是个块,稍后将会介绍。)

回忆一下,在默认情况下,Swift方法会外化除第一个参数外的所有其他参数名。因此,Objective-C方法声明会按照如下规则转换为Swift:

·第一个冒号前面的内容会成为函数名。

·除了第一个冒号,其他每个冒号前面的内容会成为一个外部参数名。第一个参数没有外部名。

·参数类型后面的名字会成为内部(局部)参数名。如果外部参数名与内部(局部)参数名同名,那就没必要再重复声明一次了。

这样,上述Objective-C方法声明转换为Swift后如以下代码所示:


func presentViewController(viewControllerToPresent: UIViewController,    animated flag: Bool,    completion: ( -> Void)?)  

当在Swift中调用方法时,内部参数名是不起作用的:


let svc = SecondViewControllerself.presentViewController(svc, animated: true, completion: nil)  

在实现Objective-C中声明的方法时需要遵循所使用的协议或重写继承下来的方法。Xcode的代码完成特性会帮你提供好内部参数名,不过你可以修改它们。不过,外部参数名是不能修改的;它们是方法名的一部分!

这样,如果要重写presentViewController:animated:completion:(你可能不会这么做),那么可以像下面这样做:


override func presentViewController(vc: UIViewController,    animated anim: Bool,    completion handler: ( -> Void)?) {    // ...}  

与Swift不同,Objective-C并不允许方法重载。在Objective-C中,如果两个ViewController实例方法都叫作myMethod:并且不返回结果,其中一个方法接收CGFloat参数,另一个方法接收NSString参数,那么这是不合法的。因此,对于这样两个Swift方法来说,虽然在Swift中是合法的,但如果它们都对Objective-C可见,结果就是不合法的了。在Swift 2.0中,你可以通过@nonobjc特性将某些正常情况下对Objective-C可见的东西隐藏。这样,将其中一个方法标记为@nonobjc就可以解决问题。

Objective-C有自己的可变参数形式。比如,NSArray实例方法arrayWithObjects:的声明如下所示:


+ (id)arrayWithObjects:(id)firstObj, ... ;  

与Swift不同,我们必须得显式告诉这样的方法所提供的参数个数。很多这样的方法(包括arrayWithObjects:)都使用了nil终止符;也就是说,调用者在最后一个参数后会提供一个nil,被调用者知道最后一个参数是什么时候传递的,因为它遇到了nil。在Objective-C中是这样调用arrayWithObjects:的:


NSArray* pep = [NSArray arrayWithObjects: manny, moe, jack, nil];  

Objective-C无法调用(也看不到)接收可变参数的Swift方法。不过,Swift却可以调用接收可变参数的Objective-C方法,只要方法被标识为NS_REQUIRES_NIL_TERMINATION即可。arrayWithObjects:就是通过这种方式标记的,因此可以这样使用NSArray(objects:1,2,3),Swift会提供缺失的nil终止符。

A.2.4 Objective-C初始化器与工厂

Objective-C初始化器方法是实例方法;实际的实例化是由NSObject的类方法alloc执行的,Swift并未提供对应之物(其实也不需要),初始化器消息会发送给生成的实例。比如,如下代码展示了如何在Objective-C中通过提供red、green、blue与alpha值来创建UIColor实例:


UIColor* col = [[UIColor alloc] initWithRed:0.5 green:0.6 blue:0.7 alpha:1];  

在Objective-C中,这个初始化器的名字是initWithRed:green:blue:alpha:。其声明如下所示:


- (UIColor *)initWithRed:(CGFloat)red green:(CGFloat)green    blue:(CGFloat)blue alpha:(CGFloat)alpha;  

简而言之,初始化器方法从外表来看就是个实例方法,与Objective-C中的其他实例方法一样。

不过,Swift可以检测到Objective-C中的初始化器,因为其名字很特殊,以init开头。因此,Swift可以将Objective-C初始化器转换为Swift初始化器。

这种转换是以一种特殊的方式进行的。与普通方法不同,Objective-C初始化器在转换为Swift时会将所有参数名作为圆括号中的外部名。同时,第一个参数的外部名会自动缩短:单词init会从第一个参数名的开头去掉,如果存在单词With,它也会被去掉。这样,Swift中该初始化器的第一个参数的外部名就是red:。如果外部名与内部名相同,那就没必要重复使用了。这样,Swift会将Objective-C的initWithRed:green:blue:alpha:转换为Swift初始化器init(red:green:blue:alpha:),其声明如下所示:


init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)  

下面是调用:


let col = UIColor(red: 0.5, green: 0.6, blue: 0.7, alpha: 1.0)  

还有一种方式可以在Objective-C中创建实例。很多时候,类会提供一个类方法,这个类方法是实例工厂。比如,UIColor类有一个类方法colorWithRed:green:blue:alpha:,其声明如下所示:


+ (UIColor*) colorWithRed: (CGFloat) red green: (CGFloat) green                     blue: (CGFloat) blue alpha: (CGFloat) alpha;  

Swift会通过一些模式匹配规则检测到这种工厂方法,即返回类实例的类方法,其名字以类名开头并去掉了前缀,同时会将其转换为初始化器,并去掉第一个参数名开头的类名(以及With)。

如果得到的初始化器已经存在,就像这个示例中那样,那么Swift就会认为这个工厂方法是多余的,并且不再使用它!这样,Objective-C的类方法colorWithRed:green:-blue:alpha:就无法从Swift调用,因为它与已经存在的init(red:green:blue:alpha:)相同。

这种同名规则反过来也是适用的:比如,Swift初始化器init(value:)会被Objective-C看作initWithValue:并调用。

A.2.5 选择器

有时,Objective-C方法希望接收一个稍后会被调用的方法名作为参数。这样的名字叫作选择器。比如,addTarget:action:forControlEvents:方法调用时会告诉界面上的按钮:“从现在起,当用户轻拍你时,请将这条消息发送给这个对象。”消息action:参数就是个选择器。

可以这么想,如果这是个Swift方法,那么你会传递一个函数。不过,选择器与函数不同。它仅仅是个名字而已。与Swift不同,Objective-C是动态的,它可以在运行期只根据名字即可构建并向任意对象发送任意消息。

不过,虽然仅仅是个名字,选择器并非字符串。实际上,它是个独立的对象类型,在Objective-C声明中被指定为SEL,在Swift声明中被指定为Selector。不过在大多数情况下,如果需要一个选择器,那么Swift还是允许你传递一个字符串的,这是一种简便方式!比如:


b.addTarget(self, action: "doNewGame:", forControlEvents: .TouchUpInside)  

有时,你需要构造实际的Selector对象,这可以通过将字符串转换为Selector来实现。在该示例中,Selector是一个参数,我们需要通过比较来确定它。不能将Selector与字符串进行比较,因此需要将字符串转换为Selector,从而比较两个Selector:


override func canPerformAction(action: Selector,    withSender sender: AnyObject!) -> Bool {        if action == Selector("undo:") { // ...  

在提供选择器时,如果要获得它的名字该怎么办呢?如果调用了addTarget:action:for-ControlEvents:这样的方法,并且在提供action:参数时搞错了方法名,那么编译期是不会有错误和警告的,不过Objective-C会尝试将这个错误的消息发送给目标,这时应用就会崩溃,控制台会打印出“unrecognized selector”消息。这是为数不多的Swift给Objective-C程序员带来麻烦的地方,而这本可以避免(我认为这是Swift语言的一个严重的问题)。

要想得到正确的名字,你需要将Swift方法声明转换为对应的Objective-C名字。这种转换很简单,并且遵循着一些确定的原则,不过你会将这个名字作为字面值输入,这太容易敲错了,因此请小心行事:

1.名字以方法名中左圆括号前面的字符开头。

2.如果方法不接收参数,那就结束了。

3.如果方法接收参数,那么请添加一个冒号。

4.如果方法接收多个参数,那么请添加除第一个参数外的其他所有参数的外部名,并且在每个外部参数名后加上一个冒号。

这意味着如果方法接收参数,那么其Objective-C名字就会以一个冒号结尾。这里是区分大小写的,除了冒号,名字不应该包含任何空格或其他符号。

下面就来说明一下,这里有3个Swift方法声明,注释中给出的是其对应的Objective-C名字:


func sayHello -> String // "sayHello"func say(s:String) // "say:"func say(s:String, times n:Int) // "say:times:"  

如果不喜欢外化Swift方法的第1个参数名,那么Objective-C对方法名的第一部分添加了"With"和大写的外部参数名。比如:


func say(string s:String) // "sayWithString:"  

即便选择器名能够正确对应上所声明的方法,应用还是可能会崩溃。比如,下面是个简单的测试类,它创建了一个NSTimer,并让其每隔一秒钟调用某个方法一次:


class MyClass {    var timer : NSTimer?    func startTimer {        self.timer = NSTimer.scheduledTimerWithTimeInterval(1,            target: self, selector: "timerFired:",            userInfo: nil, repeats: true)    }    func timerFired(t:NSTimer) {        print("timer fired")    }}  

从结构上来看,这个类没有任何问题;它可以编译通过,并且当应用运行时会实例化。不过在调用startTimer时,应用会崩溃。问题并不是因为timerFired不存在,或"timerFired:"不是其名字;问题在于Cocoa找不到timerFired。这是因为MyClass类是个纯Swift类;因此,它缺少Objective-C的内省能力与消息发送机制,而Cocoa正是通过它们发现并调用timerFired的。这个问题有如下几种解决方案:

·将MyClass声明为NSObject子类。

·声明timerFired时加上@objc特性。

·声明timerFired时加上dynamic关键字(不过这么做有些过犹不及;当Objective-C需要修改类成员实现时需要用到dynamic,不过不应该过度使用这个关键字)。

A.2.6 CFTypeRefs

CFTypeRef是个全局C函数,调用起来也很简单。其代码看起来给人的感觉就好像Swift跟C一样。

要想了解CFTypeRef假对象及其内存管理,请参见第12章。CFTypeRef是个指针,因此它可以与C中指向void的指针互换。由于它是个指向假对象的指针,因此它可以与Objective-C id和Swift AnyObject互换。

很多CFTypeRefs可以自动桥接到相应的Objective-C对象类型。比如,CFString与NSString、CFNumber与NSNumber、CFArray与NSArray、CFDictionary与NSDictionary都是自动桥接的(除此之外还有很多)。每一对都可以通过类型转换进行互换,有时也需要这么做。此外,在Swift中要比Objective-C中更容易一些。在Objective-C中,你需要执行桥接转换,告诉Objective-C当这个对象跨越了Objective-C的内存管理方式与C和CFTypeRefs的非托管内存管理方式时该如何管理其内存。不过在Swift中,CFTypeRefs的内存是托管的,因此没必要进行桥接转换;你只需进行普通的转换即可。实际上在很多情况下,Swift都知道自动桥接,并且会自动进行类型转换。

比如,如下代码来自于我开发的一个应用,这里使用了ImageIO框架。该框架有一个C API并使用了CFTypeRefs。CGImageSourceCopyPropertiesAtIndex会返回一个CFDictionary,其键是CFStrings。从字典中获取值最简单的方式是通过下标,不过无法对CFDictionary这么做,因为它并不是对象;因此,我将其转换为NSDictionary。键kCGImagePropertyPixelWidth是个CFString,它并非Hashable(它并不是一个真正的对象,不能使用协议),因此无法作为Swift字典的键;不过,当我尝试直接通过下标来使用它时,Swift是允许的,因为它会帮我将其转换为NSString:


let result =    CGImageSourceCopyPropertiesAtIndex(src, 0, nil)! as [NSObject:AnyObject]let width = result[kCGImagePropertyPixelWidth] as! CGFloat  

与之类似,在如下代码中,我通过CFString键构造了一个字典d并将其传递给CGImageSourceCreateThumbnailAtIndex函数(该函数接收一个CFDictionary)。我不需要显式做任何强制类型转换!不过,我需要指定字典类型,从而让Swift能帮我将所有键和值转换为Objective-C对象:


let d : [NSObject:AnyObject] = [    kCGImageSourceShouldAllowFloat : true,    kCGImageSourceCreateThumbnailWithTransform : true,    kCGImageSourceCreateThumbnailFromImageAlways : true,    kCGImageSourceThumbnailMaxPixelSize : w]let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, d)!  

A.2.7 块

块是Apple在iOS 4中引入的一个C语言特性。它非常类似于C函数,但并不是C函数;其行为类似于闭包,可以作为引用类型进行传递。实际上,块相当于Swift函数并与之兼容,它们之间可以互换:当需要块时,你可以传递一个Swift函数;当Cocoa将块传递给你时,它看起来就像是函数一样。

在C与Objective-C中,块的声明是通过插入符号(^)表示的,它可以用在C函数声明中函数名出现的地方(或是圆括号中的星号)。比如,NSArray的实例方法sortedArrayUsingComparator:接收一个NSComparator参数,它是通过typedef定义的,如以下代码所示:


typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);  

要想读懂上述声明,请从中间开始,然后向两边看;它表示的是“NSComparator是块的类型,它接收两个id参数并返回一个NSComparisonResult”。因此在Swift中,该typedef会被转换为:


typealias NSComparator = (AnyObject, AnyObject) -> NSComparisonResult  

很多时候都没有typedef,块的类型会直接出现在方法声明中。下面是Objective-C中UIView类方法的声明,它接收两个块参数:


+ (void)animateWithDuration:(NSTimeInterval)duration    animations:(void (^)(void))animations    completion:(void (^ __nullable)(BOOL finished))completion;  

在上述声明中,animations:是个块,它不接收参数(void),也没有返回值;completion:也是个块,它接收一个类型为BOOL的参数,没有返回值。下面是转换后的Swift代码:


class func animateWithDuration(duration: NSTimeInterval,    animations:  -> Void,    completion: ((Bool) -> Void)?)  

对于这些方法来说,在调用时如果需要一个块参数,那么可以将函数作为参数传递进去。下面这个方法示例中,一个函数会传递给你。这是其Objective-C声明:


- (void)webView:(WKWebView *)webView    decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction    decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;  

实现这个方法后,当用户轻拍了Web View上的链接时,它会被调用,从而可以决定该如何响应。第3个参数是个块,它接收一个枚举类型的参数WKNavigationActionPolicy,并且没有返回值。块会作为一个Swift函数传递给你,你通过调用该函数作出响应:


func webView(webView: WKWebView,    decidePolicyForNavigationAction navigationAction: WKNavigationAction,    decisionHandler: ((WKNavigationActionPolicy) -> Void)) {       // ...       decisionHandler(.Allow)}  

在Objective-C中,块可以转换为id。不过,Swift函数却无法转换为AnyObject。然而,有时在Objective-C中,当需要id时你可以提供块;你希望在Swift中也可以这样,当需要AnyObject时可以提供Swift函数。比如,一些对象类型(如CALayer与CAAnimation)允许使用键值编码来追加任意键值对并在后面获取到它;将函数作为值追加上去也是合情合理的。

一个简单的解决办法就是声明一个NSObject子类,其中包含一个函数类型的属性:


typealias MyStringExpecter = (String) -> class StringExpecterHolder : NSObject {     var f : MyStringExpecter!}  

现在可以将函数包装到类实例中:


func f (s:String) {print(s)}let holder = StringExpecterHolderholder.f = f  

接下来在需要AnyObject时将该实例传递过去:


/let lay = CALayerlay.setValue(holder, forKey:"myFunction")  

后面就可以抽取该实例,将其从AnyObject进行向下类型转换,并调用它所包装的函数,这一切都是非常简单的:


let holder2 = lay.valueForKey("myFunction") as! StringExpecterHolderholder2.f("testing")  

C函数并不是块,不过在Swift 2.0中,你还可以在需要C函数的地方使用Swift函数,这一点在之前已经介绍过了。另外,为了将某个类型声明为C中指向函数的指针,请将类型标记为@convention(c)。比如,如下是两个Swift方法声明:


func blockTaker(f:->) {}func functionTaker(f:@convention(c) -> ) {}  

Objective-C将第1个看作接收一个Objective-C块,将第2个看作接收一个C中的指向函数的指针。

A.2.8 API标记

当Swift于2014年6月首次进入公众视线时,人们认为其严格、具体的类型相对于Objective-C动态、松散的类型来说很不相配。主要的问题有:

·在Objective-C中,任何对象实例引用都可以为nil。不过在Swift中,只有Optional才能为nil。默认的解决方案是将隐式展开的Optional作为Objective-C与Swift之间对象交换的媒介。不过这么做有些一刀切,因为来自于Objective-C的大多数对象实际上都不会为nil。

·在Objective-C中,诸如NSArray这样的集合类型可以包含多种对象类型的元素,而集合本身并不知道其所包含的元素类型。不过,Swift集合类型只能包含一种类型的元素,其本身的类型也是由所包含的元素类型所决定的。默认的解决方案是对来自于Objective-C的通用类型的集合来说,我们需要在Swift端显式对其进行向下类型转换。当我们要获取一个视图的子视图时,常常会得到一个[AnyObject],然后需要将其向下类型转换为[UIView];但实际上,视图的子视图一定是UIView对象,这么做有些麻烦。

这些问题随后通过修改Objective-C语言得到了解决,解决方案是允许在声明时使用标记,从而让Objective-C能与Swift对所需要的对象类型进行更为具体的沟通。

Objective-C对象类型可以标记为nonnull或nullable,分别表示对象不会为nil或可能为nil。与之类似,C指针类型可以标记为__nonnull或__nullable。借助于这些标记,我们就不必再将隐式展开的Optional作为交换的中间媒介了;每种类型都可以是常规类型或是常规的Optional。这样,现如今的Cocoa API中就很少会出现隐式展开的Optional了。

如果编写Objective-C头文件,但没有将任何类型标记为可空类型,那就会回到以往的阴暗日子了:Swift会将你定义的类型看作隐式展开的Optional。比如,下面是一个Objective-C方法声明:


- (NSString*) badMethod: (NSString*) s;  

由于缺少标记,Swift看到的是下面这个样子:


func badMethod(s: String!) -> String!  

如果头文件包含了标记,但标记不完全,Objective-C编译器就会发出警告。为了解决这个问题,你可以使用默认的nonnull设置将整个头文件都标记起来;接下来则需要只标记那些nullable类型:


NS_ASSUME_NONNULL_BEGIN- (NSString*) badMethod: (NSString*) s;- (nullable NSString*) goodMethod: (NSString*) s;NS_ASSUME_NONNULL_END  

Swift不会再将其看作隐式展开的Optional了:


func badMethod(s: String) -> Stringfunc goodMethod(s: String) -> String?  

这种标记还可以让Swift编译器对继承下来或基于协议的Objective-C方法声明的正确性执行更为严格的检查。过去,你可以修改一个类型的可选择性(optionality);现在,如果做得不对编译器会告诉你。比如,如果Objective-C将一个类型声明为nullable-NSString*,那么你就不能将其声明为String;你必须得将其声明为String?。

要想标记包含某种元素类型的集合类型,请将元素类型放到尖括号(<>)中,置于集合类型名之后,星号之前。下面是一个返回字符串数组的Objective-C方法:


- (NSArray<NSString*>*) pepBoys;  

Swift会将该方法的返回类型看作[String],并且不需要再对其进行向下类型转换了。

在实际的Objective-C集合类型的声明中,占位符名表示尖括号中的类型。比如,NSArray的声明以如下内容开始:


@interface NSArray<ObjectType>- (NSArray<ObjectType> *)arrayByAddingObject:(ObjectType)anObject;// ...  

第1行表示我们将要使用ObjectType作为元素类型的占位符名。第2行表示arrayByAddingObject:方法接收一个ObjectType元素类型的对象,并返回该元素类型的一个数组。如果某个数组声明为NSArray<NSString*>*,那么ObjectType占位符就会被解析为NSString*(从这里面可以看到为何Apple将其叫作“轻量级泛型”)。

A.3 双语言目标

一个目标可以是双语言目标:既包含Swift文件又包含Objective-C文件的目标。出于几个原因,双语言目标是很有用的。你想要充分利用Objective-C的语言特性;想要利用上由Objective-C编写的第三方代码;想要利用自己使用Objective-C编写的既有代码。应用本身原来可能是由Objective-C编写的,现在想要将其中一部分(或是逐步将全部代码)迁移到Swift。

关键的问题是在单个目标中,Swift与Objective-C如何能在第一时间就理解对方的代码。回想一下,与Swift不同,Objective-C已经有可见性问题:Objective-C文件不能自动看到彼此。相反,能看到其他Objective-C文件的每个Objective-C文件都需要显式声明才行,通常是在第1行顶部通过一个#import指令来实现的。为了避免私有信息的意外暴露,Objective-C类声明按照惯例会位于两个以上的文件中:一个头文件(.h),它包含了@interface部分;一个代码文件(.m),它包含了@implementation部分。此外,只需要导入.h文件即可。这样,如果类成员、常量等的声明为公开的,那么它们就会被放到.h文件中。

Swift与Objective-C之间的可见性取决于这种约定:这是通过.h文件实现的。有两个方向的可见性,需要分别对待:

Swift如何看到Objective-C

在将Swift文件添加到Objective-C目标中,或将Objective-C文件添加到Swift目标中时,Xcode会创建一个桥接头文件。它在项目中是个.h文件。其默认名源自目标名(如,MyCoolApp-Bridging-Header.h),不过该名字是任意的,也可以修改,只要修改目标的Objective-C Bridging Header构建设置与之匹配即可。(与之类似,如果没有生成桥接头文件,后面又想拥有一个,那么可以手工创建一个.h文件,并在应用目标的Objective-C Bridging Header构建设置中指向它即可。)如果在该桥接头文件中#import它,那么Objective-C.h文件就会对Swift可见了。

Objective-C如何看到Swift

如果有了桥接头文件,那么在构建目标时,所有Swift文件的顶层声明都会自动转换为Objective-C,并用于构建隐藏的桥接头文件,它位于该目标的Intermediates构建目录中,在DerivedData目录内。查看它的最简单的方式是使用如下终端命令:


$ find ~/Library/Developer/Xcode/DerivedData -name "*Swift.h"  

上述命令会显示出隐藏的桥接头文件名。比如,对于名为MyCoolApp的目标来说,隐藏的桥接头文件叫作MyCoolApp-Swift.h;名字可能会涉及一些转换;比如,目标名中的空格已经被转换为了下划线。此外,查看(或修改)目标的Product Module Name构建设置;隐藏的桥接头文件名源自于它。要想让Objective-C文件可以看到Swift声明,需要将这个隐藏的桥接头文件#import到需要看到它的每个Objective-C文件中。

出于简洁性的考虑,我分别称这两个桥接头文件为可见与不可见桥接头文件。

比如,假设向名为MyCoolApp的Swift目标添加了一个使用Objective-C编写的Thing类。它由两个文件构成,分别是Thing.h与Thing.m,那么:

·要想让Swift代码能够看到Thing类,我需要在可见桥接头文件(MyCoolApp-Bridging-Header.h)中#import"Thing.h"。

·要想让Thing类代码看到Swift声明,我需要在Thing.m顶部导入不可见桥接头文件(#import"MyCoolApp-Swift.h")。

在此基础上,下面是将我自己的Objective-C应用转换为Swift应用的步骤:

1.选取一个待转换为Swift的.m文件。Objective-C不能继承Swift类,因此如果使用Objective-C定义了一个类及其子类,那就从子类开始。将应用委托类留在最后。

2.从目标中删除该.m文件。要想做到这一点,请选择该.m文件,然后使用文件查看器。

3.在#import了相应.h文件的每个Objective-C文件中,删除该#import语句,并导入不可见桥接头文件(如果之前没有导入过)。

4.如果在可见桥接头文件中导入了相应的.h文件,请删除#import语句。

5.为该类创建.swift文件,请确保将其添加到目标中。

6.在.swift文件中,声明类并为.h文件中声明为公开的所有成员提供桩声明。如果该类需要使用Cocoa协议,那就使用它们;还需要提供所需协议方法的桩声明。如果该文件引用了目标在Objective-C中声明的其他类,那么在可见桥接头文件中导入其.h文件。

7.项目现在应该可以编译通过了!当然,没法使用,因为还没有在.swift文件中编写任何实际代码。不过,这都是小事!

8.现在在.swift文件中编写代码。我的做法是逐行转换原始的Objective-C代码,这个因人而异。

9.当.m文件中的代码全部被转换为了Swift后,构建、运行并测试。如果运行时说(很可能还会出现崩溃)找不到类,那就请在nib编辑器中寻找对其的所有引用,并在身份查看器中重新加入类名(按下Tab来设定修改)。保存并重试。

10.进入下一个.m文件!重复上述所有步骤。

11.转换完所有文件后,请转换应用委托类。这时,如果目标中已经没有Objective-C文件,那就请删除main.m文件(在应用委托类声明中将其替换为@UIApplicationMain特性)与.pch(预编译头文件)文件。

应用现在应该可以运行了,并且现在是由纯Swift编写的(至少是按照你的期望来的)。回过头来思考一下代码,使其更加符合Swift习惯。你会发现,Objective-C中笨拙且难以解决的事情在Swift会变得更加简洁和清晰。

此外,还可以通过在Swift中对Objective-C进行扩展来部分转换Objective-C类。这对于整体的转换过程是很有帮助的,也可以只在Swift中编写一两个Objective-C类的方法,因为Swift能够很好地理解它们。不过,如果不是公开的,那么Swift就无法看到Objective-C类的成员,因此之前在Objective-C类的.m文件中设定为私有的方法和属性需要在.h文件中进行声明。