Objective-C API
为了让 Swift 继承 Objective-C 的成熟生态,苹果开发了 Clang Importer 将 Objective-C API 引入 Swift,并自动进行了若干命名转换。而 Swift 3 更是对命名原则做了大规模的调整,使之更清晰、更适应于 Swift,详见 SE-0005、SE-0006 两份提案,并且最终形成了一份 API Design Guidelines。
初始化
- 使用 Swift 调用 Objective-C 类的构造器时,方法名中的
init 和 initWith 前缀会被截去,其余各部分依次变为构造器的参数名。alloc 方法不必再手动调用,Swift 会自己处理内存分配。
- 简洁起见,Objective-C 类的工厂方法也被映射成了 Swift 的便利构造器。
- 在 Objective-C 中可能返回
nil 的构造器,在 Swift 中会被映射为可失败构造器。
// Objective-C
UITableView *myTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];
// Swift
let myTableView = UITableView(frame: .zero, style: .grouped)
let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)
属性
- Objective-C 中使用
@property 语法定义的属性会被映射为 Swift 中的属性;而不带参数且有返回值的方法虽然在 Objective-C 可以用点语法调用,但在 Swift 中仍然是普通的方法。
- Objective-C 属性声明中形如
(attribute) 的特性在 Swift 中会特殊处理:
readonly 会被映射为 Swift 中带 { get } 的计算属性;
- Swift 的
weak / unowned(unsafe) 关键字分别对应原来的 weak / unsafe_unretained;Swift 没有基本类型的概念,不考虑 assign;Swift 的值类型赋值默认进行拷贝,引用类型可以添加 @NSCopying,皆对应于 copy。
- Objective-C 中的属性默认拥有原子性,即会加锁以防止多线程同时访问;而 Swift 语言并没有该特性,即不保证原子性。Objective-C 中的
atomic 和 nonatomic 将不会反映在 Swift 的属性声明上,但其 Objective-C 实现仍然会保证其原子性。
可空性
- Objective-C 中的裸指针均可为
NULL 或 nil,前者为 (void *) 0 后者为 (id) 0;而 Swift 中所有类型默认是不可空的,并有更安全的可空类型来处理可空性。为了弥合语言间的不协调,Xcode 6.3(官方博客)为 Objective-C 引入了新的语法:
- 类型声明默认为
null_unspecified,将被映射为隐式解包可空类型;
nullable 将被映射为可空类型;
nonnull 将被映射为非可空类型。
id
- Objective-C 定义了
typedef struct objc_object *id,即 id 可以指向任意 Objective-C 类的对象,与 Swift 中的 AnyObject 类似。不过为了充分发挥 Swift 值类型的特性,Swift 3(SE-0116,官方博客)将其桥接范围扩展到了 Any,因此引入了一些额外的桥接规则。
- 为了将
Any 桥接到 id,编译器引入了通用桥接转换(universal bridging conversion):
- 引用类型的类在 Swift 和 Objective-C 中都存在,所以能直接转换;
- 可桥接的值类型如
String,会借助 _ObjectiveCBridgeable 协议转换到对应的 Objective-C 类如 NSString;
- 不可桥接的值类型会被封装在一个不可变类的实例中,我们不期待在 Objective-C 中能使用它,只求它能保证
id 兼容性、能在语言间往返即可。
- 将
id 桥接到 Any 则需要利用运行时模棱两可的动态转换(ambivalent dynamic casting),因为无法预先得知 Objective-C 对象是希望被桥接到值类型还是引用类型,因此会在动态类型转换时再决定。譬如 NSString 对象在桥接后既可以 as? String 也可以 as? NSString。
- 另外,
AnyObject 允许在不进行类型转换的情况下调用任何 Objective-C 的方法和属性。因为动态方法查找失败会触发运行时错误,所以 Swift 用可空类型对其进行了包装,AnyObject 上的方法调用行为类似于隐式解包可空值,可空链式调用等特性亦可适用。
let myObject: AnyObject = NSDate()
myObject.character(at: 5) // Unrecognized selector error!
myObject.character?(at: 5) // : unichar? = nil
闭包
- Objective-C 中的代码块和 Swift 中的闭包是互相兼容的:
// Objective-C
void (^completionBlock)(NSData *) = ^(NSData *data) { … }
// Swift
let completionBlock: (Data) -> Void = { data in … }
- 然而闭包和代码块有一个关键性的不同:闭包中的变量是可修改的,而不像代码块那样使用值拷贝。换句话说,Swift 闭包捕获的变量相当于都在 Objective-C 的变量声明中加了
__block。
判等
- Objective-C 只有一种等号,而 Swift 有两种:相等(
==)和相同(===)。
- Swift 让所有继承自
NSObject 的类遵循了 Equtable 协议,其 == 运算符的默认实现直接返回了 Objective-C isEqual: 方法的结果;
- 全局定义的
func === (lhs: AnyObject?, rhs: AnyObject?) -> Bool 会判断对象指针是否相等。
Selector
- Selector 可以在运行时表示 Objective-C 方法名,类似于成员函数指针。
Selector 结构体可以用字符串构造,不过对于字面量 Swift 2.2(SE-0022)引入了一种更安全的做法:通过 #selector 表达式来构造,编译器会检查方法是否存在。
let string: NSString = "Sapientia et Virtus"
let selector = #selector(NSString.lowercased(with:))
if let result = string.perform(selector, with: Locale.current) {
print(result.takeUnretainedValue()) // result : Unmanaged<AnyObject>
}
Key / Key Path
- Key 可以在运行时表示 Objective-C 对象的属性,而 Key Path 还能表示多级的链式路径。
- Key Path 常常用于键值编码(KVC)和键值观察(KVO):前者可以间接访问任意对象的属性,如
value(forKeyPath:) 和 setValue(_:forKeyPath:);后者用于观察任意对象属性的修改,如 addObserver(_:forKeyPath:options:context:)。
- Key Path 本质上就是以点分隔的字符串,不过 Swift 3(SE-0062)和 Swift 4(SE-0161)分别引入了新的构造方式:
- 通过
#keyPath 表达式来构造,如 #keyPath(Member.name),经编译器验证后会变成字符串 "Member.name";
- 形如
\Member.name 的表达式则会生成 KeyPath 对象,保留了各种类型信息,还可以通过 member[keyPath: \.name] 的语法来访问属性。
Cocoa 框架
- Swift 3(SE-0086)去掉了 Foundation 框架大部分类型名中的
NS 前缀,除了一些例外:
- 与 Objective-C 联系十分紧密的类:
NSObject NSAutoreleasePool NSException 等;
- 用于特定平台的类,它们虽然位于 Foundation 但其实应当属于 AppKit / UIKit 这些更高层的框架:
NSUserNotification NSBackgroundActivity NSXPCConnection 等;
- 在 Swift 中有值类型等价物的类,详见 SE-0069:
NSString NSDictionary NSURL 等。
- Foundation 中还定义了很多枚举和常量,在 Swift 中它们会成为相关类型的嵌套类型,如
NSJSONReadingOptions 会成为 JSONSerialization.ReadingOptions。
字符串
- 在 Swift 中应当尽量使用值类型的
String,避免引用类型的 NSString / NSMutableString。因为值类型可以使用 Swift 原生的 let / var 来控制对象内容是否可变,而引用类型则需要使用不同的类来实现。
- Objective-C 中有四种
NSLocalizedString 开头的宏来对字符串进行本地化,而 Swift 中这被简化为了单个函数 NSLocalizedString(_:tableName:bundle:value:comment:),其中后三个参数有默认值。
数字
Int / Double / Bool 等数字类型均可用 as 安全地桥接到 NSNumber,但反过来需要使用 as? 或 as!,因为 NSNumber 可以表示多种类型。
合集类型
NSArray / NSSet / NSDictionary 这三种合集类型,一开始是桥接到 [AnyObject] / Set<NSObject> / [NSObject: AnyObject]。得益于 Swift 3 的两份提案 SE-0116、SE-0131,现在已桥接到 [Any] / Set<AnyHashable> / [AnyHashable: Any]。
- Xcode 7.0 为 Objective-C 引入了轻量级的泛型,允许指定合集的元素类型,以方便与 Swift 桥接,如
NSArray<NSData*>* 将桥接到 [Data]。
Core Foundation
- 在 Swift 中,Core Foundation 所有类型的
Ref 后缀都会自动删掉,因为 Swift 的类一定是引用类型的。另外,可以指向任意 Core Foundation 类型的 CFTypeRef 桥接到了 AnyObject。
- 对于已经标注过的 CF API,Swift 会自动进行内存管理,不再需要
CFRetain 或 CFRelease;否则需要手动进行标注或是手动管理 Unmanaged 对象,详见 NSHipster 的文章。
日志记录
- 在 macOS 10.12 / iOS 10.0 / watchOS 3.0 / tvOS 10.0 及更高版本的平台上,统一日志记录系统于
os.log 模块提供了 os_log 函数来记录日志,以此取代 Foundation 中的 NSLog。
Cocoa 设计模式
委托
- 不论是 Swift 还是 Objective-C,委托均由协议来表达。被委托类型遵循委托协议并实现了一系列委托方法,而委托对象则会保存一个被委托类型的实例,并在各种事件发生时委托其处理。
class MyDelegate: NSObject, NSWindowDelegate {
func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize {
return proposedSize
}
}
myWindow.delegate = MyDelegate()
if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) {
print(NSStringFromSize(fullScreenSize))
}
错误处理
- 按照 Objective-C 的惯例,可能产生错误的方法接受一个
NSError** 作为输出参数,并返回 BOOL 值表示方法调用是否成功。
- 在 Swift 1 时代,这些 Objective-C API 没有什么变动,依然需要传入
NSError 对象指针。
- 在 Swift 2 时代,引入了语言原生的错误处理机制,所有错误参数被替换为
throws 关键字,返回类型从 BOOL 改为 Void。
单例
- 单例模式使用一个全局共享的实例,以提供资源或服务的统一接入点。
- 在 Objective-C 中,可以用
dispatch_once 保证单例只被创建一次。
+ (instancetype)sharedInstance {
static id _sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedInstance = [[self alloc] init];
});
return _sharedInstance;
}
- 在 Swift 中,可以直接使用惰性初始化的类型属性。如果调用构造器之后还要做其他初始化工作,可以写一个立即执行的闭包(IIFE)。
class Singleton {
static let sharedInstance: Singleton = {
let instance = Singleton()
// Setup…
return instance
}()
}
内省
- 内省(Introspection)即运行时类型检查,Objective-C 中的
isKindOfClass: 和 conformsToProtocol: 方法、Swift 中的 is 和 as? 运算符均属于此类。
- 另外,从 Swift 3 开始可以用
type(of:) 来获取一个对象的动态类型(SE-0096 之前是对象的 dynamicType 属性)。
序列化
- 在 Swift 中,遵循
Codable 协议的类型即可使用 JSON{En,De}coder 和 PropertyList{En,De}coder 进行序列化和反序列化,处理自定义类型详见官方文档。
命令行参数
- 可以通过
CommandLine.arguments 来访问命令行参数,这和 ProcessInfo.processInfo.arguments 等价。
C API
基本类型
- Swift 为 C 语言的基本类型提供了等价的类型,但它们不会隐式转换为 Swift 原生的数字类型。
| C |
Swift |
| bool |
CBool |
| char, signed char |
CChar |
| unsigned char |
CUnsignedChar |
| short |
CShort |
| unsigned short |
CUnsignedShort |
| int |
CInt |
| unsigned int |
CUnsignedInt |
| long |
CLong |
| unsigned long |
CUnsignedLong |
| long long |
CLongLong |
| unsigned long long |
CUnsignedLongLong |
| wchar_t |
CWideChar |
| char16_t |
CChar16 |
| char32_t |
CChar32 |
| float |
CFloat |
| double |
CDouble |
指针
- Swift 尽可能避免了直接使用指针,但仍然提供了多种指针类型以操作内存地址:
| C |
Swift |
| const T * |
UnsafePointer<T> |
| T * |
UnsafeMutablePointer<T> |
| C |
Swift |
| T * const * |
UnsafePointer<T> |
| T * __strong * |
UnsafeMutablePointer<T> |
| T ** |
AutoreleasingUnsafeMutablePointer<T> |
| C |
Swift |
| const void * |
UnsafeRawPointer |
| void * |
UnsafeMutableRawPointer |
- 以常量指针为参数的函数可以接受以下值:
- 常量指针、变量指针或自动释放指针;
- (如果
T 为 Int8 / UInt8)字符串,以 UTF-8 编码;
inout T,即相应类型值前加 &;
- 数组
[T]。
- 以变量指针为参数的函数可以接受以下值:
- 变量指针;
inout T;
inout [T]。
- 以自动释放指针为参数的函数可以接受以下值:
- 自动释放指针;
inout T,不过传递的指针指向一个回写临时缓冲区。
枚举
- Swift 会将所有用
NS_ENUM 宏标记的 C 枚举导入为 Swift 枚举,NS_OPTION 导入为遵循 OptionSet 的结构体和一系列类型属性,而没有用宏标记的 C 枚举则导入为遵循 RawRepresentable 的结构体和一系列全局变量。
- Xcode 10.0 引入了相仿的
NS_TYPED_ENUM 宏,Swift 会将一系列全局常量导入为遵循 RawRepresentable 的结构体和一系列类型属性,详见官方文档。
预处理指令
- 因为 Swift 编译器不包含预处理器,所以 Swift 没有预处理指令(preprocessor directives)。
- 不过 Swift 仍支持条件编译,编译条件包括字面值
true / false、条件编译标志(swift -D <#flag#>)和平台条件函数:
| 平台条件函数 |
有效参数 |
| os() |
macOS, iOS, watchOS, tvOS, Linux |
| arch() |
x86_64, arm, arm64, i386 |
| swift() |
>=x.x |
#if arch(arm) || arch(arm64)
print("Using ARM code")
#elseif arch(x86_64)
print("Using 64-bit x86 code")
#else
print("Using general code")
#endif
- 顺便一提,因为没有宏系统来支持元编程,Swift 团队维护了一个名叫 GYB(Generate Your Boilerplate)的轻量级模板工具,详见 NSHipster 的文章。
Swift / Objective-C 混编
应用内混编
| |
导入到 Swift |
导入到 Objective-C |
| Swift 代码 |
不需要导入语句 |
#import "ProductModuleName-Swift.h" |
| Objective-C 代码 |
不需要导入语句,但需要配置桥接头文件 |
#import "Header.h" |
- 将 Swift 代码导入 Objective-C,只需
#import Xcode 为 Swift 自动生成的头文件,它声明了所有 Swift 中定义的公开接口,如果已经配置了桥接头文件则亦会声明所有内部接口。
- 将 Objective-C 代码导入到 Swift,需要在配置好的 Objective-C 桥接头文件(文件名为
ProductModuleName-Bridging-Header.h)中 #import 相关头文件。
框架内混编
| |
导入到 Swift |
导入到 Objective-C |
| Swift 代码 |
不需要导入语句 |
#import <ProductName/ProductModuleName-Swift.h> |
| Objective-C 代码 |
不需要导入语句,但依赖框架的伞头文件 |
#import "Header.h" |
- 将 Swift 代码导入到 Objective-C,只需
#import Xcode 为 Swift 自动生成的头文件,它声明了所有 Swift 中定义的公开接口。
- 将 Objective-C 代码导入到 Swift,需要在框架的 Objective-C 伞头文件(文件名为
ProductModuleName.h)中 #import 相关头文件,当然它们也就成为了公开接口。
小提示
- 为避免循环引用,千万别把 Swift 导入到 Objective-C 头文件中,但可以用
@class MyClass; @protocol MyProtocol; 来前向声明 Swift 的类或协议。
- 上文多次提到的 product module name,默认与用户设定的 product name 相同,不过其中的非字母数字字符会被下划线替代。
@objc
- 将 Swift 导入到 Objective-C 之后,便可访问标为
@objc 或 @objc(<#name#>) 的 API。
- Swift 的独有特性不可以被标为
@objc:泛型、元组、非 Int 原始值的枚举、结构体、全局函数、全局变量、类型别名、可变参数、嵌套类型、柯里化的函数。
- 从 Swift 4(SE-0160)开始,继承自
NSObject 的类及其属性和方法不再自动标为 @objc,现在仍然自动标为 @objc 的情况只剩下几种确保语义一致性的情况:
- 该类继承自 Objective-C 中定义的类;
- 其声明重写了父类中的
@objc 声明;
- 其声明满足了一项
@objc 协议的要求;
- 已被标为
@IBAction / @IBOutlet / @IBDesignable / @IBInspectable / @GKInspectable / @NSManaged。
- 可以用
@objcMembers 让一个类本身、其扩展、其子类、其子类的扩展都被自动标为 @objc,另外也有 @nonobjc 显式取消隐式的 @objc。
- 在 Objective-C 中调用的 Swift API 必须支持动态派发(dynamic dispatch),但这并不意味着在 Swift 中编译器不会将
@objc 方法优化成静态派发。如果一定要使用 Objective-C 运行时中的 KVO、Method Swizzling 等动态特性,得用 @objc dynamic 来强制方法进行动态派发。
- Objective-C 的类不可以继承自 Swift 的类。
<Prev> Swift 学习笔记(二)