Swift 與 Objective-C 的互動 (Interop)

雖然 Swift 已經成為 Apple 平台開發的主流語言,但在許多歷史悠久的大型專案中,仍然存在著大量的 Objective-C 程式碼。此外,某些古老但穩定的第三方庫可能也尚未遷移至 Swift。

因此,互操作性 (Interoperability),即 Swift 與 Objective-C 互相呼叫的能力,是 iOS/macOS 開發者必須掌握的重要技能。Apple 在設計 Swift 時就高度重視這一點,讓兩者可以幾乎無縫地在同一個專案中共存 (Mixed and Matched)。

Swift 使用 Objective-C (Swift calls Obj-C)

要在 Swift 中使用 Objective-C 的程式碼,關鍵在於 Bridging Header

什麼是 Bridging Header?

這是一個特殊的 .h 標頭檔,它的作用是作為橋樑,將 Objective-C 的標頭檔暴露給 Swift。

當你在一個 Objective-C 專案中第一次建立 Swift 檔案,或是反過來在 Swift 專案中建立 Objective-C 檔案時,Xcode 通常會跳出提示視窗問你:"Would you like to configure an Objective-C bridging header?"。點選 "Create Bridging Header" 即可自動建立。

如果需要手動建立:

  1. 新增一個 Header File (通常命名為 專案名稱-Bridging-Header.h)。
  2. Build Settings 搜尋 "Objective-C Bridging Header"。
  3. 填入該檔案的路徑 (例如 MyApp/MyApp-Bridging-Header.h)。

如何使用?

假設你有一個 Objective-C 類別 MyObjCLogger

// MyObjCLogger.h
#import <Foundation/Foundation.h>

@interface MyObjCLogger : NSObject
@property (nonatomic, copy) NSString *prefix;
- (instancetype)initWithPrefix:(NSString *)prefix;
- (void)logMessage:(NSString *)message;
@end

只需要在 Bridging Header 中引入它:

// MyApp-Bridging-Header.h
#import "MyObjCLogger.h"

然後,在專案中任何一個 Swift 檔案裡,你都可以直接使用 MyObjCLogger,不需要再寫 import

// Swift Code
// 不需 import,直接使用
let logger = MyObjCLogger(prefix: "[INFO]") // 對應 initWithPrefix:
logger.logMessage("App started")            // 對應 logMessage:
logger.prefix = "[DEBUG]"

Swift 編譯器會自動將 Objective-C 的語法轉換為 Swift 風格 (例如 initWithPrefix: 變成了 init(prefix:))。

Objective-C 使用 Swift (Obj-C calls Swift)

也就是在舊有的 OC 檔案中呼叫新寫的 Swift 類別。這需要依賴 Generated Header

Generated Interface Header

Xcode 會自動為你的 Swift 程式碼生成一個 Objective-C 標頭檔。這個檔案不會顯示在專案導航列中,它是完全隱藏的,但你可以直接引用它。

預設名稱格式為:<ProductModuleName>-Swift.h。 例如你的專案 Target 名稱是 MyApp,則生成的檔案就是 MyApp-Swift.h

如何使用?

在你的 Objective-C .m 檔案中引入這個 Generated Header:

// SomeOldClass.m
#import "SomeOldClass.h"
#import "MyApp-Swift.h" // 引入 Swift 生成的 Header

@implementation SomeOldClass
- (void)doSomething {
    MySwiftClass *swiftObj = [[MySwiftClass alloc] init];
    [swiftObj sayHello];
}
@end

讓 Swift 類別對 Obj-C 可見

並不是所有的 Swift 程式碼都能被 Objective-C 看見。必須滿足以下條件:

  1. 類別必須繼承自 NSObject (直接或間接)。
  2. 屬性或方法如果希望被看到,最好加上 @objc 修飾字 (雖繼承 NSObject 通常會自動推斷,但明確加上更保險,特別是在 Extension 中)。
  3. Swift 特有的型別 (如 struct, enum with associated values, tuple, generics) 是無法在 Objective-C 中使用的。
// MySwiftClass.swift
import Foundation

// 必須繼承 NSObject 才能在 Obj-C 使用
@objcMembers // 讓整個 Class 的成員都預設加上 @objc
class MySwiftClass: NSObject {
    var title: String = "Swift"
    
    func sayHello() {
        print("Hello from \(title)")
    }
    
    // 純 Swift 結構,Obj-C 看不到這個方法
    func ensureStruct(p: Point) { } 
}

struct Point { var x, y: Double } // Obj-C 無法使用
@objcMembers 是一個便利屬性,它會自動將類別內所有可相容的成員都標記為 @objc,省去逐一添加的麻煩。

語法映射與細節 (Mapping details)

1. 初始化方法 (Initializers)

Objective-C 的 init 方法會映射為 Swift 的 init 關鍵字。

  • Obj-C: [[MyClass alloc] init]

  • Swift: MyClass()

  • Obj-C: [[MyClass alloc] initWithString:@"Hi"]

  • Swift: MyClass(string: "Hi")

2. 可空性 (Nullability)

Objective-C 的指標預設是可以在任何地方為 nil 的,這與 Swift 的強型別安全理念衝突。為了讓轉換更精確,Objective-C 引入了 Nullability 標註。

Obj-C 標註Swift 對應意義
nonnullString絕不會是 nil
nullableString?可能是 nil
null_unspecifiedString!未指定 (隱式解包)

為了減少重複寫 nonnull,Objective-C 提供了 NS_ASSUME_NONNULL_BEGINEND 巨集:

NS_ASSUME_NONNULL_BEGIN

@interface User : NSObject
@property (nonatomic, copy) NSString *name; // 自動視為 nonnull -> String
@property (nonatomic, copy, nullable) NSString *email; // nullable -> String?
- (instancetype)initWithName:(NSString *)name;
@end

NS_ASSUME_NONNULL_END

如果 Objective-C 程式碼沒有加上這些標註,Swift 會將其視為 Implicitly Unwrapped Optional (例如 String!),這在使用上會有潛在的崩潰風險。

3. 錯誤處理 (Error Handling)

Objective-C 使用 NSError ** 指標來報告錯誤,Swift 則會自動將其轉換為 throws 機制。

Objective-C:

- (BOOL)saveContent:(NSString *)content error:(NSError **)error;

Swift:

// Bool 回傳值被移除,指針對應變成 throws
do {
    try fileManager.saveContent("Data")
} catch {
    print("Error: \(error)")
}

優化 Swift 介面 (Refining for Swift)

如果你是 Library 開發者,想讓你的 Objective-C API 在 Swift 中用起來更順手,可以使用 NS_SWIFT_NAME 巨集來重命名。

重命名類別或方法

// 將冗長的工廠方法轉為 Swift 的 init
+ (instancetype)userWithJSONData:(NSData *)data NS_SWIFT_NAME(init(json:));

Swift 使用時:

let user = User(json: data) // 看起來就像原生的 Swift 程式碼

將常數轉為 Enum 或 Struct

Objective-C 常使用一堆 String 常數,在 Swift 看起來很雜亂。可以使用 NS_STRING_ENUM 將其群組化。

Objective-C:

typedef NSString * TrafficLightState NS_STRING_ENUM;

extern TrafficLightState const TrafficLightStateRed;
extern TrafficLightState const TrafficLightStateGreen;
extern TrafficLightState const TrafficLightStateYellow;

Swift:

// 自動變成 Struct/Enum 風格
let state: TrafficLightState = .red

常見陷阱提醒

  1. 動態特性 (Dynamic Dispatch):Swift 預設使用靜態派發 (Static Dispatch) 以提升效能,而 Objective-C 完全依賴動態派發。如果你需要在 Swift 中使用 KVO (Key-Value Observing) 或 Method Swizzling,必須將屬性或方法標記為 @objc dynamic
  2. 效能開銷:雖然互操作很方便,但在頻繁呼叫的迴圈中,Swift 與 Obj-C 之間的型別轉換 (如 String <-> NSString, Array <-> NSArray) 仍會有微小的效能損耗。
  3. id 類型:Objective-C 的 id 類型在 Swift 中會映射為 AnyAnyObject,使用時通常需要進行 as? 轉型。

透過良好的互操作性設計,我們可以漸進式地將舊專案遷移至 Swift,而不需要一次重寫所有程式碼,這是 iOS 開發者必須掌握的生存之道。