翻译自苹果官方文档
和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<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] {
// 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类。
调整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())
}
// 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"]
网友评论