转载

Swift和Objective-C混编

翻译自苹果官方文档

和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") {
    // loaded the image successfully
} else {
    // could not load the image
}

获取属性

在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   // 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)")
}

如果你能确定对象的类型,那么可以用强制向下类型转换(as!)。

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

如果强制向下转换失败,会触发一个运行时错误:

let myDate = lastRefreshDate as! String // Error

动态方法查找

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)
// 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类型的值调用方法时,一定要解包,但还是推荐解包,以此来避免不可预知的行为。(译者注: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)  // Bridge me, please.
OptionalBridging.logSomeValue(nilValue as Any)   // Received an NSNull value.

协议限定类(Protocol-Qualified Classes)

实现了一个或者多个协议的Objective-C类,在Swift中会转换成协议组合类型。例如以下的Objective-C中定义的view controller属性

@property UIViewController * 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)codingClass;

Swift中的样子:

func doSomething(for codingClass: NSCoding.Type)

轻量级泛型

用轻量级泛型参数化(译者注:generic parameterization泛型参数化,意思是,给泛型指定了类型,也即尖括号里有具体的类型。所以后面说到的参数化,都是指已经给泛型指定了具体的类型)。声明的Objective-C类型,内容的类型信息在Swift会保留下来。例如下面的Objective-C属性声明:(译者注:Objective-C里面的泛型实际是Xcode7引入的一个语法糖,称为轻量级泛型。Stack Overflow上有这个讨论)

@property NSArray *dates;
@property NSCache *cachedData;
@property NSDictionary  *supportedLocales;

Swift中会是这样:

var dates: [Date]
var cachedData: NSCachevar 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 : NSObject
- (List *)listByAppendingItemsInList:(List *)otherList;
@end
 
@interface ListContainer : NSObject
- (List *)listOfValues;
@end
 
@interface ListContainer (ObjectList)
- (List *)listOfObjects;
@end

Swift中是这个样子的:

class List : NSObject {
    func listByAppendingItemsInList(otherList: List) -> List}

class ListContainer : NSObject {
    func listOfValues() -> List}

extension ListContainer {
    func listOfObjects() -> List}

扩展

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 *qualifiedDictionary;

@property NSSet *unqualifiedSet;
@property NSSet *qualifiedSet;

转为Swift:

var unqualifiedDictionary: [AnyHashable: Any]
var qualifiedDictionary: [String: Date]

var unqualifiedSet: Setvar qualifiedSet: Set

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  
    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(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 *library;
@property (nonatomic, copy, nullable) NSString *nowPlaying;
@property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying;
@property (nonatomic, class, readonly, nonnull) NSArray *favoritesPlaylist;
- (nonnull instancetype)initWithSongs:(NSArray * __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!),因为返回值的类型和所有者在编译期不能确定。按照约定,在一个指定的线程,或者做一个延时再执行一个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())
}
// Prints "hello, cocoa!"

对象调用不能识别的selector,会触发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)

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"
llama[keyPath: nameCountAccessor]
// "5"

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)
// "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"]

作者:桔子听

链接:https://www.jianshu.com/p/742c6af3afa

正文到此结束
Loading...