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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容