讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 附錄A C、Objective-C與Swift >

附錄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 = CGRectZero
var body = CGRectZero
CGRectDivide(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 OSStatus
AudioServicesAddSystemSoundCompletion(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 = 0
AudioServicesCreateSystemSoundID(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 = SecondViewController
self.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 = StringExpecterHolder
holder.f = f
  

接下來在需要AnyObject時將該實例傳遞過去:


\let lay = CALayer
lay.setValue(holder, forKey:"myFunction")
  

後面就可以抽取該實例,將其從AnyObject進行向下類型轉換,並調用它所包裝的函數,這一切都是非常簡單的:


let holder2 = lay.valueForKey("myFunction") as! StringExpecterHolder
holder2.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) -> String
func 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文件中進行聲明。