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" 即可自動建立。
如果需要手動建立:
- 新增一個 Header File (通常命名為
專案名稱-Bridging-Header.h)。 - 到 Build Settings 搜尋 "Objective-C Bridging Header"。
- 填入該檔案的路徑 (例如
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 看見。必須滿足以下條件:
- 類別必須繼承自
NSObject(直接或間接)。 - 屬性或方法如果希望被看到,最好加上
@objc修飾字 (雖繼承NSObject通常會自動推斷,但明確加上更保險,特別是在 Extension 中)。 - Swift 特有的型別 (如
struct,enumwith 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 對應 | 意義 |
|---|---|---|
nonnull | String | 絕不會是 nil |
nullable | String? | 可能是 nil |
null_unspecified | String! | 未指定 (隱式解包) |
為了減少重複寫 nonnull,Objective-C 提供了 NS_ASSUME_NONNULL_BEGIN 和 END 巨集:
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
常見陷阱提醒
- 動態特性 (Dynamic Dispatch):Swift 預設使用靜態派發 (Static Dispatch) 以提升效能,而 Objective-C 完全依賴動態派發。如果你需要在 Swift 中使用 KVO (Key-Value Observing) 或 Method Swizzling,必須將屬性或方法標記為
@objc dynamic。 - 效能開銷:雖然互操作很方便,但在頻繁呼叫的迴圈中,Swift 與 Obj-C 之間的型別轉換 (如
String<->NSString,Array<->NSArray) 仍會有微小的效能損耗。 - id 類型:Objective-C 的
id類型在 Swift 中會映射為Any或AnyObject,使用時通常需要進行as?轉型。
透過良好的互操作性設計,我們可以漸進式地將舊專案遷移至 Swift,而不需要一次重寫所有程式碼,這是 iOS 開發者必須掌握的生存之道。