Swift開發指南:使用Swift與Cocoa和Objective-C(Swift 4) - 2.互通性

章節導航:
Swift開發指南:使用Swift與Cocoa和Objective-C(Swift 4) - 1.入門
Swift開發指南:使用Swift與Cocoa和Objective-C(Swift 4) - 2.互通性


與Objective-C API進行交互

互操作性是能夠在任何一個方向上與Swift和Objective-C進行接口,讓您訪問并使用以其他語言的文件中的一些代碼。當您開始將Swift集成到應用程序開發工作流程中時,了解如何利用互操作性來重新定義、改進和增強編寫Cocoa應用程序的方式是一個好主意。

互操作性的一個重要特點是,它可以讓您在編寫Swift代碼時使用Objective-C API。導入Objective-C框架后,您可以實例化類,并使用本地Swift語法與它們進行交互。

初始化

要在Swift中實例化一個Objective-C類,可以使用Swift構造器語法調用其中的一個構造方法。

Objective-C構造方法以init開始,如果構造方法需要一個或多個參數則使用initWith:。當由Swift導入Objective-C初始化程序時,該init前綴將成為一個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))

這些UITableViewUITextField對象是您在Objective-C中實例化的對象。您可以以與Objective-C中相同的方式使用它們,訪問任何屬性并調用其各自類型上定義的任何方法。


類初始化方法和便利構造器

為了一致性和簡單性,在Swift中,Objective-C類初始化方法作為便利構造器導入。這允許它們與構造器使用相同的語法。

例如,在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中,構造器直接返回它們初始化的對象。要在初始化失敗時通知調用者,Objective-C構造器可以返回nil。在Swift中,此模式內置于稱為可失敗構造器的語言功能中。

系統框架中的許多Objective-C初始化程序已被審計,可指示構造是否可以失敗。您可以在你的Objective-C類中使用nullability annotations指出構造方法是否可以失敗,如可空性和可選項章節所述。Objective-C構造器導入為init(...)如果構造不會失敗,導入為init?(...)如果會失敗。否則,構造器將被導入為init!(...)

例如,如果在提供的路徑上不存在圖像文件,則UIImage(contentsOfFile:)初始化程序可能無法初始化UIImage對象。如果初始化成功,您可以使用可選項綁定來打開可用構造器的結果。

if let image = UIImage(contentsOfFile: "MyImage.png") {
    // loaded the image successfully
} else {
   // could not load the image
}
<br>

訪問屬性

使用@property語法的Objective-C屬性聲明將以以下列方式導入為Swift屬性:

  • 具有可空屬性屬性(nonnullnullablenullresettable)的屬性作為Swift屬性導入,具有可選或非可選類型,如可空項和可選項所述。
  • 具有readonly屬性屬性的屬性將導入為具有getter({ get })的Swift計算屬性。
  • 具有weak屬性屬性的屬性將導入為標有weak關鍵字(weak var)的Swift屬性。
  • weak外與所有權有關的屬性(即,assigncopystrong,或unsafe_unretained)被導入為合適的Swift屬性存儲。
  • class屬性導入為Swift類型屬性。
  • 原子屬性(atomicnonatomic)不會反映在相應的Swift屬性聲明中,但是從Swift訪問導入的屬性時,Objective-C實現的原子屬性仍然保持不變。
  • Swift忽略 Accessor屬性(getter=setter=)。

您可以使用點語法訪問Swift中的Objective-C對象上的屬性,使用不帶括號的屬性名稱。

例如,您可以使用以下代碼設置對象的屬性textColortext屬性UITextField

myTextField.textColor = .darkGray
myTextField.text = "Hello world"

返回值并且不帶參數的Objective-C方法可以像使用點語法的Objective-C屬性一樣調用。但是,由Swift作為實例方法導入,因為只有Objective-C @property聲明由Swift作為屬性導入。方法被導入并按照“ 使用方法”章節中的描述進行調用。


使用方法

您可以使用點語法從Swift調用Objective-C方法。

當將Objective-C方法導入到Swift中時,Objective-C選擇器的第一部分將成為基本方法名稱,并顯示在括號之前。第一個參數立即出現在括號內,沒有名稱。選擇的其余部分對應于參數名稱,并顯示在括號內。調用時需要所有選擇器對應的參數。

例如,在Objective-C中,你可以這樣做:

[myTableView insertSubview:mySubview atIndex:2];

在Swift,你這樣做:

myTableView.insertSubview(mySubview, at: 2)

如果您調用一個沒有參數的方法,那么還必須包含括號。

myTableView.layoutIfNeeded()



id兼容性

Objective-C id類型被Swift導入為Any。在編譯時和運行時,當Swift值或對象作為id參數傳入Objective-C時,編譯器將引入通用橋接轉換操作。當id值導入Swift時成為Any,運行時會自動處理橋接到類引用或Swift值類型。

var x: Any = "hello" as String
x as? String // String with value "hello"
x as? NSString // NSString with value "hello"

x = "goodbye" as NSString
x as? String // String with value "goodbye"
x as? NSString // NSString with value "goodbye"

向下轉換Any

當處理的Any是已知基礎類型或可以合理確定的類型的對象時,將這些對象降級到更具體的類型通常是有用的。但是,由于該Any類型可以引用任何類型,所以不能保證向更具體類型的轉換成功。

您可以使用條件類型轉換運算符(as?),該運算符返回一個可選的您嘗試向下轉換的類型值:

let userDefaults = UserDefaults.standard
let lastRefreshDate = userDefaults.object(forKey: "LastRefreshDate") // lastRefreshDate is of type Any?
if let date = lastRefreshDate as? Date {
print("(date.timeIntervalSinceReferenceDate)")
}

如果您確定對象的類型,可以使用強制downcast操作符(as!)。

let myDate = lastRefreshDate as! Date
let timeInterval = myDate.timeIntervalSinceReferenceDate

但是,如果強制降級失敗,則會觸發運行時錯誤:

let myDate = lastRefreshDate as! String // Error

動態方法查找

Swift還包括一種AnyObject表示某種對象的類型,并具有動態查找任何@objc方法的特殊功能。這允許您訪問返回id值的Objective-C API時保持未定義類型的靈活性。

例如,您可以將任何類類型的對象分配給AnyObject類型的常量或變量,然后將再分配給其他類型的對象。您還可以調用任何Objective-C方法并訪問AnyObject值上的任何屬性而不轉換為更具體的類類型。

var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow

無法識別的選擇器和可選鏈接

因為在AnyObject運行時之前,值的具體類型是不知道的,所以有可能無意中寫入不安全的代碼。在Swift以及Objective-C中,嘗試調用不存在的方法會觸發無法識別的選擇器錯誤。

例如,以下代碼編譯時沒有編譯器警告,但會在運行時觸發錯誤:

myObject.character(at: 5)
// crash, myObject doesn't respond to that method

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,但建議您采取措施來防止意外行為。



可空性和可選項

在Objective-C中,使用裸指針來處理對可能為NULL的對象(Objective-C中稱為nil)的引用。在Swift中,所有值(包括結構和對象引用)都被保證為非空值。作為替代,通過將值的類型包裝在可選類型中來表示可能丟失的值。當您需要指出值丟失時,你可以使用nil值。有關可選項的更多信息,請參見The Swift Programming Language (Swift 4)文檔里的Optional章節

Objective-C可以使用可空性注釋來指定參數類型、屬性類型或返回類型可以有NULLnil值。單個類型聲明可以使用_Nullable_Nonnull修飾來進行靜態編譯時檢查,單個屬性聲明可以使用nullablenonnullnull_resettable修飾來進行檢查,或整個區域可使用NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END宏來設置可空性檢查。如果沒有為類型提供可空性信息,則Swift不能區分可選項和不可選項引用,并將其導入為隱式解包可選項。

  • 聲明為nonnullable或有_Nonnull修飾、或處在整個靜態編譯檢查區域的類型,會被Swift導入為nonoptional(不可選項)
  • 聲明為nullable或有_Nullable修飾的類型,會被Swift導入為optional(可選項)
  • 沒有可空性聲明的類型,會被Swift導入為implicitly unwrapped optional(隱式解包可選項)

例如,參考以下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!)

大多數Objective-C系統框架(包括Foundation)都提供了可空性注釋,允許您以慣用的和類型安全的方式處理值。

橋接可選項到不可空對象

Swift根據可選項是否包含包裝了的值,將可選值橋接至Objective-C對象。如果可選項是nil,Swift將該nil橋接為NSNull實例。否則,Swift將可選項橋接為其展開的值。例如,當可選項傳遞給Objective-C API中具有非空值的id類型,或者將可選項數組([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

由于參數valueFromSwiftid類型的,它以Swift的Any類型導入下面的Swift代碼。但是,由于Any在預期之中時傳遞可選項并不常見,所以傳遞給logSomeValue(_:)類方法的可選項被顯式轉換為Any類型,這會使編譯警告靜默。

let someValue: String? = "Bridge me, please."
let nilValue: String? = nil

OptionalBridging.logSomeValue(someValue as Any) // Bridge me, please.
OptionalBridging.logSomeValue(nilValue as Any) // Received an NSNull value.



協議限制類

由一個或多個協議限定的Objective-C類由Swift作為協議組合類型導入。例如,給定以下引用視圖控制器的Objective-C屬性:

@property UIViewController< UITableViewDataSource, UITableViewDelegate> * myController;

以下是Swift導入的方式:

var myController: UIViewController & UITableViewDataSource & UITableViewDelegate

Objective-C協議限制的元類由Swift作為協議元類型導入。例如,給定以下Objective-C方法對指定的類執行操作:

- (void)doSomethingForClass:(Class< NSCoding>)codingClass;

以下是Swift導入的方式:

func doSomething(for codingClass: NSCoding.Type)



輕量級泛型

使用通過輕量泛型聲明的Objective-C的參數化類型由Swift導入時,包含著其保存的類型的內容。例如,給定如下Objective-C的屬性聲明:

@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中將作為具有相同數量類型參數的泛型類。Swift導入的所有Objective-C通用類型參數都有一個類型約束,它要求該類型為一個類(T: Any)。如果Objective-C的泛型參數化類指定了類限定,則導入的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中使用的名稱相同的名稱來引用類,結構或枚舉。

例如,您可以根據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

您也可以使用擴展來添加協議一致性而不對其進行子類化。如果在Swift中定義了協議,那么您也可以將它添加到結構或枚舉中,無論是在Swift還是Objective-C中定義。

您不能使用擴展來覆蓋Objective-C類型上的現有方法或屬性。



閉包

Objective-C的block被Swift自動導入為具有Objective-Cblock調用約定的閉包,由@convention(block)屬性表示。例如,這里是一個Objective-C塊變量:

void (^completionBlock)(NSData *) = ^(NSData *data) {
 // ...
}

這里是Swift的樣子:

let completionBlock: (Data) -> Void = { data in
 // ...
}

Swift的閉包和Objective-C的block是兼容的,所以您可以將Swift閉包傳遞給Objective-C方法中預期的block。Swift的閉包和函數具有相同的功能,所以你甚至可以傳遞Swift函數的名稱。

閉包具有與block類似的捕獲語義,但在一個關鍵方面有所不同:變量是可變的而不是復制的。換句話說,Objective-C中__block的行為是Swift中變量的默認行為。

避免獲取自己時產生強引用循環

在Objective-C中,如果您需要在block中獲取self,那么考慮內存管理的含義就顯得很重要。

block保留對任何獲取的對象的強引用,包括self。如果self保持對block的強引用,例如拷貝屬性,這將創建一個強引用循環。為了避免這種情況,你可以讓block獲取一個弱引用self

__weak typeof(self) weakSelf = self;
self.block = ^{
 __strong typeof(self) strongSelf = weakSelf;
 [strongSelf doSomething];
};

像Objective-C中的block一樣,Swift中的閉包也保持對任何獲取的對象的強引用,包括self。為了防止強引用循環,您可以在閉包的捕獲列表里指定selfunowned

self.closure = { [unowned self] in
 self.doSomething()
}

欲了解更多信息,請參見 The Swift Programming Language (Swift 4)Resolving Strong Reference Cycles for Closures 章節。



對象比較

您可以在Swift中的兩個對象之間進行兩種截然不同的比較。第一種,平等性(==),比較對象的內容。第二種,一致性(===)確定常量或變量是否引用相同的對象實例。

Swift提供了=====操作符的默認實現,并為從NSObject類派生的對象采用Equatable協議。==操作符的默認實現調用isEqual:方法,===運算符的默認實現檢查指針一致性。您不應該重載從Objective-C導入的類型的平等性或一致性運算符。

NSObject類提供的isEqual:的基本實現等同于通過指針相等性的身份檢查。您可以在一個子類覆蓋isEqual:方法,使Swift和Objective-C API基于對象的內容而不是其指針一致性。有關比較邏輯的詳細信息,請參閱 *** Cocoa Core Competencies*** 的 Object comparison 章節。

注意
Swift自動提供平等性和一致性運算符的邏輯互補實現(!=!==)。這些不應該被重載。

哈希

Swift將聲明為NSDictionary的沒有指定鍵類型的Objective-C對象導入為Key類型為AnyHashableDictionary類型。類似的,不具有類型限定的NSSet類型被導入為Element類型為AnyHashableSet類型。如果NSDictionaryNSSet聲明分別對其鍵或對象類型進行參數化,則使用該類型。例如,給定以下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>

當導入未指定或id這樣的不能導入為Any的Objective-C類型時,Swift會使用AnyHashable類型,因為類型需要遵守Hashable協議。該AnyHashable類型是從任何Hashable類型隱式轉換的,您可以使用as?as!運算符將AnyHashable轉換為更具體的類型。

有關更多信息,請參閱AnyHashable章節。



Swift類型兼容性

當從Objective-C類創建Swift類時,可以從Objective-C中自動獲得與Objective-C兼容的類及其成員屬性、方法、下標和構造器。這不包括僅Swift擁有的功能,例如列出的功能:

  • 泛型
  • 元組
  • 在Swift中定義的不包含Int原始值類型的枚舉
  • Swift中定義的結構
  • Swift中定義的頂級函數
  • Swift中定義的全局變量
  • 在Swift中定義的類型別名
  • Swift風格的變體
  • 嵌套類型
  • 柯里化函數

Swift API被翻譯成Objective-C,類似于Objective-C API如何翻譯成Swift,但反過來:

  • Swift可選類型注釋為__nullable
  • Swift非選擇類型被注釋為__nonnull
  • Swift常量的存儲屬性和計算屬性成為只讀Objective-C屬性
  • Swift變量的存儲屬性成為讀寫Objective-C屬性
  • Swift類屬性成為具有class屬性的Objective-C屬性
  • Swift類方法成為Objective-C類方法
  • Swift構造器和實例化方法成為Objective-C實例方法
  • 拋出錯誤的Swift方法成為具有NSError **參數的Objective-C方法。如果Swift方法沒有參數,AndReturnError:將附加到Objective-C方法名稱上,否則附加error:。如果Swift方法未指定返回類型,則相應的Objective-C方法具有BOOL返回類型。如果Swift方法返回不可選類型,則相應的Objective-C方法具有可選的返回類型。

例如,參考以下Swift聲明:

class Jukebox: NSObject {
 var library: Set< String>
 
 var nowPlaying: String?
 
 var isCurrentlyPlaying: Bool {
  return nowPlaying != nil
 }
 
 class var favoritesPlaylist: [String] {
  // return an array of song names
 }
 
 init(songs: String...) {
  self.library = Set< String>(songs)
 }

func playSong(named name: String) throws {
  // play song or throw an error if unavailable
 }
}

以下是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的類

在Objective-C中配置Swift的接口

在某些情況下,您需要對Swift API如何暴露于Objective-C進行更細粒度的控制。您可以使用@objc(name)屬性來更改接口中暴露于Objective-C代碼的類,屬性,方法,枚舉類型或枚舉情況聲明的名稱。

例如,如果你的Swift類名稱中包含Objective-C不支持的字符,則您可以提供其在Objective-C中的替代字符。如果您為Swift函數提供Objective-C名稱,請使用Objective-C選擇器語法。記著在參數跟隨選擇器的地方加一個冒號(:)。

@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中使用,而無需任何命名空間。因此,當將可歸檔的Objective-C類遷移到Swift時,此屬性也將非常有用。因為歸檔對象將其類的名稱存儲在存檔中,您應該使用@objc(name)屬性來指定與Objective-C類相同的名稱,以便舊的存檔可以由新的Swift類取消存檔。

注意
相反的,Swift還提供了一個@nonobjc屬性,這使得在Objective-C中不能使用Swift聲明。您可以使用它來解決橋接方法的循環性,并允許由Objective-C導入的類的方法重載。如果通過不能在Objective-C中表示的Swift方法覆蓋Objective-C方法,例如將參數指定為變量,則該方法必須標記為@nonobjc

需要動態調度

可以從Objective-C調用的Swift API必須通過動態調度才能使用。然而,當從Swift代碼調用這些API時,動態調度的可用性并不能阻止Swift編譯器選擇更有效的調度方法。

您使用@objc屬性及dynamic修飾符要求通過Objective-C運行時動態調度成員的訪問。要求這種動態調度是很沒有必要的。然而,對于使用Objective-C運行時的API,比如鍵值觀察者(KVO)或者 method_exchangeImplementations 方法,這一類在運行時需要動態替換具體實現的情況,動態調度是很有必要的。

dynamic修飾符標記的聲明也必須用@objc屬性顯式標記,除非@objc屬性被聲明的上下文隱式添加。有關什么時候@objc隱式添加屬性的信息,請參閱 The Swift Programming Language (Swift 4)Declaration Attributes 章節。



選擇器

在Objective-C中,選擇器是一種引用Objective-C方法名稱的類型。在Swift中,Objective-C選擇器由Selector結構表示,可以使用#selector表達式構造。要為可以從Objective-C調用的方法創建一個選擇器,請傳遞方法的名稱,例如#selector(MyViewController.tappedButton(_:))。要為屬性的Objective-C gettersetter方法構造一個選擇器,請傳遞以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))。

Objective-C方法的不安全調用

您可以使用具有perform(_:)方法或其變體之一的選擇器在Objective-C兼容對象上調用Objective-C方法。調用使用選擇器的方法本質上是不安全的,因為編譯器不能對結果做出任何保證,甚至不能保證對象是否響應選擇器。因此,除非您的代碼特別依賴于Objective-C運行時提供的動態方法解析,否則強烈建議不要使用這些API。例如,如果你需要在其界面中實現使用目標動作設計模式的類,那么這樣時恰當的,如同NSResponder所做的。而在大多數情況下,將對象轉化為AnyObject以及在方法調用時使用可選鏈接更加安全和方便,如同id兼容性章節所述。

因為通過執行選擇器返回的值的類型和所有權在編譯時無法確定,所以同步執行的選擇器的方法,例如perform(_:),返回一個將隱式解包的可選項指到一個AnyObject實例上的非托管指針(Unmanaged< AnyObject>!)。相反,在特定線程上執行選擇器或延遲之后的方法,例如 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())
}
// Prints "hello, cocoa!"

嘗試調用一個對象的無法識別的選擇器的方法會導致接收方調用doesNotRecognizeSelector(_:),默認情況下會引發NSInvalidArgumentException異常。

let array: NSArray = ["delta", "alpha", "zulu"]

// Not a compile-time error because NSDictionary has this selector.
let selector = #selector(NSDictionary.allKeysForObject)

// Raises an exception because NSArray does not respond to this selector.
array.perform(selector)



鍵和鍵路徑

在Objective-C中,是用于標識對象的特定屬性的字符串。鍵路徑是一個由用點作分隔符的鍵組成的字符串,用于指定一個連接在一起的對象性質序列。鍵路徑經常用于鍵值對編碼(KVC),一種間接訪問對象的屬性使用字符串來標識屬性的機制。鍵路徑也常用于鍵值對觀察者(KVO),一種可以在另一個對象的屬性更改時直接通知對象的機制。

在Swift中,您可以使用鍵路徑表達式創建訪問屬性的關鍵路徑。例如,您可以使用\Animal.namekey-path表達式來訪問下面顯示nameAnimal類的屬性。使用鍵路徑表達式創建的關鍵路徑包括有關其引用的屬性的類型信息。將實例的關鍵路徑應用于實例會產生與直接訪問該實例屬性相同類型的值。鍵路徑表達式接受屬性引用和鏈接屬性引用,例如\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"
llama[keyPath: nameCountAccessor]
// "5"

在Swift中,你也可以使用#KeyPath字符串表達式來創建可以在如value(forKey:)value(forKeyPath:)之類的KVC方法中使用的編譯檢查鍵和鍵路徑,以及類似addObserver(_:forKeyPath:options:context:)的KVO方法。#keyPath字符串表達式允許鏈式方法或屬性引用。它還支持通過鏈中的可選值鏈接,例如#keyPath(Person.bestFriend.name)。與使用鍵路徑表達式創建的關鍵路徑不同,使用#keyPath字符串表達式創建的關鍵路徑不會傳遞有關其引用到接受關鍵路徑的API的屬性或方法的類型信息。

注意
#keyPath字符串表達式的語法類似于#selector表達的語法,如選擇器章節所述。



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)
// "name"
gabrielle.value(forKey: #keyPath(Person.name))
// "Gabrielle"
#keyPath(Person.bestFriend.name)
// "bestFriend.name"
gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name))
// "Yuanyuan"
#keyPath(Person.friends.name)
// "friends.name"
gabrielle.value(forKeyPath: #keyPath(Person.friends.name))
// ["Yuanyuan", "Jim"]






文章翻譯自Apple Developer Page : Using Swift with Cocoa and Objective-C (Swift 4)
方便大家學習之用,如果翻譯存在錯誤,歡迎指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容