翻譯自蘋果官方文檔
和Objective-C交互
互用性是指,在Swift和Objective-C之間可以建立一個互通接口,不管是Swift生成接口給Objective-C對接,還是Objective-C生成接口給Swift對接。既然你決定開始用Swift來開發,那麼有必要理解一下怎麼運用互用性來重定義、提高、改進你寫Cocoa app的方式。
互用性重要性之一是,在Swift中調用Objective-C的API。在你import一個Objective-C框架之後,你就可以用Swift的語法來實例化裡面的類,繼而使用它們。
初始化
要在Swift裡初始化一個Objective-C類,需要用Swift的初始化語法來調Objective-C的初始化方法。
Objective-C初始化方法都以init開頭,或者,如果有一個或多個參數,會以initWith:開頭。在Swift文件裡如果要調用Objective-C初始化方法,那麼init前綴會變成Swift初始化方法。如果此時初始化方法帶有參數,會去掉with,而其他參數會根據情況劃分到各個參數中。
Objective-C初始化方法的聲明:
- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;
轉為Swift的初始化聲明:
init() { }
init(frame: CGRect, style: UITableViewStyle) { }
實例化對象的過程,更能看出Objective-C和Swift語法的不同:
Objective-C:
UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
Swift:
let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)
不用調用alloc,Swift替你處理了。還有,調用Swift風格的初始化函數,不會到處出現init。
當給變量或者常量賦值的時候,你可以指明一個類型,或者可以不指明這個類型,讓Swift根據初始化方法自動推導出類型。
let myTextField = UITextField(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 40.0))
這裡的UITableView和UITextField和你在Objective-C裡實例化出來的對象是一樣的,Objective-C裡怎麼用,這裡就怎麼用,根據各自的類型,獲取屬性、調用方法都一樣。
類工廠方法和方便初始化方法
為了保持一致性和簡單,Objective-C的類工廠方法引入Swift後,會改為方便初始化方法。這樣,使用這些方法就像使用初始化方法一樣。
例如,下面這個就是Objective-C中的一個工廠方法:
UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];
在Swift中,要這樣調用:
let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)
可失敗的初始化
Objective-C中初始化函數直接返回初始化的結果,如果初始化失敗,就會直接返回nil。在Swift中,這個模式由語言的一個特性來實現,叫可失敗的初始化(原文:failable initialization)。
很多系統框架裡的初始化方法都有標識初始化能不能失敗。在你自己的Objective-C類裡可以使用可空性(原文:nullability)來標識是不是能失敗,Nullability and Optionals裡有描述。在Objective-C裡不可失敗的話,在Swift裡是init(...),可失敗的話,用init?(...)。否則,都用init!(...)。
例如,UIImage(contentsOfFile:)初始化方法,如果提供的路徑下面沒有圖片文件,就會初始化失敗。如果可失敗初始化函數初始化成功了,你可以用可選綁定去解包返回的結果。(譯者註:public init?(contentsOfFile path: String))
if let image = UIImage(contentsOfFile: "MyImage.png") {
} else {
}
獲取屬性
在Objective-C中用@property聲明的屬性,Swift會做下面的處理:
已經用nonnull、nullable和 null_resettable修飾的屬性,按照Nullability and Optionals說的,轉成可選和非可選屬性。
readonly屬性會轉化為Swift中的計算型屬性,也即只有一個getter方法({ get })。
weak屬性轉化為Swift中的weak屬性(weak var)。
對於不是weak的所有者屬性(原文:ownership property)(即assign、 copy、strong、 unsafe_unretained)都會轉成相對應的存儲屬性。
class修飾的屬性(譯者註:Objective-C在Xcode9引入了一個新的屬性class,應該就是為了對接Swift吧),轉為在Swift中類型屬性。
Objective-C中的原子性修飾符(atomic和nonatomic)在Swift中沒有對應的修飾符,但是原子性所提供的功能在Swift裡是依然存在的。(譯者註:Atomicity property attributes (atomic and nonatomic) are not reflected in the corresponding Swift property declaration, but the atomicity guarantees of the Objective-C implementation still hold when the imported property is accessed from Swift.這句翻譯的不是太好,請指教!)
存取屬性(getter=和setter=)在Swift被忽略掉。(譯者註:今天才知道getter=和setter=也是屬性)
在Swift中,獲取Objective-C 對象的屬性值,用點語法直接帶上屬性名字,不需要括號。
例如,設置UITextField的textColor和text屬性:
myTextField.textColor = .darkGray
myTextField.text = "Hello world"
Objective-C中用點語法調用無參數有返回值的方法,形式和獲取屬性很像(譯者註:方法除了點,還有括號,其實並不一樣)。雖然調用形式一樣,方法在Swift中還是會轉換為方法,只有Objective-C中用@property聲明的變量才能轉為Swift中的屬性。關於方法的引入和調用可以參考方法的使用。
方法的使用
在Swift中,用點語法來調用Objective-C方法。
Objective-C方法引入到Swift後,方法的第一部分,變成方法名,在括號前面。第一個參數在括號裡,沒有參數名。剩下的參數對應各自的參數名排在括號裡。方法的所有組成部分都是需要的,少了任何部分,調用地址就是不對的。
例如Objective-C中:
[myTableView insertSubview:mySubview atIndex:2]
在Swift中,會是這樣:
myTableView.insertSubview(mySubview, at: 2)
調用一個沒有參數的方法,依然要帶上括號。
myTableView.layoutIfNeeded()
id兼容
Objective-C中的id類型會轉為Swift中的Any類型。在編譯時和運行時,將Swift的值或者對象(譯者註:Swift裡引入了結構體,所以不光是類引用類型,也包括結構體值類型,所以,總是出現「值或者對象」)傳給Objective-C的類型為id參數時,編譯器會去執行一個全局橋接轉換操作(原文:universal bridging conversion operation)。當將一個Objective-C中的id傳給Swift的Any參數時,運行時會自動橋接回Swift的類引用或者值類型。(譯者註:換句話說就是,id和Any可以轉換,並且轉換由系統完成)
var x: Any = "hello" as String
x as? String
x as? NSString
x = "goodbye" as NSString
x as? String
x as? NSString
(譯者註:這個例子代碼說明啥?不是特別清楚,求指教!)
向下轉換Any(譯者註:從一個比較寬的類型,轉化為一個比較具體的類型)
如果知道Any裡實際是什麼類型,那麼將其向下轉化到一個更具體的類型比較有用。由於Any類型可以包含任意類型,所以,向下轉型到一個更具體的類型不一定都能成功。
可以試試可選類型轉化操作符(as?),返回一個包裹著轉化結果的可選值:
let userDefaults = UserDefaults.standard
let lastRefreshDate = userDefaults.object(forKey: "LastRefreshDate")
if let date = lastRefreshDate as? Date {
print("\(date.timeIntervalSinceReferenceDate)")
}
如果你能確定對象的類型,那麼可以用強制向下類型轉換(as!)。
let myDate = lastRefreshDate as! Date
let timeInterval = myDate.timeIntervalSinceReferenceDate
如果強制向下轉換失敗,會觸發一個運行時錯誤:
let myDate = lastRefreshDate as! String
動態方法查找
Swift還有一個AnyObject類型,用來表示某些對象類型(譯者註:和Any不同的是,Any還可以存儲值類型),並且這個類型還有特別的能力,可以將@objc修飾的方法轉為動態查找。通過這個特性,對Objective-C返回的id類型的值可以更好的操作與維護。
(譯者註:AnyObject能力比較強大,因為Swift是強類型,不指定具體類型是不能調用方法的,但是AnyObject不一樣,可以調用任意方法。如果一個對象的方法或者屬性標記為@objc,那麼這些方法就變成可以動態查找的方法。因為Objective-C的方法是採用動態查找實現的,所以,只有這樣,這些方法才能提供給Objective-C使用。@objc後面有詳細敘述)
例如,可以給AnyObject類型的常量或者變量賦值任意的對象,這個變量又可以再賦值一個不同類型的對象。你不需要轉換為具體的類型,直接通過AnyObject類型就可以調用Objective-C具體類的方法和屬性。
var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow
不能識別的方法和可選鏈
因為直到運行時才能知道AnyObject的值到底是什麼類型,所以,就有可能寫出不安全的代碼。無論是Swift還是Objective-C,試圖去調用一個不存在的方法都會觸發一個找不到方法(譯者註:unrecognized selector)的錯誤。
例如,下面的代碼不會有編譯時的警告,但是在運行時會報錯:
myObject.character(at: 5)
Swift可以用可選的方式來避免不安全的行為。調用AnyObject類型的方法時,實際進行了一個隱式解包。可以用可選鏈語法來調用AnyObject類型的方法。
例如下面的代碼,第一行和第二行不會執行,因為NSDate對象沒有count屬性和character(at:)方法。常量myCount會被推導為可選Int類型,值為nil。可以用if let語句來解包方法返回的結果,如第三行所示。
// myObject has AnyObject type and NSDate value
let myCount = myObject.count
// myCount has Int? type and nil value
let myChar = myObject.character?(at: 5)
// myChar has unichar? type and nil value
if let fifthCharacter = myObject.character?(at: 5) {
print("Found \(fifthCharacter) at index 5")
}
// conditional branch not executed
注意
雖然Swift沒有強制要求,AnyObject類型的值調用方法時,一定要解包,但還是推薦解包,以此來避免不可預知的行為。(譯者註:Although Swift does not require forced unwrapping when calling methods on values of type AnyObject, it is recommended as a way to safeguard against unexpected behavior.不是太理解,求指教!)
可空和可選
Objective-C中,通過原始指針(原文:raw pointer)來引用對象,指針可能是NULL(在Objective-C中是nil)。在Swift中,所有的結構體類型,或者對象類型都不會為空(譯者註:不會直接是nil,而是一個可選值)。如果要表示這個值可以為空,那麼要將這個值包裝為可選類型。關於可選值的更多信息,請看Swift程式語言(Swift 4.0.3)中可選值。
Objective-C中可以用可空性標識來表示參數、屬性、返回值是不是可以為NULL或者nil。單個的類型聲明,我們可以用_Nullable和_Nonnull標識,單個的屬性聲明可以用nullable, nonnull和null_resettable標識,或者用宏NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END來標識整個區域的可空性。如果沒有給一個類型設置可空性修飾,Swift無法區分是可選還是非可選,那麼統一設置為隱式解包可選類型。
(譯者註:對於隱式解包可選類型加個說明,隱式解包的可選類型主要用在一個變量/常量在定義瞬間完成之後值一定會存在的情況,在使用時,不需要解包,直接使用,系統自動解包)
用_Nonnull標識,或者在整個非空宏區域裡的類型,聲明為不可空,在Swift裡都轉為不可選。
聲明為_Nullable的可空類型,在Swift裡轉為可選類型。
沒有任何可空性標識的類型,在Swift裡都轉為隱式解包可選類型。
例如,下面的Objective-C聲明:
@property (nullable) id nullableProperty;
@property (nonnull) id nonNullProperty;
@property id unannotatedProperty;
NS_ASSUME_NONNULL_BEGIN
- (id)returnsNonNullValue;
- (void)takesNonNullParameter:(id)value;
NS_ASSUME_NONNULL_END
- (nullable id)returnsNullableValue;
- (void)takesNullableParameter:(nullable id)value;
- (id)returnsUnannotatedValue;
- (void)takesUnannotatedParameter:(id)value;
下面是在Swift中的樣子:
var nullableProperty: Any?
var nonNullProperty: Any
var unannotatedProperty: Any!
func returnsNonNullValue() -> Any
func takesNonNullParameter(value: Any)
func returnsNullableValue() -> Any?
func takesNullableParameter(value: Any?)
func returnsUnannotatedValue() -> Any!
func takesUnannotatedParameter(value: Any!)
大部分的系統框架,包括Foundation,都有可空性標識,所以在和這些值打交道時,可以保持類型安全和語言的習慣
橋接可選類型到非空對象
Swift將可選值橋接為Objective-C的非空對象,如果可選值裡有值,那麼把這個值傳遞給Objective-C對象,如果可選值是nil,那麼會傳遞一個NSNull實例給Objective-C對象。例如,可以將一個Swift可選類型直接傳入Objective-C接受非空id參數的API,也可以將包含可選項的數組([T?])橋接為NSArray。
下面的代碼可以看出,怎麼根據實際所包含的值來橋接String?實例到Objective-C中。
@implementation OptionalBridging
+ (void)logSomeValue:(nonnull id)valueFromSwift {
if ([valueFromSwift isKindOfClass: [NSNull class]]) {
os_log(OS_LOG_DEFAULT, "Received an NSNull value.");
} else {
os_log(OS_LOG_DEFAULT, "%s", [valueFromSwift UTF8String]);
}
}
@end
valueFromSwift參數類型是id,所以在Swift中要傳入Any類型,如下所示。一般情況下,給參數為Any函數傳入一個可選類型並不普遍,所以,需要將傳入logSomeValue(_:)類方法的可選值進行顯式的轉換為Any類型,避免編譯器警告。
let someValue: String? = "Bridge me, please."
let nilValue: String? = nil
OptionalBridging.logSomeValue(someValue as Any)
OptionalBridging.logSomeValue(nilValue as Any)
協議限定類(Protocol-Qualified Classes)
實現了一個或者多個協議的Objective-C類,在Swift中會轉換成協議組合類型。例如以下的Objective-C中定義的view controller屬性
@property UIViewController<UITableViewDataSource, UITableViewDelegate> * myController;
Swift中會是這樣:
var myController: UIViewController & UITableViewDataSource & UITableViewDelegate
Objective-C中協議限定元類(protocol-qualified metaclasses),在Swift中轉換為協議元類型(protocol metatypes)。例如以下Objective-C方法對特定的類執行操作(原文:Objective-C protocol-qualified metaclasses are imported by Swift as protocol metatypes. For example, given the following Objective-C method that performs an operation on the specified class):
- (void)doSomethingForClass:(Class<NSCoding>)codingClass;
Swift中的樣子:
func doSomething(for codingClass: NSCoding.Type)
輕量級泛型
用輕量級泛型參數化(譯者註:generic parameterization泛型參數化,意思是,給泛型指定了類型,也即尖括號裡有具體的類型。所以後面說到的參數化,都是指已經給泛型指定了具體的類型)。聲明的Objective-C類型,內容的類型信息在Swift會保留下來。例如下面的Objective-C屬性聲明:(譯者註:Objective-C裡面的泛型實際是Xcode7引入的一個語法糖,稱為輕量級泛型。Stack Overflow上有這個討論)
@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;
Swift中會是這樣:
var dates: [Date]
var cachedData: NSCache<NSObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]
一個Objective-C中參數化的類,引入Swift後,轉換為泛型類,有同樣數量的類型參數(譯者註:type parameters類型參數,這裡的意思是,泛型尖括號裡的類型)。所有Objective-C中的泛型類型參數,在Swift中,會將類型參數轉換為像(T: Any)類似的類的形式。如果Objective-C裡泛型參數化指定了一個限制類(譯者註:class qualification限制類,意思是,這個類起到了一個限制的作用,下面說道的protocol qualification協議限制,也是一樣的理解,表示,這個協議對這個泛型起到一個限制作用),在Swift中也會轉換這個限制:這個類必須是這個限制類的子類。如果Objective-C離泛型參數化指定了一個限制協議,在Swift中也會轉換這個限制:這個類也必須遵守指定的協議。對於沒有任何限制的Objective-C類型,Swift會推導給出限制,如下,Objective-C類和類別的聲明:
@interface List<T: id<NSCopying>> : NSObject
- (List<T> *)listByAppendingItemsInList:(List<T> *)otherList;
@end
@interface ListContainer : NSObject
- (List<NSValue *> *)listOfValues;
@end
@interface ListContainer (ObjectList)
- (List *)listOfObjects;
@end
Swift中是這個樣子的:
class List<T: NSCopying> : NSObject {
func listByAppendingItemsInList(otherList: List<T>) -> List<T>
}
class ListContainer : NSObject {
func listOfValues() -> List<NSValue>
}
extension ListContainer {
func listOfObjects() -> List<NSCopying>
}
擴展
Swift擴展和Objective-C中的類別相似。擴展增加了已有類,結構體,枚舉的功能,也可以擴展在Objective-C中定義的這些類型(譯者註:應該是系統將Objective-C中的結構體和枚舉類型轉成了Swift中的結構體和枚舉類型,這樣才具備擴展功能)。可以給系統類型添加擴展,也可以給自定義的類添加擴展。Swift裡引入Objective-C的模塊,用Objective-C中的名字 ,來引用這些類,結構體,枚舉類型。
例如,給UIBezierPath類擴展功能,根據邊長和起點生成一個等邊三角形,再由這個等邊三角形創建一個貝塞爾路徑。
extension UIBezierPath {
convenience init(triangleSideLength: CGFloat, origin: CGPoint) {
self.init()
let squareRoot = CGFloat(sqrt(3.0))
let altitude = (squareRoot * triangleSideLength) / 2
move(to: origin)
addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y))
addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude))
close()
}
}
可以用擴展來添加屬性(包括類屬性和靜態屬性)。但是,只能是計算屬性;擴展不能給類,結構體,枚舉類型添加存儲屬性。
下面的例子是給CGRect結構體擴展一個計算屬性area:
extension CGRect {
var area: CGFloat {
return width * height
}
}
let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
let area = rect.area
可以不通過繼承,通過擴展來讓一個類來實現協議(譯者註:Objective-C中分類也是可以實現這個功能)。如果協議是在Swift中定義的,那麼也可以給Objective-C和Swift中的結構體,枚舉類型添加實現協議的擴展。
擴展不能覆蓋Objective-C中已經存在的方法和屬性。
閉包
Objective-C中的block使用@convention(block)自動轉換為Swift中的閉包。下面是Objective-C block變量:
void (^completionBlock)(NSData *) = ^(NSData *data) {
}
Swift中:
let completionBlock: (Data) -> Void = { data in
// ...
}
Swift閉包和Objective-C block是兼容的,可以將Swift閉包傳遞給接受block的Objective-C方法。Swift閉包和函數是相同的類型,所以也可以給Objective-C的block傳遞一個Swift的函數名字。
閉包和block有相似的值捕獲功能,但是閉包有一點不一樣:變量默認是可以修改的。換句話說,Objective-C中給變量添加__block修飾,在Swift是默認行為。
避免捕獲self而造成循環引用
Objective-C中,在block中捕獲self,需要考慮內存管理。
block會對任何捕獲的對象保持一個強引用,包括self。如果self又對block保持一個強引用,例如block是self的一個copy屬性,那麼會造成一個循環引用。為了避免循環引用,我們讓block捕獲一個弱引用的self。
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
};
和Objective-C裡的block一樣,Swift中的閉包也會對捕獲的對象保持一個強引用,當然也包括self。為了防止造成循環引用,可以在閉包的捕獲列表裡,將self設置為unowned
self.closure = { [unowned self] in
self.doSomething()
}
更多信息參照Swift程式語言(Swift 4.0.3)中的解決閉包循環引用
對象比較
在Swift中有兩個不同的比較類型來比較兩個對象。一個是等號(==),來比較對象的內容。還有一個是恆等(===),來判斷比較的變量或常量是不是指向同一個對象。
Swift對==和===操作符提供了默認的實現,對於繼承自NSObject的對象,遵守Equatable協議。==的默認實現是調用isEqual:方法,===的默認實現是檢查指針是否相等。從Objective-C引入的類型,不能重寫等號和恆等操作符。
NSObject類提供的isEqual:實現和恆等是一樣的,也是比較指針是否相等。在Swift和Objective-C中,可以通過重寫isEqual:,來比較對象的內容是不是相等,而不是比較指針。關於比較邏輯實現的更多信息,請看Cocoa Core Competencies裡的對象比較。
注意
Swift也提供了等和恆等對應的相反的實現(!=和!==)。這兩個操作符不能重寫。
哈希
Objective-C中NSDictionary沒有對key指定類型限制,Swift會轉化為Dictionary,key的類型是AnyHashable。NSSet也一樣,如果在Objective-C中沒有對其中的元素指定類型限制,Swift會將Set中的元素類型設置為AnyHashable。如果NSDictionary和NSSet已經參數化key或者元素的,那麼Swift裡就會用相應的類型。如下Objective-C的聲明:
@property NSDictionary *unqualifiedDictionary;
@property NSDictionary<NSString *, NSDate *> *qualifiedDictionary;
@property NSSet *unqualifiedSet;
@property NSSet<NSString *> *qualifiedSet;
轉為Swift:
var unqualifiedDictionary: [AnyHashable: Any]
var qualifiedDictionary: [String: Date]
var unqualifiedSet: Set<AnyHashable>
var qualifiedSet: Set<String>
Objective-C中未指明類型或者類型為id的類型,轉到Swift中都是AnyHashable,為什麼不是Any類型,因為在這裡,這些類型都需要遵守Hashable協議。AnyHashable類型表示任何可哈希的類型,可以用as?和as!操作符轉化為更具體的類型。
更多信息,見AnyHashable。
Swift類型兼容
Swift可以繼承Objective-C類,生成一個Swift類,其中成員屬性、方法、索引(譯者註:subscript索引是Swift裡的新功能)、初始化方法,這些能在Objective-C中找到對應的轉化,都會自動轉化為Objective-C相應的功能。但是有些特性是Swift特有的,無法轉到Objective-C,如下所示:
泛型(譯者註:Objective-C沒有泛型,後來引入的稱作輕量級泛型,只是一個編譯器的語法糖,和Swift中真正的泛型不是一回事)
元祖
在Swift中定義的枚舉類型,raw值不為Int類型(譯者註:Objective-C中也有枚舉類型,但是,原始值一定是int類型。在Swift中,枚舉類型可以是其他類型,不光是int類型,所以要和Objective-C兼容的話,必須限定為int類型)。
Swift中定義的結構體
Swift中定義的頂級函數(原文:Top-level functions意思是,不屬於某個類,某個結構體,枚舉等的函數,直接寫在文件裡的函數)
Swift中定義的全局變量
Swift中的Typealiases關鍵字
Swift風格的可變參數(譯者註:Objective-C中也有可變參數,但是和Swift相比,功能弱很多,所以,Swift特有的功能轉不過去)
嵌套類型(譯者註:Swift裡類型可以嵌套,例如類裡面還可以定義類)
柯裡化函數(譯者註:王巍的Swift 100 tips第一節就是介紹柯裡化)
Swift API轉為Objective-C API,和上面的Objective-C API轉為Swift API類似,下面是Swift轉Objective-C:
Swift可選類型轉為__nullable
Swift非可選類型轉為__nonnull
Swift常量的存儲屬性和計算屬性都轉為Objective-C的read-only。
Swift變量存儲屬性轉為Objective-C的read-write。
Swift的類型屬性(type properties)轉為class屬性(譯者註:參考Xcode8添加一個屬性class)。
Swif類型方法轉為Objective-C的類方法(靜態方法)。
Swift初始化方法(譯者註:Swift的初始化函數,在Swift裡是按特殊函數對待,但是Objective-C初始化函數也是實例函數)和實例方法轉為Objective-C的實例方法。
Swift中(throw)拋出錯誤的方法,會轉化為Objective-C中的帶NSError **參數的方法。如果這個Swift方法沒有參數,會追加AndReturnError: 到Objective-C方法名的後面,否則就追加error:參數。如果Swift方法沒有指定返回類型,相應的Objective-C方法會返回一個BOOL類型。如果Swift方法返回一個非可選類型,相應的Objective-C方法會返回一個可選類型(譯者註:原文是「 If the Swift method returns a nonoptional type, the corresponding Objective-C method has an optional return type. 」,Objective-C中沒有所謂的可選類型,這句不甚了解,求指導!)。
以下Swift聲明:
class Jukebox: NSObject {
var library: Set<String>
var nowPlaying: String?
var isCurrentlyPlaying: Bool {
return nowPlaying != nil
}
class var favoritesPlaylist: [String] {
}
init(songs: String...) {
self.library = Set<String>(songs)
}
func playSong(named name: String) throws {
}
}
轉為Objective-C:
@interface Jukebox : NSObject
@property (nonatomic, strong, nonnull) NSSet<NSString *> *library;
@property (nonatomic, copy, nullable) NSString *nowPlaying;
@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;
@property (nonatomic, class, readonly, nonnull) NSArray<NSString *> *favoritesPlaylist;
- (nonnull instancetype)initWithSongs:(NSArray<NSString *> * __nonnull)songs OBJC_DESIGNATED_INITIALIZER;
- (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error;
@end
注意
Objective-C不能繼承Swift類。
調整Swift,適配Objective-C
某些情況下,對於暴露給Objective-C的Swift API需要有一個更細粒度的控制(譯者註:原文finer grained control)。用@objc(name)可以給類,屬性,方法,枚舉,及枚舉裡的case重新改一個名字,暴露給Objective-C。
例如,Swift類包含的字符是Objective-C不支持的,那麼我們可以改個名字暴露給Objective-C。如果要給Swift函數改名,需要用Objective-C的selector語法。如果有參數,不要漏了冒號(:)。
@objc(Color)
enum Цвет: Int {
@objc(Red)
case Красный
@objc(Black)
case Черный
}
@objc(Squirrel)
class Белка: NSObject {
@objc(color)
var цвет: Цвет = .Красный
@objc(initWithName:)
init (имя: String) {
}
@objc(hideNuts:inTree:)
func прячьОрехи(количество: Int, вДереве дерево: Дерево) {
}
}
當給一個Swift類使用@objc(name)屬性的時候,Objective-C就可以訪問這個類,並且不會帶有任何命名空間信息(譯者註:因為Swift是有命名空間的,如果不去掉命名空間,直接搬到Objective-C中使用,類名就會是xxx.類名的形式,這個在Objective-C裡當然不能使用,所以這裡的意思是,轉到Objective-C,會去掉命名空間,也就是去掉類名前的xxx)。
在將可歸檔的Objective-C類遷移到Swift中時(譯者註:意思是將一個Objective-C類改寫為Swift類),這個屬性也非常有用。因為歸檔的對象會儲存類名,用@objc(name)指定一個和Objective-C中一樣的名字,這樣才能用新的Swift類來解檔之前Objective-C歸檔的類。
(譯者註:用我的話說就是,Objective-C歸檔的時候,沒有所謂的命名空間,直接按照類名來歸檔,但是改成Swift之後,想把之前用Objective-C歸檔的東西解檔,就必須去掉命名空間,去掉命名空間的做法,就是用@objc(name)來指定類名)
注意
相反,Swift也有@nonobjc屬性,這個屬性表示Objective-C中不能訪問。You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C.(譯者註:譯者對於混編經驗不足,這句意思不甚了解,求指教!原文在上面)有些功能,例如可變參數,在Objective-C中不能表示,所以,對於這類方法,需要標記為@nonobjc。
要求動態派發(譯者註:Objective-C裡面的消息調用機制稱作動態派發)
暴露給Objective-C調用的Swift API,可必須是通過動態派發的方式調用。但是,這些通過動態派發的Swift API,當Swift調用這些API的時候,Swift編譯器會選擇一個更加高效的方法來調用,而不會直接用動態派發的方式調用(譯者註:其實Objective-C中的動態派發是一種低效的機制,Swift已經摒棄)。
在@objc後面加上dynamic,表示通過Objective-C運行時動態派發來訪問成員。很少情況下需要這種動態派發。但是在使用KVO和 method_exchangeImplementations等那些需要在運行時動態替換方法的時候,就需要指明動態派發。
添加dynamic的聲明也必須添加@objc,除非@objc被系統隱式添加了。@objc隱式添加的相關信息請看Swift程式語言中屬性聲明
選擇子(譯者註:原文selector)
Objective-C中,selector表示一個方法的類型。Swift中,用Selector結構體表示Objective-C的selector類型,可以用#selector表達式來創建這個結構體。要給一個方法創建一個可供Objective-C調用的selector,需要傳入方法名,例如#selector(MyViewController.tappedButton(_:))。如果要給一個屬性的getter或者setter方法創建一個selector,需要傳遞一個以getter或者setter標籤為前綴的屬性名,例如#selector(getter: MyViewController.myButton)。
import UIKit
class MyViewController: UIViewController {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let action = #selector(MyViewController.tappedButton)
myButton.addTarget(self, action: action, forControlEvents: .touchUpInside)
}
@objc func tappedButton(_ sender: UIButton?) {
print("tapped button")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
注意
Objective-C方法指針可以加括號,可以使用as操作符來區別不同的重載函數,例如#selector(((UIView.insert(subview:at:)) as (UIView) -> (UIView, Int) -> Void))(譯者註:可以參考Stack Overflow)
Objective-C方法的不安全調用
可以使用perform(_:)或者它的變體去調用一個Objective-C方法。用selector調用方法是不安全的,因為編譯器對結果不能做任何保證,甚至不能保證對象是否能響應這個selector。除非非要依賴Objective-C運行時的動態解決方案,否則我們不建議使用這些API。例如,你需要實現一個類,使用target-action設計模式,像NSResponder那樣,這種情況使用這些API是合適的。大部分情況下,將對象轉為AnyObject,再使用可選鏈來調用,會更安全,更方便。這個在id兼容性裡有說到(譯者註:這裡蘋果推薦先將對象轉為AnyObject,再直接來調用方法,而不是使用selector,出於安全考慮)。
通過selector同步執行方法,例如perform(_:),返回一個隱式解包的Unmanaged指針,指向AnyObject實例(Unmanaged<AnyObject>!),因為返回值的類型和所有者在編譯期不能確定。按照約定,在一個指定的線程,或者做一個延時再執行一個selector,例如perform(_:on:with:waitUntilDone:modes:)和perform(_:with:afterDelay:)不會返回一個值。更多信息見非託管對象。
let string: NSString = "Hello, Cocoa!"
let selector = #selector(NSString.lowercased(with:))
let locale = Locale.current
if let result = string.perform(selector, with: locale) {
print(result.takeUnretainedValue())
}
對象調用不能識別的selector,會觸發doesNotRecognizeSelector(_:),拋出NSInvalidArgumentException異常。
let array: NSArray = ["delta", "alpha", "zulu"]
let selector = #selector(NSDictionary.allKeysForObject)
array.perform(selector)
Key和Key Path
Objective-C中,key是一個字符串,表示一個對象的屬性。key path是一個用點分割的字符串,表示一個對象屬性的屬性。key和key path經常用於鍵值編碼(KVC),一種通過字符串間接獲取對象屬性的機制。key和key path也用於健值觀察(KVO),一個對象的屬性改變,會通知另外一個對象的機制。
Swift中,可以用key-path表達式創建一個key path來獲取屬性,例如用\Animal.name來獲取Animal的name屬性,例如下面代碼。用key-path表達式創建的key path指向的屬性已經包含了類型信息。對一個實例用key path取得的屬性值,和直接用屬性取得的值一樣。key-path表達式可以是一個屬性,也可以是鏈式的屬性,如\Animal.name.count。
class Animal: NSObject {
@objc var name: String
init(name: String) {
self.name = name
}
}
let llama = Animal(name: "Llama")
let nameAccessor = \Animal.name
let nameCountAccessor = \Animal.name.count
llama[keyPath: nameAccessor]
llama[keyPath: nameCountAccessor]
Swift中,還可以用#keyPath字符串表達式創建一個可以在編譯期檢查的key和key path,用於KVC方法,如value(forKey:)和value(forKeyPath:),用於KVO方法,如addObserver(_:forKeyPath:options:context:)。#keyPath也支持鏈式的方法或屬性。鏈中可以有可選值,如#keyPath(Person.bestFriend.name)(譯者註:bestFriend就是一個可選值)。和用key-path表達式創建的key path不同,用#keyPath字符串創建的key path所指定的屬性或者方法沒有類型信息。
注意
#keyPath字符串語法和#selector表達式類似,見Selectors。
class Person: NSObject {
@objc var name: String
@objc var friends: [Person] = []
@objc var bestFriend: Person? = nil
init(name: String) {
self.name = name
}
}
let gabrielle = Person(name: "Gabrielle")
let jim = Person(name: "Jim")
let yuanyuan = Person(name: "Yuanyuan")
gabrielle.friends = [jim, yuanyuan]
gabrielle.bestFriend = yuanyuan
#keyPath(Person.name)
gabrielle.value(forKey: #keyPath(Person.name))
#keyPath(Person.bestFriend.name)
gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))
#keyPath(Person.friends.name)
gabrielle.value(forKeyPath: #keyPath(Person.friends.name))
作者:桔子聽
連結:https://www.jianshu.com/p/742c6af3afae
相關推薦: