混用swift和objc - 与objc APIs交互
Swift
与Objective-C
之间具备双向的互操作性,在一种语言中可以使用另一种语言写的代码。目前,在用 Swift 写新项目时,可能会调用以前 objc 写的代码,这是一个重要的方面。苹果很好的做到了这一点,在用原生 Swift 写代码时,可以通过导入 objc 文件,就可以初始化 objc 对象和调用方法了。
objc 的初始化方法都以init
开头,with
后面的参数都会放在 Swift 方法后的括号中。Swift 中初始化的方法名都为init
,而后面括号中的参数可以不同:
- (instancetype)init;
- (instancetype)initWithFrame:(CGRect)frame
style:(UITableViewStyle)style;
上面的 objc 代码转化为 Swift 代码:
init() { /* ... */ }
init(frame: CGRect, style: UITableViewStyle) { /* ... */ }
在调用初始化方法时:
UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
在 Swift 中则是这样:
let myTableView: UITableView = UITableView(frame: .zero, style: .grouped)
类工厂方法和快捷初始化器
在 objc,可以这样用类工厂方法:
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)
(在 Swift 中,类方法跟初始化方法调用起来没啥不同。。)
可失败的初始化
在 objc 中,如果初始化失败,会返回nil
。在 Swift 中,这种特性叫做failable initialization
。objc 中,可以通过nullability annotations
反应初始化是否会失败,但这不是强制的,详见可空和可选。Swift 中,不会失败的初始化方法定义为init(...)
,而可能失败的初始化方法定义为init?(...)
。另外,objc 的初始化方法会转换成init!(...)
。
比如,一个UIImage
对象可能因为图片地址错误而初始化失败:
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 访问属性的方式:
myTextField.textColor = .darkGray
myTextField.text = "Hello world"
objc 中,无参数有返回值的方法可以用.
访问,但这类方法只会导入成 Swift 的实例方法。
objc 方法的第一部分会变成 Swift 方法的方法名,之后的部分变成参数名,在 Swift 中调用时,第一个参数不需要参数名,其他参数需要些参数名。
[myTableView insertSubview:mySubview atIndex:2];
myTableView.insertSubview(mySubview, at: 2)
调用的方法没有参数时,也要写上括号:
myTableView.layoutIfNeeded()
id 的兼容性
objc 中的id
类型导入 Swift 中变成Any
类型。在编译时和运行时,当 Swift 值作为id
参数传给 objc,编译器会提供通用的桥接转换操作。当id
值作为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
类型的对象的确切类型时,可以将其向下转化(downcasting)为一个更确切的对象,但向下转换不保证可以成功。
条件类型转换操作符(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
如果强制转换错误,会导致 crash:
let myDate = lastRefreshDate as! String // Error
动态方法查找
Swift 提供AnyObject
对象,可以代表一些类型的对象,并且可以动态查找任何@any
方法。这样,对于 objc 中返回id
的方法,你可以保持无类型访问的灵活性。
var myObject: AnyObject = UITableViewCell()
myObject = NSDate()
let futureDate = myObject.addingTimeInterval(10)
let timeSinceNow = myObject.timeIntervalSinceNow
未识别的选择子和可选链
调用一个AnyObject
的不存在的方法时,会导致程序 crash。Swift 利用可选方式防止这样不安全的行为。当你调用一个AnyObject
的方法时,这个方法调用会表现的像隐式解包可选值。你可以用同样的可选链的语法来在AnyObject
上调用方法。
// 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
指定参数值、属性或返回值是否可以是NULL
或nil
。单个类型声明使用_Nullable
或_Nonnull
,单个属性声明使用nullable
、nonnull
和null_resettable
,整体域对于可空值使用NS_ASSUME_NONNULL_BEGIN
和NS_ASSUME_NONNULL_END
宏。如果没有提供可空信息,Swift 会导入为隐式解包可选值。
objc 声明:
@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!) {}
桥接可选值与非空对象
根据可选值是否有值,Swift 将可选值桥接到非空 objc 对象。如果可选值是nil
,Swift 将其桥接为NSNull
实例。否则,桥接为解包值。可选值的数组会桥接为NSArray
。
@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]);
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 中的协议类型值。
- (void)doSomethingForClass:(Class<NSCoding>)codingClass;
func doSomething(for codingClass: NSCoding.Type) {}
轻量级泛型(Lightweight Generics)
objc 使用lightweight generic
的类型声明导入 Swift 将提供保存内容的类型信息。
@property NSArray<NSDate *> *dates;
@property NSCache<NSObject *, id<NSDiscardableContent>> *cachedData;
@property NSDictionary <NSString *, NSArray<NSLocale *>> *supportedLocales;
var dates: [Date]
var cachedData: NSCache<AnyObject, NSDiscardableContent>
var supportedLocales: [String: [Locale]]
所有导入 Swift 的 objc 泛型类型参数有一个类型限制,需要那个类型是一个类(T: Any
)。
@interface List<T: id<NSCopying>> : NSObject
- (List<T> *)listByAppendingItemsInList:(List<T> *)otherList;
@interface ListContainer : NSObject
- (List<NSValue *> *)listOfValues;
@interface ListContainer (ObjectList)
- (List *)listOfObjects;
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 中的分类类似。
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 可以用关联对象实现):
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 的闭包。
void (^completionBlock)(NSData *) = ^(NSData *data) {
// ...
let completionBlock: (Data) -> Void = { data in
// ...
block 和闭包是兼容的,所以可以将闭包作为 block 传给 objc 方法。而且闭包和 Swift 方法是相同类型的,所以也可以将 Swift 方法直接传过去。
闭包和 block 相似,会捕获变量,但是方式不同:变量可以修改,而不是拷贝。也就是说,objc 中的__block
是 Swift 中变量的默认行为。
捕获 self 时防止循环引用
objc 中防止循环引用:
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf doSomething];
Swift:
self.closure = { [unowned self] in
self.doSomething()
在 Swift 中,两种比较对象方式:
equality(==)
:比较对象内容
identity(===)
:是否指向相同的对象实例
==
的默认实现是调用isEqual:
方法,===
的默认实现是检查指针是否相等。这两个操作符不应该重载。NSObject 提供的isEqual:
的基本实现跟检查指针等同性一样,这个方法可以在子类中重载。
@property NSDictionary *unqualifiedDictionary;
@property NSDictionary<NSString *, NSDate *> *qualifiedDictionary;
@property NSSet *unqualifiedSet;
@property NSSet<NSString *> *qualifiedSet;
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 方法有一个可选的返回值。
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
@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;
不能在 Objective-C 中子类化一个 Swift 类。
在 objc 中配置 Swift 接口
用@objc(name)
属性可以修改你接口中暴露给 objc 的类名、属性、方法、枚举类型或枚举情况声明(enumeration case declaration)。
@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 来动态分派。通常这不是必须的,但使用KVO
或method_exchangeImplementations
方法时需要这么做。
标记为dynamic
的声明不能再用@nonobjc
标记。
在 Swift 中,objc 中的选择子用Selector
结构体表示,并且可以用#selector
表达式构造。要构造一个能被 objc 调用的方法的选择子,如#selector(MyViewController.tappedButton(sender:))
。要构造 objc 的 getter 和 setter
方法的选择子,要用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: 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.
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)
Keys and Key Paths
在 Swift 中,可以用#keyPath
表达式生成一个通过编译器检查的 key 和 key paths,交给 KVC 方法(value(forKey:)
和value(forKeyPath:)
)和 KVO 方法(addObserver(_:forKeyPath:options:context:)
)使用。
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"]