混用swift和objc - 与objc APIs交互

SwiftObjective-C之间具备双向的互操作性,在一种语言中可以使用另一种语言写的代码。目前,在用 Swift 写新项目时,可能会调用以前 objc 写的代码,这是一个重要的方面。苹果很好的做到了这一点,在用原生 Swift 写代码时,可以通过导入 objc 文件,就可以初始化 objc 对象和调用方法了。

初始化

objc 的初始化方法都以init开头,with后面的参数都会放在 Swift 方法后的括号中。Swift 中初始化的方法名都为init,而后面括号中的参数可以不同:

1
2
3
- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame
style:(UITableViewStyle)style;

上面的 objc 代码转化为 Swift 代码:

1
2
init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }

在调用初始化方法时:

1
UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];

在 Swift 中则是这样:

1
let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)

类工厂方法和快捷初始化器

在 objc,可以这样用类工厂方法:

1
UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

Swift:

1
let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

(在 Swift 中,类方法跟初始化方法调用起来没啥不同。。)

可失败的初始化

在 objc 中,如果初始化失败,会返回nil。在 Swift 中,这种特性叫做failable initialization。objc 中,可以通过nullability annotations反应初始化是否会失败,但这不是强制的,详见可空和可选。Swift 中,不会失败的初始化方法定义为init(...),而可能失败的初始化方法定义为init?(...)。另外,objc 的初始化方法会转换成init!(...)

比如,一个UIImage对象可能因为图片地址错误而初始化失败:

1
2
3
4
5
if let image = UIImage(contentsOfFile: "MyImage.png") {
// loaded the image successfully
} else {
// could not load the image
}

访问属性

objc 中的属性会以以下方式转化:

  • 可空的属性(nonnull, nullable, null_resettable)会以可选或非可选类型导入,详见可空和可选
  • 只读属性导入为带getter的属性
  • weak属性转化为 Swift 属性后,用weak标记(weak var
  • 其他内存管理修饰符(assign, copy, strong, unsafe_unretained)转化为正确的存储方式
  • class性质的属性导入为 Swift 的类型属性
  • 原子性(atomic, nonatomic)不会显示在 Swift 的属性声明中,但从 Swift 中访问 objc 属性时,objc 定义的原子性将会保持。
  • 存取方法名(getter=, setter=)会被 Swift 忽略

Swift 访问属性的方式:

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

objc 中,无参数有返回值的方法可以用.访问,但这类方法只会导入成 Swift 的实例方法。

调用方法

objc 方法的第一部分会变成 Swift 方法的方法名,之后的部分变成参数名,在 Swift 中调用时,第一个参数不需要参数名,其他参数需要些参数名。

1
[myTableView insertSubview:mySubview atIndex:2];

转化后

1
myTableView.insertSubview(mySubview, at: 2)

调用的方法没有参数时,也要写上括号:

1
myTableView.layoutIfNeeded()

id 的兼容性

objc 中的id类型导入 Swift 中变成Any类型。在编译时和运行时,当 Swift 值作为id参数传给 objc,编译器会提供通用的桥接转换操作。当id值作为Any导入 Swift 时,运行时自动处理。

1
2
3
4
5
6
7
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类型的对象的确切类型时,可以将其向下转化(downcasting)为一个更确切的对象,但向下转换不保证可以成功。

条件类型转换操作符(as?)返回一个可选值。

1
2
3
4
5
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!):

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

如果强制转换错误,会导致 crash:

1
let myDate = lastRefreshDate as! String // Error

动态方法查找

Swift 提供AnyObject对象,可以代表一些类型的对象,并且可以动态查找任何@any方法。这样,对于 objc 中返回id的方法,你可以保持无类型访问的灵活性。

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

未识别的选择子和可选链

调用一个AnyObject的不存在的方法时,会导致程序 crash。Swift 利用可选方式防止这样不安全的行为。当你调用一个AnyObject的方法时,这个方法调用会表现的像隐式解包可选值。你可以用同样的可选链的语法来在AnyObject上调用方法。

1
2
3
4
5
6
7
8
9
// 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

可空和可选

objc 用nullability annotations指定参数值、属性或返回值是否可以是NULLnil。单个类型声明使用_Nullable_Nonnull,单个属性声明使用nullablenonnullnull_resettable,整体域对于可空值使用NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END宏。如果没有提供可空信息,Swift 会导入为隐式解包可选值。

objc 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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:

1
2
3
4
5
6
7
8
9
10
11
12
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!) {}

桥接可选值与非空对象

根据可选值是否有值,Swift 将可选值桥接到非空 objc 对象。如果可选值是nil,Swift 将其桥接为NSNull实例。否则,桥接为解包值。可选值的数组会桥接为NSArray

1
2
3
4
5
6
7
8
9
@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
1
2
3
4
5
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)

objc 协议限制的类导入为 Swift 中的协议类型值。

1
- (void)doSomethingForClass:(Class<NSCoding>)codingClass;
1
func doSomething(for codingClass: NSCoding.Type) {}

轻量级泛型(Lightweight Generics)

objc 使用lightweight generic的类型声明导入 Swift 将提供保存内容的类型信息。

1
2
3
@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;
1
2
3
var dates: [Date]
var cachedData: NSCache<AnyObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]

所有导入 Swift 的 objc 泛型类型参数有一个类型限制,需要那个类型是一个类(T: Any)。

1
2
3
4
5
6
7
8
9
10
11
@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
1
2
3
4
5
6
7
8
9
10
11
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 中的扩展和 objc 中的分类类似。

1
2
3
4
5
6
7
8
9
10
11
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()
}
}

跟分类一样,扩展只能添加可计算的属性,不能添加要存储的属性(虽然 objc 可以用关联对象实现):

1
2
3
4
5
6
7
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

不能使用扩展重载 objc 中已有的方法和属性。

闭包

用 objc block calling convertion (标记为@convention(block)属性),objc 的 block 可以自动导入为 Swift 的闭包。

1
2
3
void (^completionBlock)(NSData *) = ^(NSData *data) {
// ...
}
1
2
3
let completionBlock: (Data) -> Void = { data in
// ...
}

block 和闭包是兼容的,所以可以将闭包作为 block 传给 objc 方法。而且闭包和 Swift 方法是相同类型的,所以也可以将 Swift 方法直接传过去。

闭包和 block 相似,会捕获变量,但是方式不同:变量可以修改,而不是拷贝。也就是说,objc 中的__block是 Swift 中变量的默认行为。

捕获 self 时防止循环引用

objc 中防止循环引用:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
};

Swift:

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

对象比较

在 Swift 中,两种比较对象方式:

  • equality(==):比较对象内容
  • identity(===):是否指向相同的对象实例

==的默认实现是调用isEqual:方法,===的默认实现是检查指针是否相等。这两个操作符不应该重载。NSObject 提供的isEqual:的基本实现跟检查指针等同性一样,这个方法可以在子类中重载。

哈希

1
2
3
4
5
@property NSDictionary *unqualifiedDictionary;
@property NSDictionary<NSString *, NSDate *> *qualifiedDictionary;
@property NSSet *unqualifiedSet;
@property NSSet<NSString *> *qualifiedSet;
1
2
3
4
5
var unqualifiedDictionary: [AnyHashable: Any]
var qualifiedDictionary: [String: Date]
var unqualifiedSet: Set<AnyHashable>
var qualifiedSet: Set<String>

Swift 类型兼容

Swift-only features:

  • Generics
  • Tuples
  • Enumerations defined in Swift without Int raw value type
  • Structures defined in Swift
  • Top-level functions defined in Swift
  • Global variables defined in Swift
  • Typealiases defined in Swift
  • Swift-style variadics
  • Nested types
  • Curried functions

Swift API 转换成 objc 的方式与 objc API 如何装换成 Swift 类似,但反过来转换的时候:

  • Swift 可选类型标注为__nullable
  • 不可选类型标注为__nonnull
  • 常量属性和计算的属性变成只读的
  • 存储的变量变成读写的
  • Swift type properties become Objective-C properties with the class property attribute.
  • 类型方法变成类方法
  • 初始化和实例方法变成实例方法
  • 抛出错误的方法变成带NSError **变量的方法,如果 Swift 方法没有参数,AndReturnError:添加到 objc 方法名中,否则添加error:。如果 Swift 方法没有明确返回类型,对应的 objc 方法有一个布尔返回值。如果 Swift 方法返回非可选类型,对应的 objc 方法有一个可选的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}
}
1
2
3
4
5
6
7
8
@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 类。

在 objc 中配置 Swift 接口

@objc(name)属性可以修改你接口中暴露给 objc 的类名、属性、方法、枚举类型或枚举情况声明(enumeration case declaration)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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, вДереве дерево: Дерево) {
// ...
}
}

@objc(name)在将 objc 项目迁移到 Swift 时也很有用。归档对象使用它们的类名进行归档,可以用@objc(name)将名字指定为与 objc 一样,所以以前的归档可以在新的 Swift 类中解档。

Swift 还提供了 @nonobjc 属性,使声明在 objc 中不可见。You can use it to resolve circularity for bridging methods and to allow overloading of methods for classes imported by Objective-C. If an Objective-C method is overridden by a Swift method that cannot be represented in Objective-C, such as by specifying a parameter to be a variable, that method must be marked @nonobjc.

要求动态分派

当 Swift API 被 objc runtime 导入时,不能保证属性、方法、下标或初始器的动态分派。可以用dynamic要求成员的访问通过 objc runtime 来动态分派。通常这不是必须的,但使用KVOmethod_exchangeImplementations方法时需要这么做。

标记为dynamic的声明不能再用@nonobjc标记。

选择子

在 Swift 中,objc 中的选择子用Selector结构体表示,并且可以用#selector表达式构造。要构造一个能被 objc 调用的方法的选择子,如#selector(MyViewController.tappedButton(sender:))。要构造 objc 的 getter 和 setter
方法的选择子,要用getter:setter:作为前缀,如#selector(getter: MyViewController.myButton)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import UIKit
class MyViewController: UIViewController {
let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let action = #selector(MyViewController.tappedButton)
myButton.addTarget(self, action: action, forControlEvents: .touchUpInside)
}
func tappedButton(sender: UIButton?) {
print("tapped button")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}

An Objective-C method reference can be parenthesized, and it can use the as operator to disambiguate between overloaded functions, such as #selector(((UIView.insert(subview:at:)) as (UIView) -> (UIView, Int) -> Void)).

objc 方法的不安全调用

objc 中的perform(_ :)方法不安全,Swift 中比较好的做法是将对象转换成AnyObject,然后使用可选链。

The methods that perform a selector synchronously, such asperform(_:), return an implicitly unwrapped optional unmanaged pointer to an AnyObject instance (Unmanaged<AnyObject>!), because the type and ownership of the value returned by performing the selector can’t be determined at compile time. In contrast, the methods that perform a selector on a specific thread or after a delay, such as perform(_:on:with:waitUntilDone:modes:) and perform(_:with:afterDelay:), don’t return a value.

1
2
3
4
5
6
7
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异常。

1
2
3
4
5
6
7
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)

Keys and Key Paths

在 Swift 中,可以用#keyPath表达式生成一个通过编译器检查的 key 和 key paths,交给 KVC 方法(value(forKey:)value(forKeyPath:))和 KVO 方法(addObserver(_:forKeyPath:options:context:))使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person: NSObject {
var name: String
var friends: [Person] = []
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"]

参考