├── 01-Getting Started ├── Basic Setup.md └── newproject_2x.png ├── 02-Interoperability ├── 01-Interacting with Objective-C APIs.md ├── 02-Writing Swift Classes and Protocols with Objective-C Behavior.md ├── 03-Working with Cocoa Frameworks.md ├── 04-Adopting Cocoa Design Patterns.md ├── 05-Interacting with C APIs.md ├── Attributes Inspector@2x.png ├── Identity Inspector@2x.png ├── coredataeditor_2x.png └── coredatanamespace_2x.png ├── 03-Mix and Match ├── DAG_2x.png ├── Swift and Objective-C in the Same Project.md └── bridgingheader_2x.png ├── 04-Migration └── Migrating Your Objective-C Code to Swift.md └── README.md /01-Getting Started/Basic Setup.md: -------------------------------------------------------------------------------- 1 | # 基本设置 2 | 3 | - [搭建 Swift 环境](#setting_up_your_swift_environment) 4 | - [理解 Swift 导入过程](#understanding_the_swift_import_process) 5 | 6 | Swift 可无缝兼容 Cocoa 和 Objective-C。在 Swift,可以使用 Objective-C API,也可以在 Objective-C 中使用 Swift API。这种兼容性能让 Swift 作为一个简洁、易用、强大的工具集成到你的 Cocoa 应用开发流程中。 7 | 8 | 这篇指南包括了三个有关兼容性的重要方面,能够帮你更好地利用这种兼容性来开发 Cocoa 应用: 9 | 10 | - **交互** 可以将 Swift 和 Objective-C 相接合,在 Objective-C 中使用 Swift 类,并利用熟悉的 Cocoa 类、设计模式以及各种实践经验。 11 | - **混搭** 可以创建结合了 Swift 和 Objective-C 文件的混合语言应用,并且它们能跟彼此进行通信。 12 | - **迁移** 由于以上两点,从现有的 Objective-C 代码迁移到 Swift 会非常简单,这使运用最新的 Swift 特性取代你的 Objective-C 应用中的部分内容成为了可能。 13 | 14 | 在开始学习这些特性前,需要对如何搭建 Swift 环境来使用 Cocoa 系统框架有个大致了解。 15 | 16 | 17 | ## 搭建 Swift 环境 18 | 19 | 为了开始体验在 Swift 中访问 Cocoa 框架,使用 Xcode 的一个模板来创建一个基于 Swift 的应用。 20 | 21 | ##### 在 Xcode 中创建一个 Swift 项目 22 | 23 | 1.选择`File > New > Project > (iOS,watchOS,tvOS 或 OS X) > Application`,然后选择一个模板。 24 | 25 | 2.点击`Language`下拉菜单并选择 Swift。 26 | 27 | ![](newproject_2x.png) 28 | 29 | Swift 项目的结构几乎和 Objective-C 项目一模一样,只有一个重要的区别:Swift 没有头文件。在接口和实现之间没有显式划分,一个类中的所有信息都在一个单独的`.swift`文件中,详情请参阅 [在项目中同时使用 Swift 和 Objective-C](../03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md) 章节。 30 | 31 | 现在可以开始尝试在`AppDelegate`中编写 Swift 代码,或者可以选择`File > New > File > (iOS,watchOS,tvOS 或 OS X) > Source > Swift`来创建一个新的 Swift 文件。 32 | 33 | ### 要求 34 | 35 | 使用 Swift 3.0 创建应用需要使用 Xcode 8.0 或更高版本,而且使用的 SDK 有如下要求: 36 | 37 | 38 | 平台 | SDK 要求 39 | :---: | :---: 40 | macOS | 10.12 41 | iOS | 10.0 42 | watchOS | 3.0 43 | tvOS | 10.0 44 | 45 | Swift 编译器和 Xcode 强制要求最低部署版本为`iOS 7`或`macOS 10.9`,设置更早的部署版本会导致编译错误。 46 | 47 | > 注意 48 | > 由命令行构建的 Swift 可执行文件必须能在它的`@rpath`下找到 Swift 标准库,因此发布时需要同时包含 Swift 动态库。由 Xcode 构建的 Swift 可执行文件则自带静态链接的运行时库。 49 | 50 | 51 | ## 理解 Swift 导入过程 52 | 53 | 在你建立 Xcode 项目后,可以在 Swift 里导入任何 Cocoa 或者 Cocoa Touch 的框架。 54 | 55 | 任何 Objective-C 框架或 C 语言类库,都将作为一个`module`直接导入到 Swift 中。这包括了所有 Objective-C 系统框架——比如 Foundation、UIKit 和 SpriteKit,以及系统支持的 C 语言类库。例如,想导入 Foundation,只要简单地添加导入语句到 Swift 文件的顶部: 56 | 57 | ```swift 58 | import Foundation 59 | ``` 60 | 61 | 通过这个导入语句,Swift 文件现在可以访问所有 Foundation 中的类、协议、方法、属性以及常量。 62 | 63 | 导入过程非常简单。Objective-C 框架在头文件中公开 API。在 Swift,那些头文件被编译成 Objective-C 模块,接着被导入到 Swift 作为 Swift 的 API。导入过程决定了 Objective-C 代码中的函数、类、方法以及声明的类型如何在 Swift 中呈现。对于函数和方法,这个过程将影响它们的参数和返回值的类型。对于类型来说,导入过程会做以下处理: 64 | 65 | - 重映射 Objective-C 类型到 Swift 中的同等类型,例如`id`映射为`Any` 66 | - 重映射 Objective-C 核心类型到 Swift 中的替代类型,例如`NSString`映射为`String` 67 | - 重映射 Objective-C 概念到 Swift 中相对应的概念,例如指针映射为可选类型 68 | 69 | 想了解更多信息,请参阅 [与 Objective-C API 交互](../02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md) 章节。 70 | 71 | > 注意 72 | > C++ 代码无法直接导入 Swift,必须为其创建 Objective-C 或者 C 的封装。 73 | 74 | 导入 Swift 模块到 Objective-C 和上述过程非常相似。Swift 公开它的 API,例如来自系统框架的 API,作为 Swift 模块。随着这些 Swift 模块还会生成 Objective-C 头文件。这些头文件公开了那些可以映射回 Objective-C 的 API。一些 Swift API 无法映射回 Objective-C,因为它们使用了 Objective-C 中不存在的语言特性。 75 | 76 | 想了解更多信息,请参阅 [在项目中同时使用 Swift 和 Objective-C](../03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md) 章节。 77 | -------------------------------------------------------------------------------- /01-Getting Started/newproject_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/01-Getting Started/newproject_2x.png -------------------------------------------------------------------------------- /02-Interoperability/01-Interacting with Objective-C APIs.md: -------------------------------------------------------------------------------- 1 | # 与 Objective-C API 交互 2 | 3 | - [初始化](#initialization) 4 | - [类工厂方法和便利构造器](#class_factory_methods_and_convenience_initializers) 5 | - [可失败初始化](#failable_initialization) 6 | - [访问属性](#accessing_properties) 7 | - [方法](#working_with_methods) 8 | - [id 兼容性](#id_compatibility) 9 | - [将 Any 向下转换](#downcasting_any) 10 | - [动态方法查找](#dynamic_method_lookup) 11 | - [无法识别的选择器和可选链语法](#unrecognized_selectors_and_optional_chaining) 12 | - [为空性和可选类型](#nullability_and_optionals) 13 | - [将可选值桥接为 NSNull 实例](#bridging_optionals_to_nonnullable_objects) 14 | - [协议限定类](#protocol_qualified_classes) 15 | - [轻量泛型](#lightweight_generics) 16 | - [扩展](#extensions) 17 | - [闭包](#closures) 18 | - [在捕获 self 时避免强引用循环](#avoiding_strong_reference_cycles_when_capturing_self) 19 | - [对象比较](#object_comparison) 20 | - [哈希](#hashing) 21 | - [Swift 类型兼容性](#swift_type_compatibility) 22 | - [配置 Swift 在 Objective-C 中的接口](#configuring_swift_interfaces_in_objective_c) 23 | - [需要动态分派](#requiring_dynamic_dispatch) 24 | - [选择器](#selectors) 25 | - [Objective-C 方法的不安全调用](#unsafe_invocation_of_objective_c_methods) 26 | - [键和键路径](#keys_and_key_paths) 27 | 28 | **互用性**是能让 Swift 和 Objective-C 相接合的特性,这使你能够在一种语言编写的文件中使用另一种语言。当你准备开始把 Swift 融入到你的开发流程中时,学会如何利用互用性来重新定义、改善并提高你编写 Cocoa 应用的方法,不失为一个好主意。 29 | 30 | 互用性很重要的一点就是允许你在编写 Swift 代码时使用 Objective-C API。导入一个 Objective-C 框架后,可以使用原生的 Swift 语法实例化这些类并与之交互。 31 | 32 | 33 | ## 初始化 34 | 35 | 要在 Swift 中实例化一个 Objective-C 类,你应该使用 Swift 语法调用它的一个构造器。 36 | 37 | Objective-C 构造器以`init`开头,如果它接收参数,则会以`initWith`开头。Objective-C 构造器导入到 Swift 后,`init`前缀变为`init`关键字,以此表明该方法是 Swift 构造方法。如果构造器接收参数,`With`单词会被移除,方法名的其余部分则会被分配为相应的命名参数。 38 | 39 | 例如,思考如下 Objective-C 构造器声明: 40 | 41 | ```objective-c 42 | - (instancetype)init; 43 | - (instancetype)initWithFrame:(CGRect)frame 44 | style:(UITableViewStyle)style; 45 | ``` 46 | 47 | Swift 中等价的构造器声明如下所示: 48 | 49 | ```swift 50 | init() { /* ... */ } 51 | init(frame: CGRect, style: UITableViewStyle) { /* ... */ } 52 | ``` 53 | 54 | Objective-C 和 Swift 构造器语法的区别在实例化对象时将更为明显: 55 | 56 | 在 Objective-C 这样写: 57 | 58 | ```objective-c 59 | UITableView *myTableView = [[UITableView alloc] initWithFrame:.zero 60 | style:UITableViewStyleGrouped]; 61 | ``` 62 | 63 | 在 Swift 则应该这样写: 64 | 65 | ```swift 66 | let myTableView: UITableView = UITableView(frame: .zero, style: .grouped) 67 | ``` 68 | 69 | 注意,你不需要调用`alloc`,Swift 会为你处理。另外,调用任何 Swift 构造方法时都不会出现`init`单词。 70 | 71 | 你可以在赋值时显式地声明类型,也可以省略类型,Swift 能从构造器自动推断出类型。 72 | 73 | ```swift 74 | let myTextField = UITextField(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 40.0)) 75 | ``` 76 | 77 | 此处的`UITableView`和`UITextField`对象和你在 Objective-C 中实例化的一样。你可以用同样的方式使用它们,例如访问属性或者调用各自类中的方法。 78 | 79 | 80 | ### 类工厂方法和便利构造器 81 | 82 | 为了统一和简洁,Objective-C 中的类工厂方法被导入为 Swift 中的便利构造器,从而能使用同样简洁的构造器语法。 83 | 84 | 例如,在 Objective-C 像这样调用一个工厂方法: 85 | 86 | ```objective-c 87 | UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0]; 88 | ``` 89 | 90 | 在 Swift 应该这样写: 91 | 92 | ```swift 93 | let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0) 94 | ``` 95 | 96 | 97 | ### 可失败初始化 98 | 99 | 在 Objective-C,构造器会直接返回它们初始化的对象。初始化失败时,为了告知调用者,Objective-C 构造器会返回`nil`。在 Swift,这种模式被内置到语言特性中,被称为`可失败初始化`。 100 | 101 | 许多系统框架中的 Objective-C 构造器在导入到 Swift 时会被检查初始化是否会失败。你可以在你的 Objective-C 类中,使用`为空性标注`来指明初始化是否会失败,请参阅 [为空性和可选类型](#nullability_and_optionals) 小节。如果初始化不会失败,这些 Objective-C 构造器会作为`init(...)`导入,而如果初始化会失败,则会作为`init?(...)`导入。在没有任何为空性标注的情况下,Objective-C 构造器会作为`init!(...)`导入。 102 | 103 | 例如,当指定路径的图片文件不存在时,使用`UIImage(contentsOfFile:)`构造器初始化`UIImage`对象便会失败。你可以用可选绑定语法对这种构造器的可选类型的返回值进行解包,从而判断初始化是否成功。 104 | 105 | ```swift 106 | if let image = UIImage(contentsOfFile: "MyImage.png") { 107 | // 加载图片成功 108 | } else { 109 | // 无法加载图片 110 | } 111 | ``` 112 | 113 | 114 | ## 访问属性 115 | 116 | 在 Objective-C 中使用`@property`语法声明的属性会以如下方式导入到 Swift: 117 | 118 | - 具有为空性属性特性的属性(`nonnull`,`nullable`,`null_resettable`),作为可选类型或者非可选类型的属性导入。请参阅 [为空性和可选类型](#nullability_and_optionals) 小节。 119 | - 具有`readonly`属性特性的属性,作为只读计算型属性(`{ get }`)导入。 120 | - 具有`weak`属性特性的属性,作为标记`weak`关键字(`weak var`)的属性导入。 121 | - 具有`assign`,`copy`,`strong`,`unsafe_unretained`属性特性的属性,导入后会具有相应的内存管理策略。 122 | - 具有`class`属性特性的属性,作为类型属性导入。 123 | - 原子属性特性(`atomic`,`nonatomic`)在 Swift 属性声明中并无相应体现,不过 Objective-C 中具有原子性的属性在 Swift 中将保持其原子性。 124 | - 存取器属性特性(`getter=`,`setter=`)会被 Swift 忽略。 125 | 126 | 在 Swift 中使用点语法对属性进行存取,直接使用属性名即可,不需要附加圆括号。 127 | 128 | 例如,如下代码设置了`UITextField`对象的`textColor`和`text`属性: 129 | 130 | ```swift 131 | myTextField.textColor = UIColor.darkGray 132 | myTextField.text = "Hello world" 133 | ``` 134 | 135 | 在 Objective-C,一个有返回值的无参数方法可以像属性那样使用点语法调用。然而,它们会被导入为 Swift 中的实例方法,只有在 Objective-C 中使用`@property`声明的属性才会导入为 Swift 中的属性。方法的导入和调用请参阅 [方法](#working_with_methods) 小节。 136 | 137 | 138 | ## 方法 139 | 140 | 在 Swift 中使用点语法调用方法。 141 | 142 | 当 Objective-C 方法导入到 Swift 时,Objective-C 选择器的第一部分将会成为基本方法名并出现在圆括号的前面。第一个参数将直接在圆括号中出现,并且没有参数名。选择器的其余部分作为相应的参数名,出现在方法的圆括号内。选择器的所有部分在调用时都必须写上。 143 | 144 | 例如,在 Objective-C 这样写: 145 | 146 | ```objective-c 147 | [myTableView insertSubview:mySubview atIndex:2]; 148 | ``` 149 | 150 | 在 Swift 则应该这样写: 151 | 152 | ```swift 153 | myTableView.insertSubview(mySubview, atIndex: 2) 154 | ``` 155 | 156 | 如果调用一个无参数的方法,仍须在方法名后面跟上一对圆括号: 157 | 158 | ```swift 159 | myTableView.layoutIfNeeded() 160 | ``` 161 | 162 | 163 | ## id 兼容性 164 | 165 | Objective-C 中的`id`类型会作为 Swift 中的`Any`类型导入。无论在编译期还是运行期,当 Swift 值或者对象作为`id`参数传入 Objective-C 时,编译器都会引入一种通用的桥接转换操作。当`id`类型作为`Any`类型导入到 Swift 时,运行时会自动处理桥接回相应的 Swift 类类型或值类型的工作。 166 | 167 | ```swift 168 | var x: Any = "hello" as String 169 | x as? String // 值为 "hello" 的 String 170 | x as? NSString // 值为 "hello" 的 NSString 171 | 172 | x = "goodbye" as NSString 173 | x as? String // 值为 "goodbye" 的 String 174 | x as? NSString // 值为 "goodbye" 的 NSString 175 | ``` 176 | 177 | 178 | ### 将 Any 向下转换 179 | 180 | 使用一个`Any`类型的对象时,如果其具体类型已知或者可以断定,将其向下转换到具体类型往往会更有用处。然而,由于`Any`类型可以引用任何类型,因此无法保证向下转换到具体类型一定成功。 181 | 182 | 可以使用条件转换操作符(`as?`),这将返回目标类型的可选值: 183 | 184 | ```swift 185 | let userDefaults = UserDefaults.standard 186 | let lastRefreshDate = userDefaults.object(forKey: "LastRefreshDate") // lastRefreshDate 类型为 Any? 187 | if let date = lastRefreshDate as? Date { 188 | print("\(date.timeIntervalSinceReferenceDate)") 189 | } 190 | ``` 191 | 192 | 如果确信对象的类型,可以使用强制下转操作符(`as!`): 193 | 194 | ```swift 195 | let myDate = lastRefreshDate as! Date 196 | let timeInterval = myDate.timeIntervalSinceReferenceDate 197 | ``` 198 | 199 | 然而,一旦强制下转失败,将触发一个运行时错误: 200 | 201 | ```swift 202 | let myDate = lastRefreshDate as! String // Error 203 | ``` 204 | 205 | 206 | ### 动态方法查找 207 | 208 | Swift 还引入了`AnyObject`类型来表示一种对象类型,这种对象具有能够动态查找`@objc`方法的能力。这使你在访问返回值为`id`类型的 Objective-C 方法时能保持无类型的灵活性。 209 | 210 | 例如,你可以将任意类型的对象赋值给一个`AnyObject`类型的常量或者变量,如果是变量,你还可以再用其他类型的对象为其赋值。你还可以在`AnyObject`类型的值上调用任意 Objective-C 方法以及访问属性,而无需将其转换为具体类型。 211 | 212 | ```swift 213 | var myObject: AnyObject = UITableViewCell() 214 | myObject = NSDate() 215 | let futureDate = myObject.addingTimeInterval(10) 216 | let timeSinceNow = myObject.timeIntervalSinceNow 217 | ``` 218 | 219 | 220 | ### 无法识别的选择器和可选链语法 221 | 222 | 因为`AnyObject`类型的值在运行时才能确定其真实类型,所以有可能在不经意间写出不安全代码。在 Swift,和 Objective-C 一样,试图调用不存在的方法将触发未识别的选择器错误。 223 | 224 | 例如,如下代码在编译时没有任何编译警告,但是会在运行时触发一个错误: 225 | 226 | ```swift 227 | myObject.character(at: 5) 228 | // 崩溃,myObject 无法响应该方法 229 | ``` 230 | 231 | Swift 使用可选来防止这种不安全的行为。当用`AnyObject`类型的值调用一个 Objective-C 方法时,方法调用在行为上类似于隐式解包可选。可以像调用协议中的可选方法那样,使用可选链语法在`AnyObject`上调用方法。 232 | 233 | 例如,在下面的代码中,第一和第二行代码不会被执行,因为`count`属性和`character(at:)`方法不存在于`NSDate`对象中。`myCount`常量会被推断为`Int?`,并被赋值为`nil`。你也可以使用`if-let`语句来有条件地解包这种可能无法被响应的方法的返回值,就像第三行所做的一样。 234 | 235 | ```swift 236 | // myObject 类型为 AnyObject,保存着 NSDate 类型的值 237 | let myCount = myObject.count // myCount 类型为 Int?,其值为 nil 238 | let myChar = myObject.character?(at: 5) // myChar 类型为 unichar?,其值为 nil 239 | if let fifthCharacter = myObject.character?(at: 5) { 240 | print("Found \(fifthCharacter) at index 5") // 条件分支不会被执行 241 | } 242 | ``` 243 | 244 | > 注意 245 | > 尽管在使用`AnyObject`类型的值调用方法时不需要强制解包,但建议使用强制解包的方式来防止一些意想不到的行为。 246 | 247 | 248 | ## 为空性和可选类型 249 | 250 | 在 Objective-C,用于操作对象的原始指针的值可能会是`NULL`(在 Objective-C 中称为`nil`)。而在 Swift,所有的值,包括结构体与对象的引用,都能确保是非空值。作为替代,可以将可能会值缺失的值包装为该类型的`可选类型`。当你需要表示值缺失的情况时,你可以将其赋值为`nil`。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [可选类型](http://wiki.jikexueyuan.com/project/swift/chapter2/01_The_Basics.html#optionals) 小节获取更多关于可选类型的信息。 251 | 252 | 在 Objective-C 可以使用`为空性标注`来指明一个参数类型、属性类型或者返回值类型是否可以为`NULL`或`nil`值。单个类型声明可以使用`_Nullable`和`_Nonnull`标注,单个属性声明可以使用`nullable`,`nonnull`,`null_resettable`属性特性,批量标注可以使用`NS_ASSUME_NONNULL_BEGIN`和`NS_ASSUME_NONNULL_END`宏。如果一个类型没有任何为空性标注,Swift 就无法辨别它是可选类型还是非可选类型,因此将其作为隐式解包可选类型导入。 253 | 254 | - 以`_Nonnull`或者范围宏标注的类型,会作为`非可选类型`导入到 Swift。 255 | - 以`_Nullable`标注的类型,会作为`可选类型`导入到 Swift。 256 | - 没有为空性标注的类型,会作为`隐式解包可选类型`导入到 Swift。 257 | 258 | 举个例子,思考如下 Objective-C 声明: 259 | 260 | ```objective-c 261 | @property (nullable) id nullableProperty; 262 | @property (nonnull) id nonNullProperty; 263 | @property id unannotatedProperty; 264 | 265 | NS_ASSUME_NONNULL_BEGIN 266 | - (id)returnsNonNullValue; 267 | - (void)takesNonNullParameter:(id)value; 268 | NS_ASSUME_NONNULL_END 269 | 270 | - (nullable id)returnsNullableValue; 271 | - (void)takesNullableParameter:(nullable id)value; 272 | 273 | - (id)returnsUnannotatedValue; 274 | - (void)takesUnannotatedParameter:(id)value; 275 | ``` 276 | 277 | 导入到 Swift 后将如下所示: 278 | 279 | ```swift 280 | var nullableProperty: Any? 281 | var nonNullProperty: Any 282 | var unannotatedProperty: Any! 283 | 284 | func returnsNonNullValue() -> Any 285 | func takesNonNullParameter(value: Any) 286 | 287 | func returnsNullableValue() -> Any? 288 | func takesNullableParameter(value: Any?) 289 | 290 | func returnsUnannotatedValue() -> Any! 291 | func takesUnannotatedParameter(value: Any!) 292 | ``` 293 | 294 | 大多数 Objective-C 系统框架,包括 Foundation,都已经提供了为空性标注,这使你能以更加类型安全的方式去操作各种值。 295 | 296 | 297 | ### 将可选值桥接为 NSNull 实例 298 | 299 | Swift 会根据可选值是否有值来决定是否将其桥接为 Objective-C 的`NSNull`实例。如果可选值的值为`nil`,Swift 会将`nil`值桥接为`NSNull`实例。否则,Swift 会将可选值桥接为它所包装的值。例如,当一个可选值传给一个接受非空`id`类型参数的 Objective-C API 时,或者将一个元素为可选类型的数组(`[T?]`)桥接为`NSArray`时。 300 | 301 | 如下代码展示了`String?`实例如何根据它是否有值来桥接到 Objective-C。 302 | 303 | ```objective-c 304 | @implementation OptionalBridging 305 | + (void)logSomeValue:(nonnull id)valueFromSwift { 306 | if ([valueFromSwift isKindOfClass: [NSNull class]]) { 307 | os_log(OS_LOG_DEFAULT, "Received an NSNull value."); 308 | } else { 309 | os_log(OS_LOG_DEFAULT, "%s", [valueFromSwift UTF8String]); 310 | } 311 | } 312 | @end 313 | ``` 314 | 315 | 因为`valueFromSwift`参数的类型是`id`,它会作为`Any`类型导入到 Swift。由于很少将可选值传递给`Any`类型,因此在将可选值传递给类方法`logSomeValue(_:)`时需要将参数显式转换为`Any`类型,从而消除编译警告。 316 | 317 | ```swift 318 | let someValue: String? = "Bridge me, please." 319 | let nilValue: String? = nil 320 | 321 | OptionalBridging.logSomeValue(someValue as Any) // Bridge me, please. 322 | OptionalBridging.logSomeValue(nilValue as Any) // Received an NSNull value. 323 | ``` 324 | 325 | 326 | ## 协议限定类 327 | 328 | Objective-C 中被一个或多个协议限定的类会作为协议组合类型导入到 Swift。例如,如下 Objective-C 属性引用一个视图控制器: 329 | 330 | ```objective-c 331 | @property UIViewController * myController; 332 | ``` 333 | 334 | 该属性会以如下形式导入到 Swift: 335 | 336 | ```swift 337 | var myController: UIViewController & UITableViewDataSource & UITableViewDelegate 338 | ``` 339 | 340 | Objective-C 中的协议限定元类会作为协议元类导入到 Swift。例如,如下 Objective-C 方法会在指定类上执行一个操作: 341 | 342 | ```objective-c 343 | - (void)doSomethingForClass:(Class)codingClass; 344 | ``` 345 | 346 | 如下代码展示了 Swift 如何导入它: 347 | 348 | ```swift 349 | func doSomething(for codingClass: NSCoding.Type) 350 | ``` 351 | 352 | 353 | ## 轻量泛型 354 | 355 | Objective-C 的`NSArray`,`NSSet`以及`NSDictionary`类型的声明在使用轻量泛型参数化后,被导入到 Swift 时会附带容器中元素的类型信息。 356 | 357 | 例如,思考如下 Objective-C 属性声明: 358 | 359 | ```objective-c 360 | @property NSArray *dates; 361 | @property NSCache> *cachedData; 362 | @property NSDictionary > *supportedLocales; 363 | ``` 364 | 365 | 导入到 Swift 后将如下所示: 366 | 367 | ```swift 368 | var dates: [Date] 369 | var cachedData: NSCache 370 | var supportedLocales: [String: [Locale]] 371 | ``` 372 | 373 | 使用了轻量泛型的 Objective-C 类会作为泛型类导入到 Swift。所有 Objective-C 泛型类型参数导入到 Swift 后,都会带有类型约束来要求该类型必须是一个类类型。如果 Objective-C 泛型参数化带有类限定,导入到 Swift 后,它会带有一个类型约束来要求该类必须是指定类的子类。与之类似,如果带有协议限定,导入到 Swift 后,它会带有一个类型约束来要求该类必须符合指定协议。如果未特别指定类型限定,Swift 会将根据泛型类的类型约束来推断泛型参数化。例如,思考如下 Objective-C 类声明和分类声明: 374 | 375 | ```objective-c 376 | @interface List> : NSObject 377 | - (List *)listByAppendingItemsInList:(List *)otherList; 378 | @end 379 | 380 | @interface ListContainer : NSObject 381 | - (List *)listOfValues; 382 | @end 383 | 384 | @interface ListContainer (ObjectList) 385 | - (List *)listOfObjects; 386 | @end 387 | ``` 388 | 389 | 上述声明将以如下形式导入到 swift: 390 | 391 | ```swift 392 | class List : NSObject { 393 | func listByAppendingItemsInList(otherList: List) -> List 394 | } 395 | 396 | class ListContainer : NSObject { 397 | func listOfValues() -> List 398 | } 399 | 400 | extension ListContainer { 401 | func listOfObjects() -> List 402 | } 403 | ``` 404 | 405 | 406 | ## 扩展 407 | 408 | Swift 扩展和 Objective-C 分类相似。扩展可以为既有类,结构和枚举扩充功能,即使它们是在 Objective-C 中定义的。通过导入相应的模块,还可以为系统框架中的类型或者自定义类型定义扩展。 409 | 410 | 例如,可以扩展`UIBezierPath`类,基于三角形的边长与起点来创建一个简单的三角形路径。 411 | 412 | ```swift 413 | extension UIBezierPath { 414 | convenience init(triangleSideLength: CGFloat, origin: CGPoint) { 415 | self.init() 416 | let squareRoot = CGFloat(sqrt(3.0)) 417 | let altitude = (squareRoot * triangleSideLength) / 2 418 | move(to: origin) 419 | addLine(to: CGPoint(x: origin.x + triangleSideLength, y: origin.y)) 420 | addLine(to: CGPoint(x: origin.x + triangleSideLength / 2, y: origin.y + altitude)) 421 | close() 422 | } 423 | } 424 | ``` 425 | 426 | 还可以利用扩展来增加属性(包括类型属性和静态属性)。然而,这些属性必须是计算型属性,因为扩展不能为类,结构体,枚举添加存储型属性。 427 | 428 | 下面这个例子为`CGRect`结构增加了一个叫`area`的计算型属性: 429 | 430 | ```swift 431 | extension CGRect { 432 | var area: CGFloat { 433 | return width * height 434 | } 435 | } 436 | let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0) 437 | let area = rect.area 438 | ``` 439 | 440 | 甚至可以利用扩展为类添加协议一致性而无需子类化。如果协议是在 Swift 中定义的,还可以为结构或是枚举添加协议一致性,即使它们是在 Objective-C 中定义的。 441 | 442 | 无法通过扩展覆盖类型中既有的方法与属性。 443 | 444 | 445 | ## 闭包 446 | 447 | Objective-C 的块会作为符合其调用约定的 Swift 闭包导入,通过`@convention(block)`特性表示。例如,下面是一个 Objective-C 的块变量: 448 | 449 | ```objective-c 450 | void (^completionBlock)(NSData *) = ^(NSData *data) { 451 | // ... 452 | } 453 | ``` 454 | 455 | 它在 Swift 中如下所示: 456 | 457 | ```swift 458 | let completionBlock: (Data) -> Void = { data in 459 | // ... 460 | } 461 | ``` 462 | 463 | Swift 闭包与 Objective-C 块兼容,因此可以把一个 Swift 闭包传递给一个接受块作为参数的 Objective-C 方法。而且因为 Swift 闭包与函数具有相同类型,因此甚至可以直接传递一个 Swift 函数的函数名。 464 | 465 | 闭包与块有相似的捕获语义,但有个关键的不同:被捕获的变量可以直接修改,而不是一份值拷贝。换言之,Objective-C `__block`变量的行为是 Swift 变量的默认行为。 466 | 467 | 468 | ### 在捕获 self 时避免强引用循环 469 | 470 | 在 Objective-C,如果块捕获了`self`,一定要慎重考虑内存管理问题。 471 | 472 | 块会保持对被捕获对象的强引用,包括`self`。一旦`self`也持有对块的强引用,例如通过一个拷贝语义的块属性,就将导致强引用循环。可以通过让块捕获`self`的弱引用来避免此问题: 473 | 474 | ```objective-c 475 | __weak typeof(self) weakSelf = self; 476 | self.block = ^{ 477 | __strong typeof(self) strongSelf = weakSelf; 478 | [strongSelf doSomething]; 479 | }; 480 | ``` 481 | 482 | 和 Objective-C 块类似,Swift 闭包也会保持对被捕获对象的强引用,包括`self`。为了避免强引用循环,可以在闭包的捕获列表中指定`self`为`unowned`: 483 | 484 | ```swift 485 | self.closure = { [unowned self] in 486 | self.doSomething() 487 | } 488 | ``` 489 | 490 | 请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [解决闭包引起的循环强引用](http://wiki.jikexueyuan.com/project/swift/chapter2/16_Automatic_Reference_Counting.html#resolving_strong_reference_cycles_for_closures) 小节获取更多信息。 491 | 492 | 493 | ## 对象比较 494 | 495 | 在 Swift,比较两个对象可以使用两种方式。第一种,使用`==`运算符,比较两个对象内容是否相同。第二种,使用`===`运算符,判断常量或者变量是否引用同一对象。 496 | 497 | Swift 为源自`NSObject`的类实现了`Equatable`协议,并提供了`==`运算符和`===`运算符的默认实现。`==`运算符的默认实现会调用`isEqual:`方法,`===`运算符的默认实现则会检查指针是否相同。你不应该为 Objective-C 类型重写这两个运算符。 498 | 499 | `NSObject`类为`isEqual:`方法提供的基本实现仅仅是比较两个指针是否相同。可以根据需要在子类中重写`isEqual:`方法,基于对象内容进行比较,而不是比较指针。关于如何实现对象比较逻辑的更多信息,请参阅 [Object comparison](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/DevPedia-CocoaCore/ObjectComparison.html#//apple_ref/doc/uid/TP40008195-CH37)。 500 | 501 | > 注意 502 | > Swift 会自动为`!=`和`!==`运算符提供实现,无需再进行重写。 503 | 504 | 505 | ### 哈希 506 | 507 | 如果`NSDictionary`的键类型不带有类限定,Swift 会将其导入为键类型为`AnyHashable`的`Dictionary`。与之类似,如果`NSSet`中的元素没有类型限定,Swift 会将其导入为元素类型为`AnyHashable`的`Set`。如果`NSDictionary`或`NSSet`对其键或元素的类型做了参数化,那么 Swift 导入它们时则会使用相应的类型作为键或元素的类型。例如,考虑如下 Objective-C 声明: 508 | 509 | ```objective-c 510 | @property NSDictionary *unqualifiedDictionary; 511 | @property NSDictionary *qualifiedDictionary; 512 | 513 | @property NSSet *unqualifiedSet; 514 | @property NSSet *qualifiedSet; 515 | ``` 516 | 517 | Swift 会以如下形式导入它们: 518 | 519 | ```swift 520 | var unqualifiedDictionary: [AnyHashable: Any] 521 | var qualifiedDictionary: [String: Date] 522 | 523 | var unqualifiedSet: Set 524 | var qualifiedSet: Set 525 | ``` 526 | 527 | Swift 在导入 Objective-C 中未指定的类型或`id`类型时,如果这种类型必须符合`Hashable`协议,就会将之导入为`AnyHashable`类型。`AnyHashable`由任意`Hashable`类型隐式转换而来,并可以使用`as?`和`as!`操作符将其转换为更具体的类型。 528 | 529 | 534 | 535 | 536 | ## Swift 类型兼容性 537 | 538 | 在 Swift 中继承 Objective-C 类时,该 Swift 子类及其成员,即属性、方法、下标和构造器,只要兼容于 Objective-C,就可在 Objective-C 中直接使用。但这不包括一些 Swift 独有特性,如下列所示: 539 | 540 | - 泛型 541 | - 元组 542 | - 原始值类型不是`Int`类型的枚举 543 | - 结构体 544 | - 顶级函数 545 | - 全局变量 546 | - 类型别名 547 | - 可变参数 548 | - 嵌套类型 549 | - 柯里化函数 550 | 551 | Swift API 转换到 Objective-C 时: 552 | 553 | - Swift 可选类型会被标注`__nullable`。 554 | - Swift 非可选类型会被标注`__nonnull`。 555 | - Swift 常量存储属性和计算属性会成为 Objective-C 只读属性。 556 | - Swift 变量存储属性会成为 Objective-C 读写属性。 557 | - Swift 类型属性会成为 Objective-C 中具有`class`特性的属性。 558 | - Swift 类型方法会成为 Objective-C 类方法。 559 | - Swift 构造器和实例方法会成为 Objective-C 实例方法。 560 | - Swift 中抛出错误的方法会成为接受`NSError **`参数的 Objective-C 方法。如果 Swift 方法没有参数,`AndReturnError:`会被拼接到 Objective-C 方法名上,否则,`error:`会被拼接在方法名上。如果 Swift 方法没有指定返回类型,那么相应的 Objective-C 方法会拥有 `BOOL` 类型的返回值。如果 Swift 方法返回非可选值,那么对应的 Objective-C 方法会返回可选值。 561 | 562 | 例如,思考如下 Swift 声明: 563 | 564 | ```swift 565 | class Jukebox: NSObject { 566 | var library: Set 567 | 568 | var nowPlaying: String? 569 | 570 | var isCurrentlyPlaying: Bool { 571 | return nowPlaying != nil 572 | } 573 | 574 | class var favoritesPlaylist: [String] { 575 | // return an array of song names 576 | } 577 | 578 | init(songs: String...) { 579 | self.library = Set(songs) 580 | } 581 | 582 | func playSong(named name: String) throws { 583 | // play song or throw an error if unavailable 584 | } 585 | } 586 | ``` 587 | 588 | 上述声明导入到 Objective-C 后如下所示: 589 | 590 | ```objective-c 591 | @interface Jukebox : NSObject 592 | @property (nonatomic, strong, nonnull) NSSet *library; 593 | @property (nonatomic, copy, nullable) NSString *nowPlaying; 594 | @property (nonatomic, readonly, getter=isCurrentlyPlaying) BOOL currentlyPlaying; 595 | @property (nonatomic, class, readonly, nonnull) NSArray * favoritesPlaylist; 596 | - (nonnull instancetype)initWithSongs:(NSArray * __nonnull)songs OBJC_DESIGNATED_INITIALIZER; 597 | - (BOOL)playSong:(NSString * __nonnull)name error:(NSError * __nullable * __null_unspecified)error; 598 | @end 599 | ``` 600 | 601 | > 注意 602 | > 无法在 Objective-C 中继承一个 Swift 类。 603 | 604 | 605 | ### 配置 Swift 在 Objective-C 中的接口 606 | 607 | 在某些情况下,需要更精确地控制如何将 Swift API 暴露给 Objective-C。你可以使用`@objc(name)`特性来改变类、属性、方法、枚举类型以及枚举用例暴露给 Objective-C 代码的声明。 608 | 609 | 例如,如果 Swift 类的类名包含 Objective-C 中不支持的字符,就可以为其提供一个在 Objective-C 中的别名。如果要为 Swift 函数提供 Objective-C 别名,使用 Objective-C 选择器语法,并记得在选择器参数片段后添加冒号(`:`)。 610 | 611 | ```swift 612 | @objc(Color) 613 | enum Цвет: Int { 614 | 615 | @objc(Red) 616 | case Красный 617 | 618 | @objc(Black) 619 | case Черный 620 | } 621 | 622 | @objc(Squirrel) 623 | class Белка: NSObject { 624 | 625 | @objc(color) 626 | var цвет: Цвет = .Красный 627 | 628 | @objc(initWithName:) 629 | init (имя: String) { 630 | // ... 631 | } 632 | 633 | @objc(hideNuts:inTree:) 634 | func прячьОрехи(количество: Int, вДереве дерево: Дерево) { 635 | // ... 636 | } 637 | } 638 | ``` 639 | 640 | 对 Swift 类使用`@objc(name)`特性时,该类将在 Objective-C 中可用并且没有任何命名空间。因此,这个特性在迁徙被归档的 Objecive-C 类到 Swift 时会非常有用。由于归档文件中存储了被归档对象的类名,因此应该使用`@objc(name)`特性来指定被归档的 Objective-C 对象的类名,这样归档文件才能通过新的 Swift 类解档。 641 | 642 | > 注意 643 | > 相反,Swift 还提供了`@nonobjc`特性,可以让一个 Swift 声明在 Objective-C 中不可用。可以利用它来解决桥接方法循环,以及重载由 Objective-C 中导入的类中的方法。另外,如果一个 Objective-C 方法在 Swift 中被重写后,无法再以 Objective-C 的语言特性呈现,例如将参数变为了 Swift 中的可变参数,那么这个方法必须标记为`@nonobjc`。 644 | 645 | 646 | ### 需要动态分派 647 | 648 | 可在 Objective-C 中调用的 Swift API 必须通过动态分派提供。不过,在 Swift 中调用这些 API 时,动态分派的可用性并不妨碍 Swift 编译器选择更高效的分派方法。 649 | 650 | 你可以使用 `@objc` 特性和 `dynamic` 修饰符来要求通过 Objective-C 运行时动态分派对成员的访问操作。一般很少需要这种动态分派,但是,在使用类似键值监听(KVO)这类 API,以及使用 Objective-C 运行时库中的 `method_exchangeImplementations` 这类需要在运行时动态替换方法实现的函数时,则必须使用动态分派。 651 | 652 | 使用 `dynamic` 修饰符标记的声明还必须显式标记 `@objc` 特性,除非声明所在的上下文为其隐式添加了 `@objc` 特性。关于 `@objc` 特性何时会被隐式添加的相关信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift) 中的[声明特性](http://wiki.jikexueyuan.com/project/swift/chapter3/06_Attributes.html#declaration_attributes)部分。 653 | 654 | 655 | ## 选择器 656 | 657 | Objective-C 选择器是一种用于引用 Objective-C 方法名的类型。在 Swift,Objective-C 选择器用`Selector`结构体表示。可以使用`#selector`表达式创建一个选择器,将方法名传入即可,例如`#selector(MyViewController.tappedButton(sender:))`。创建 Objective-C 存取方法的选择器时,为了区分,将属性名加上`getter:`或`setter:`标签,例如`#selector(getter: MyViewController.myButton)`。 658 | 659 | ```swift 660 | import UIKit 661 | class MyViewController: UIViewController { 662 | let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) 663 | 664 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 665 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 666 | let action = #selector(MyViewController.tappedButton) 667 | myButton.addTarget(self, action: action, for: .touchUpInside) 668 | } 669 | 670 | func tappedButton(sender: UIButton?) { 671 | print("tapped button") 672 | } 673 | 674 | required init?(coder: NSCoder) { 675 | super.init(coder: coder) 676 | } 677 | } 678 | ``` 679 | 680 | > 注意 681 | > Objective-C 方法引用还可以加上圆括号,并使用`as`运算符来消除重载方法之间的歧义,例如`#selector(((UIView.insertSubview(_:at:)) as (UIView) -> (UIView, Int) -> Void))`。 682 | 683 | 684 | ### Objective-C 方法的不安全调用 685 | 686 | 可以使用`perform(_:)`方法以及它的变体在兼容 Objective-C 的对象上调用方法。使用选择器调用方法并非内在安全的,因为编译器无法保证结果,甚至无法保证对象能否响应选择器。因此,坚决不提倡使用这些 API,除非代码确实依赖于 Objective-C 运行时提供的动态方法决议。例如,实现一个类似`NSResponder`这种在接口中使用了目标-行为设计模式的类。绝大多数情况下,将对象转换为`AnyObject`类型,再使用可选链语法调用方法会更为安全和方便,请参阅 [id 兼容性](#id_compatibility) 小节获取更多信息。 687 | 688 | `perform(_:)`方法同步执行,并返回隐式解包可选类型的非托管对象(`Unmanaged!`),这是因为通过执行选择器返回的值的类型和所有权无法在编译期决定。请参阅 [非托管对象](03-Working%20with%20Cocoa%20Data%20Types.md#%E9%9D%9E%E6%89%98%E7%AE%A1%E5%AF%B9%E8%B1%A1) 小节获取更多信息。与此相反,该方法的一些变体在指定线程执行选择器,或者延迟执行,例如`perform(_:on:with:waitUntilDone:modes:)`和`perform(_:with:afterDelay:)`,它们没有返回值。 689 | 690 | ```swift 691 | let string: NSString = "Hello, Cocoa!" 692 | let selector = #selector(NSString.lowercased(with:)) 693 | let locale = Locale.current 694 | if let result = string.perform(selector, with: locale) { 695 | print(result.takeUnretainedValue()) 696 | } 697 | // 打印输出 "hello, cocoa!" 698 | ``` 699 | 700 | 向一个对象发送无法识别的选择器将造成接收者调用`doesNotRecognizeSelector(_:)`方法,其默认实现是抛出`NSInvalidArgumentException`异常。 701 | 702 | ```swift 703 | let array: NSArray = ["delta", "alpha", "zulu"] 704 | // 下面这句代码不会导致编译错误,因为该选择器存在于 NSDictionary 中 705 | let selector = #selector(NSDictionary.allKeysForObject) 706 | // 下面这句代码将抛出异常,因为 NSArray 无法响应该选择器 707 | array.performSelector(selector) 708 | ``` 709 | 710 | 711 | ## 键和键路径 712 | 713 | 在 Objective-C,键是一个表示某个对象特定属性的字符串。键路径是一个由多个键构成的字符串,键之间用点分隔,表示一条能访问到某个对象特定属性的路径。键和键路径经常用于*键值编码*(KVC),这是一种通过字符串来访问某个对象的属性的机制。键和键路径也常用于*键值监听*(KVO),这是一种能让某个对象在另一个对象的特定属性改变时获得通知的机制。 714 | 715 | 在 Swift,可以用 key-path 表达式创建键路径来访问属性。例如,可以用 `\Animal.name` 这样的 key-path 表达式来访问 `Animal` 类中的 `name` 属性。键路径通过 key-path 表达式创建,其中包含了属性所在类型的类型信息。对一个实例使用键路径所得到的结果就如同直接访问该实例的对应属性一样。key-path 表达式接受属性引用以及链式属性引用,例如 `\Animal.name.count`。 716 | 717 | ```swift 718 | let llama = Animal(name: "Llama") 719 | let nameAccessor = \Animal.name 720 | let nameCountAccessor = \Animal.name.count 721 | 722 | llama[keyPath: nameAccessor] 723 | // "Llama" 724 | llama[keyPath: nameCountAccessor] 725 | // "5" 726 | ``` 727 | 728 | 在 Swift,还可以用 `#keyPath` 字符串表达式生成可被编译器检查的键和键路径,并可将之用于 KVC 方法 `value(forKey:)` 和  `value(forKeyPath:)`,以及 KVO 方法 `addObserver(_:forKeyPath:options:context:)`。`#keyPath` 字符串表达式接受链式的方法或属性引用。它还支持可选值链式引用,例如 `#keyPath(Person.bestFriend.name)`。与使用 key-path 表达式这种创建方式不同的是,`#keyPath` 字符串表达式不会将属性或方法所属类型的类型信息传递给接受键路径的 API。 729 | 730 | > 注意 731 | > `#keyPath` 表达式语法类似 `#selector` 表达式语法,请参阅 [选择器](#selectors)。 732 | 733 | ```swift 734 | class Person: NSObject { 735 | @objc var name: String 736 | @objc var friends: [Person] = [] 737 | @objc var bestFriend: Person? = nil 738 | 739 | init(name: String) { 740 | self.name = name 741 | } 742 | } 743 | 744 | let gabrielle = Person(name: "Gabrielle") 745 | let jim = Person(name: "Jim") 746 | let yuanyuan = Person(name: "Yuanyuan") 747 | gabrielle.friends = [jim, yuanyuan] 748 | gabrielle.bestFriend = yuanyuan 749 | 750 | #keyPath(Person.name) 751 | // "name" 752 | gabrielle.value(forKey: #keyPath(Person.name)) 753 | // "Gabrielle" 754 | #keyPath(Person.bestFriend.name) 755 | // "bestFriend.name" 756 | gabrielle.value(forKeyPath: #keyPath(Person.bestFriend.name)) 757 | // "Yuanyuan" 758 | #keyPath(Person.friends.name) 759 | // "friends.name" 760 | gabrielle.value(forKeyPath: #keyPath(Person.friends.name)) 761 | // ["Yuanyuan", "Jim"] 762 | ``` 763 | -------------------------------------------------------------------------------- /02-Interoperability/02-Writing Swift Classes and Protocols with Objective-C Behavior.md: -------------------------------------------------------------------------------- 1 | # 使用 Objective-C 特性编写 Swift 类和协议 2 | 3 | - [继承 Objective-C 类](#inheriting_from_objective-c_classes) 4 | - [NSCoding 协议](#NSCoding) 5 | - [采用协议](#adopting_protocols) 6 | - [编写构造器和析构器](#writing_initializers_and_deinitializers) 7 | - [兼容使用 Swift 类名的 Objective-C API](#using_swift_class_names_with_objective_c_apis) 8 | - [与 Interface Builder 结合](#integrating_with_interface_builder) 9 | - [使用 Outlets 和 Actions](#working_with_outlets_and_actions) 10 | - [实时渲染](#live_rendering) 11 | - [指定属性特性](#specifying_property_attributes) 12 | - [强引用和弱引用](#strong_and_weak) 13 | - [读写和只读](#read_write_and_read_only) 14 | - [拷贝语义](#copy_semantics) 15 | - [实现 Core Data 的 NSManagedObject 子类](#implementing_core_data_managed_object_subclasses) 16 | - [声明协议](#declaring_protocols) 17 | 18 | **互用性**让你在编写 Swift 代码时可以融合 Objective-C 语言特性。在编写 Swift 代码时,不仅可以继承 Objective-C 类,声明和采用 Objective-C 协议,还可以使用 Objective-C 的一些其它功能。这意味着不但可以基于 Objective-C 中耳熟能详的既有特性来编写 Swift 代码,还可以利用 Swift 更为强大的现代化语言特性来改善代码。 19 | 20 | 21 | ## 继承 Objective-C 类 22 | 23 | 在 Swift,可以定义一个继承自 Objective-C 类的 Swift 子类。在 Swift 的类名后面加上一个冒号(`:`),冒号后面跟上 Objective-C 类的类名即可。 24 | 25 | ```swift 26 | import UIKit 27 | class MySwiftViewController: UIViewController { 28 | // 定义类 29 | } 30 | ``` 31 | 32 | Swift 子类可以从 Objective-C 父类中继承所有的功能。 33 | 34 | 如果要覆盖父类中的实现,可以使用 `override` 修饰符。编译器会根据 Swift 方法名来自动推断被重写的父类方法。也可以使用 `@objc(name)` 特性来明确指定相对应的 Objective-C 符号。 35 | 36 | 当 Swift 类引入了许多需要 Objective-C 运行时行为的新方法或属性时,请在该类的声明中使用 `@objcMembers` 特性。对类使用 `@objcMembers` 特性会隐式地将 `@objc` 特性添加到类中所有兼容 Objective-C 的成员。由于使用 `@objc` 特性会增加应用程序编译后的体积并对性能产生不利影响,因此只有在每个成员都需要使用 `@objc` 特性时才应在类声明中使用 `@objcMembers` 特性。 37 | 38 | 39 | ### NSCoding 协议 40 | 41 | `NSCoding`协议要求采用协议的类型实现其所要求的构造器`init(coder:)`以及其所要求的方法`encode(with:)`。直接采用`NSCoding`协议的类必须实现这个方法。对于采用`NSCoding`协议的类的子类,如果有一个或者多个自定义的构造器或者有不具有默认初始值的属性,那么也必须实现这个方法。Xcode 提供了下面这个`fix-it`来提供一个占位实现: 42 | 43 | ```swift 44 | required init(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | ``` 48 | 49 | 对于那些从 Storyboards 里加载的对象,或者用`NSUserDefaults`或`NSKeyedArchiver`归档到硬盘的对象,则必须提供该构造器的完整实现。当然,如果一个类型不会以此种方式实例化时,就不需要提供该构造器的完整实现。 50 | 51 | 52 | ## 采用协议 53 | 54 | Objective-C 协议会被导入为 Swift 协议。所有协议都写在一个用逗号分隔的列表中,跟在父类类名后面(如果该类有父类的话)。 55 | 56 | ```swift 57 | class MySwiftViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { 58 | // 定义类 59 | } 60 | ``` 61 | 62 | 声明符合单个协议的类型时,直接使用协议名作为其类型,类似于 Objective-C 中`id`这种形式。声明符合多个协议的类型时,使用`SomeProtocol & AnotherProtocol`这种协议组合的形式,类似于 Objective-C 中`id`这种形式。 63 | 64 | ```swift 65 | var textFieldDelegate: UITextFieldDelegate 66 | var tableViewController: UITableViewDataSource & UITableViewDelegate 67 | ``` 68 | 69 | > 注意 70 | > 在 Swift,类和协议的命名空间是统一的,因此 Objective-C 的`NSObject`协议会被重映射为 Swift 的`NSObjectProtocol`协议。 71 | 72 | 73 | ## 编写构造器和析构器 74 | 75 | Swift 编译器能确保构造器不会遗留任何未初始化的属性,从而增加代码的安全性和可预测性。另外,与 Objective-C 不同,Swift 不提供单独的内存分配方法。你会始终使用原生的 Swift 构造器,即使是使用 Objective-C 类,Swift 会将 Objective-C 构造方法转换为 Swift 构造器。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [构造器](http://wiki.jikexueyuan.com/project/swift/chapter2/14_Initialization.html) 章节了解关于如何实现构造器的更多信息。 76 | 77 | 如果希望在对象释放前进行额外的清理工作,可以实现一个析构器来代替`dealloc`方法。在对象被释放前,Swift 会自动调用析构器。当 Swift 调用完子类的析构器后,会自动调用父类的析构器。使用 Objective-C 类或者继承自 Objective-C 类的 Swift 类时,Swift 也会自动调用该类父类中的`dealloc`方法。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [析构器](http://wiki.jikexueyuan.com/project/swift/chapter2/15_Deinitialization.html) 章节了解关于如何实现析构器的更多信息。 78 | 79 | 80 | ## 兼容使用 Swift 类名的 Objective-C API 81 | 82 | Swift 类的命名空间基于其所在的模块,即使是使用来自 Objective-C 的代码。在 Objective-C 中所有的类都是全局命名空间的一部分,名字不能重复。而 Swift 类可以基于其所在模块来消除歧义。例如,`MyFramework`框架中的`DataManager`类在 Swift 中的全限定名是`MyFramework.DataManager`。一个 Swift 应用的`target`就是一个模块,因此,在一个叫`MyGreatApp`的应用里,一个叫做`Observer`的 Swift 类的全限定名是`MyGreatApp.Observer`。 83 | 84 | Swift 类在 Objective-C 代码中使用时,为了保持命名空间,Swift 类会将其全限定名暴露给 Objective-C 运行时。因此,使用那些用到 Swift 类名字符串的 API 时,必须使用类的全限定名。例如,你创建了一个基于文档的 Mac 应用,需要在应用的`Info.plist`里提供`NSDocument`子类的类名。在 Swift 中,必须使用`NSDocument`子类的全限定名,即应用名或者框架名加上子类名。 85 | 86 | 下面的例子中,`NSClassFromString(_:)`函数用于从一个字符串表示的类名获取该类的引用。为了获取 Swift 类,需要使用全限定名,也就是需要加上应用的名字。 87 | 88 | ``` swift 89 | let myPersonClass: AnyClass?= NSClassFromString("MyGreatApp.Person") 90 | ``` 91 | 92 | 93 | ## 与 Interface Builder 结合 94 | 95 | Swift 编译器包含一些特性,能让 Swift 类使用 Interface Builder 的一些特色功能。和 Objective-C 一样,在 Swift 也可使用 outlet,action 以及实时渲染。 96 | 97 | 98 | ### 使用 Outlet 和 Action 99 | 100 | 使用 Outlets 和 Actions 可以连接源代码和 Interface Builder 中的 UI 对象,只需在属性或方法声明前标记`@IBOutlet`或`@IBAction`特性。声明一个 outlet 集合同样是用`@IBOutlet`特性,只不过会为该类型指定一个数组。 101 | 102 | 在 Swift 中声明一个 outlet 时,应该将类型声明为隐式解包可选类型。通过这种方式,Swift 编译器会自动为它分配空值`nil`,就不需要在构造器中为其分配初始值了。在运行时,构造过程完成后,Interface Builder 会连接各个 outlet。如果从故事版或者`xib`文件实例化对象,则可以假定这些 outlet 已经连接完毕。 103 | 104 | 例如,下面的 Swift 代码声明了一个拥有 outlet,outlet 集合和 action 的类: 105 | 106 | ```swift 107 | class MyViewController: UIViewController { 108 | @IBOutlet weak var button: UIButton! 109 | @IBOutlet var textFields: [UITextField]! 110 | @IBAction func buttonTapped(sender: AnyObject) { 111 | print("button tapped!") 112 | } 113 | } 114 | ``` 115 | 116 | 117 | ### 实时渲染 118 | 119 | 可以使用`@IBDesignable`和`@IBInspectable`特性开启实时渲染,在 Interface Builder 中对自定义视图进行交互式设计。继承`UIView`或者`NSView`来自定义一个视图时,可以在类声明前标记`@IBDesignable`特性。在 Interface Builder 里添加自定义的视图后(在 Identity Inspector 面板的 Class 输入框中进行设置),Interface Builder 将在画布上实时渲染自定义视图。 120 | 121 | 还可以将`@IBInspectable`特性添加到类型兼容`用户定义运行时属性`(可以在 Identity Inspector 面板的 User Defined Runtime Attributes 中查看)的属性前。将自定义的视图添加到 Interface Builder 后,就可以在 Attributes Inspector 面板中编辑这些属性。 122 | 123 | ![](Attributes%20Inspector%402x.png) 124 | 125 | ![](Identity%20Inspector%402x.png) 126 | 127 | ```swift 128 | @IBDesignable 129 | class MyCustomView: UIView { 130 | @IBInspectable var textColor: UIColor 131 | @IBInspectable var iconHeight: CGFloat 132 | // ... 133 | } 134 | ``` 135 | 136 | 137 | ## 指定属性特性 138 | 139 | 在 Objective-C,属性通常会有一系列用于指定该属性的一些附加信息的属性特性。在 Swift,将通过不同的方式指明这些属性特性。 140 | 141 | 142 | ### 强引用和弱引用 143 | 144 | 在 Swift,属性默认都是强引用,可以使用`weak`关键字指明某个属性持有其所指向对象的弱引用。该关键字仅能修饰可选的类类型。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [类和结构](http://wiki.jikexueyuan.com/project/swift/chapter2/09_Classes_and_Structures.html) 章节了解更多信息。 145 | 146 | 147 | ### 读写和只读 148 | 149 | 在 Swift,没有`readwrite`和`readonly`特性。声明一个存储型属性时,使用`let`使其只读;使用`var`使其可读写。声明一个计算型属性时,只为其提供一个读取方法使其只读;同时提供读取方法和写入方法使其可读写。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [属性](http://wiki.jikexueyuan.com/project/swift/chapter2/10_Properties.html) 章节了解更多信息。 150 | 151 | 152 | ### 拷贝语义 153 | 154 | 在 Swift,Objective-C 的`copy`属性特性被转化为`@NSCopying`。这种类型的属性必须符合`NSCopying`协议。请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [类和结构](http://wiki.jikexueyuan.com/project/swift/chapter2/09_Classes_and_Structures.html) 章节了解更多信息。 155 | 156 | 157 | ## 实现 Core Data 的 NSManagedObject 子类 158 | 159 | Core Data 提供了底层存储以及`NSManagedObject`子类的属性实现,还提供了在对多关系中添加和移除对象的实例方法实现。可以使用`@NSManaged`特性告知 Swift 编译器,一个声明的底层存储和实现部分将在运行时由 Core Data 提供。 160 | 161 | 在`NSManagedObject`子类中,为每一个和 Core Data 实体模型文件中相对应的属性或者方法声明标记`@NSManaged`特性。例如,思考下面这个叫做`Person`的 Core Data 实体,它有个叫做`name`的`String`类型的属性,以及一个叫做`friends`的对多关系。 162 | 163 | ![](coredataeditor_2x.png) 164 | 165 | 相对应的`NSManagedObject`子类`Person`中的代码如下: 166 | 167 | ```swift 168 | import CoreData 169 | class Person: NSManagedObject { 170 | @NSManaged var name: String 171 | @NSManaged var friends: NSSet 172 | 173 | @NSManaged func addFriendsObject(friend: Person) 174 | @NSManaged func removeFriendsObject(friend: Person) 175 | @NSManaged func addFriends(friends: NSSet) 176 | @NSManaged func removeFriends(friends: NSSet) 177 | } 178 | ``` 179 | 180 | `name`和`friends`属性都标记了`@NSManaged`特性,以此表明 Core Data 会在运行时提供其实现和存储。由于`friends`是个对多关系,因此 Core Data 还提供了一些相关的存取方法。 181 | 182 | 为了在 Swift 中配置一个`NSManagedObject`的子类供 Core Data 模型实体使用,在 Xcode 中打开 Data Model Inspector,在 Class 输入框中输入类名,并在 Module 输入框的下拉列表中选择`Current Product Module`。 183 | 184 | ![](coredatanamespace_2x.png) 185 | 186 | 187 | ## 声明协议 188 | 189 | 在 Swift 中可以定义能让 Objective-C 类采用的协议,在协议声明前添加`@objc`特性即可。 190 | 191 | ```swift 192 | import UIKit 193 | @objc protocol MyCustomProtocol { 194 | var people: [Person] { get } 195 | 196 | func tableView(_ tableView: UITableView, configure cell: UITableViewCell, forPerson person: Person) 197 | 198 | @objc optional func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forPerson person: Person) 199 | } 200 | ``` 201 | 202 | 为了符合协议,Objective-C 类必须实现协议中声明的所有构造器,属性,下标,以及方法。可选协议要求必须标记`@objc`特性以及`optional`修饰符。 203 | 204 | Objective-C 类可以像采用 Objective-C 协议一样采用 Swift 协议,实现所有协议要求即可。 205 | 206 | ```objective-c 207 | @interface MyCustomController: UIViewController 208 | @property (nonatomic, strong) NSArray *people; 209 | @end 210 | 211 | @implementation MyCustomController 212 | @synthesize people; 213 | 214 | - (void)tableView:(UITableView *)tableView 215 | configure:(UITableViewCell *)cell 216 | forPerson:(Person *)person 217 | { 218 | // ... 219 | } 220 | @end 221 | ``` 222 | -------------------------------------------------------------------------------- /02-Interoperability/03-Working with Cocoa Frameworks.md: -------------------------------------------------------------------------------- 1 | # 使用 Cocoa 框架 2 | 3 | - [Foundation](#foundation) 4 | - [被桥接的类型](#bridged_types) 5 | - [被重命名的类型](#renamed_types) 6 | - [字符串](#strings) 7 | - [数值](#numbers) 8 | - [数组](#arrays) 9 | - [集合](#sets) 10 | - [字典](#dictionaries) 11 | - [Core Foundation](#core_foundation) 12 | - [重映射类型](#remapped_types) 13 | - [内存受管理的对象](#memory_managed_objects) 14 | - [非托管对象](#unmanaged_objects) 15 | - [统一日志](#unified_logging) 16 | - [Cocoa 结构体](#cocoa_structures) 17 | 18 | 为了便于与 Objective-C 进行交互,Swift 提供了高效便捷的方式来使用 Cocoa 框架。 19 | 20 | Swift 会自动将一些 Objective-C 类型转换为 Swift 类型,也会将一些 Swift 类型转换为 Objective-C 类型。可以在 Objective-C 和 Swift 间相互转换的数据类型被称为**桥接**类型。例如,在 Swift,可以将一个`String`类型的值传递给一个接收`NSString`对象的 Objective-C 方法。除此之外,很多 Cocoa 框架,包括 Foundation,AppKit,以及 UIKit,都改善了它们的 API,从而在 Swift 中使用起来更加自然。例如,`NSCoder`的方法`decodeObjectOfClass(_:forKey:)`使用 Swift 泛型来提供强类型的方法签名。 21 | 22 | 23 | ## Foundation 24 | 25 | Foundation 框架为应用和框架提供了基层功能,包括数据存储,文本处理,日期时间,排序过滤,持久化和网络等。 26 | 27 | 28 | ### 被桥接的类型 29 | 30 | Swift 中的 Foundation 提供了如下桥接值类型用于替代如下 Objective-C 引用类型: 31 | 32 | Objective-C 引用类型 | Swift 值类型 33 | :-----------------:|:-------------: 34 | NSAffineTransform | AffineTransform 35 | NSArray | Array 36 | NSCalendar | Calendar 37 | NSCharacterSet | CharacterSet 38 | NSData | Data 39 | NSDateComponents | DateComponents 40 | NSDateInterval | DateInterval 41 | NSDate | Date 42 | NSDecimalNumber | Decimal 43 | NSDictionary | Dictionary 44 | NSIndexPath | IndexPath 45 | NSIndexSet | IndexSet 46 | NSLocale | Locale 47 | NSMeasurement | Measurement 48 | NSNotification | Notification 49 | NSNumber | Bool, Double, Float, Int, UInt 50 | NSPersonNameComponents | PersonNameComponents 51 | NSSet | Set 52 | NSString | String 53 | NSTimeZone | TimeZone 54 | NSURLComponents | URLComponents 55 | NSURLQueryItem | URLQueryItem 56 | NSURL | URL 57 | NSURLComponents | URLComponents 58 | NSURLQueryItem | URLQueryItem 59 | NSURLRequest | URLRequest 60 | NSUUID | UUID 61 | 62 | 这些值类型的功能与其对应的引用类型相同。包含不可变和可变子类的类簇被统一桥接为单个值类型。Swift 代码使用`var`和`let`来控制可变性,因此它们不再需要两个类了。原先的引用类型依然可以通过加上`NS`前缀来访问。 63 | 64 | 可以使用 Objective-C 桥接引用类型的地方就可以使用 Swift 桥接值类型,从而可以在 Swift 代码中以更加自然的方式使用这些原有的功能。基于这个原因,你应该尽量避免在 Swift 代码中使用 Objective-C 桥接引用类型。事实上,Swift 代码导入 Objective-C API 时,导入器会将引用类型替换为相对应的值类型。与之类似,Objective-C 代码导入 Swift API 时,导入器也会将值类型替换为相对应的引用类型。 65 | 66 | 如果你需要使用原本的 Foundation 对象,你可以使用`as`操作符在这些桥接类型间进行转换。 67 | 68 | 69 | ### 被重命名的类型 70 | 71 | Swift 中的 Foundation 重名了一些类,协议,枚举以及常量。 72 | 73 | 在导入 Foundation 类时,Swift 丢弃了类名的`NS`前缀,但有如下例外情况: 74 | 75 | - Objective-C 和 Objective-C 运行时中的一些特定类,例如`NSObject`,`NSAutoreleasePool`,`NSException`,`NSProxy`。 76 | - 平台特定类,例如`NSBackgroundActivity`,`NSUserNotification`,`NSXPCConnection`。 77 | - 拥有对应的桥接值类型的一些类,例如`NSString`,`NSDictionary`,`NSURL`。 78 | - 目前尚无对应的桥接值类型,但是未来会有的一些类,例如`NSAttributedString`,`NSRegularExpression`,`NSPredicate`。 79 | 80 | Foundation 中的类经常会声明一些枚举和常量类型。导入这些类型时,Swift 会将它们移到相关类型的内部作为嵌套类型。例如,`NSJSONReadingOptions`的选项集会被导入为`JSONSerialization.ReadingOptions`。 81 | 82 | 83 | ### 字符串 84 | 85 | 字符串可以在`String`类型和`NSString`类型之间桥接。你可以使用`as`运算符将`String`值转换为`NSString`对象。你也可以通过提供类型标注的方式利用字符串字面量创建`NSString`对象。 86 | 87 | ```swift 88 | import Foundation 89 | let string: String = "abc" 90 | let bridgedString: NSString = string as NSString 91 | 92 | let stringLiteral: NSString = "123" 93 | if let integerValue = Int(stringLiteral as String) { 94 | print("\(stringLiteral) is the integer \(integerValue)") 95 | } 96 | // 打印 "123 is the integer 123" 97 | ``` 98 | 99 | > 注意 100 | > Swift 的`String`类型由独立编码的 Unicode 字符组成,并提供了在各种 Unicode 表现形式下访问这些字符的支持。`NSString`类以 UTF-16 码元序列的形式编码兼容 Unicode 的文本字符串。表示字符串长度,字符索引,区间的这些依据 16 位平台字节序值的`NSString`方法,均有相对应的 Swift 方法,这些方法使用`String.Index`和`Range`值而不是`Int`和`NSRange`值。 101 | 102 | 103 | ### 数值 104 | 105 | Swift 会在 `NSNumber` 类和 Swift 算术类型之间桥接,包括 `Int`,`Double`,`Bool`。 106 | 107 | 你可以使用 `as` 运算符转换 Swift 数值来创建 `NSNumber` 对象。由于 `NSNumber` 可以包含多种不同类型,将其转换为 Swift 数值时必须使用 `as?` 运算符。例如,将表示数字500的 `NSNumber` 值转换为 Swift 类型 `Int8` 将会失败并返回 `nil`,因为 `Int8` 值可表示的最大值是127。 108 | 109 | 你还可以通过显式提供类型标注,使用浮点数、整数或布尔字面量创建 `NSNumber` 对象。 110 | 111 | ```swift 112 | import Foundation 113 | let number = 42 114 | let bridgedNumber: NSNumber = number as NSNumber 115 | 116 | let integerLiteral: NSNumber = 5 117 | let floatLiteral: NSNumber = 3.14159 118 | let booleanLiteral: NSNumber = true 119 | ``` 120 | 121 | > 注意 122 | > Objective-C 中具有平台适应性的整数类型,例如 `NSUInteger` 和 `NSInteger`,会被桥接为 `Int`。 123 | 124 | 125 | ### 数组 126 | 127 | Swift 会在`Array`类型和`NSArray`类之间桥接。使用轻量泛型的`NSArray`对象被桥接时,其元素类型也会被桥接过去。如果`NSArray`对象没有使用轻量泛型,那么它会被桥接为`[Any]`类型的 Swift 数组。 128 | 129 | 例如,思考以下 Objective-C 声明: 130 | 131 | ```objective-c 132 | @property NSArray *objects; 133 | @property NSArray *dates; 134 | - (NSArray *)datesBeforeDate:(NSDate *)date; 135 | - (void)addDatesParsedFromTimestamps:(NSArray *)timestamps; 136 | ``` 137 | 138 | Swift 以如下所示导入它们: 139 | 140 | ```swift 141 | var objects: [Any] 142 | var dates: [Date] 143 | func datesBeforeDate(date: Date) -> [Date] 144 | func addDatesParsedFromTimestamps(timestamps: [String]) 145 | ``` 146 | 147 | 还可以直接利用 Swift 数组字面量创建`NSArray`对象,这同样遵循上面提到的桥接规则。将常量或变量声明为`NSArray`类型并为其赋值数组字面量时,Swift 将会创建`NSArray`对象,而不是 Swift 数组。 148 | 149 | ```swift 150 | let schoolSupplies: NSArray = ["Pencil", "Eraser", "Notebkko"] 151 | // schoolSupplies 是一个包含三个元素的 NSArray 对象 152 | ``` 153 | 154 | 155 | ### 集合 156 | 157 | Swift 也会在`Set`类型和`NSSet`类之间桥接。使用轻量泛型的`NSSet`对象会被桥接为`Set`类型的 Swift 集合。如果`NSSet`对象没有使用轻量泛型,那么它会被桥接为`Set`类型的 Swift 集合。 158 | 159 | 例如,思考以下 Objective-C 声明: 160 | 161 | ```objective-c 162 | @property NSSet *objects; 163 | @property NSSet *words; 164 | - (NSSet *)wordsMatchingPredicate:(NSPredicate *)predicate; 165 | - (void)removeWords:(NSSet *)words; 166 | ``` 167 | 168 | Swift 以如下所示导入它们: 169 | 170 | ```swift 171 | var objects: Set 172 | var words: Set 173 | func wordsMatchingPredicate(predicate: NSPredicate) -> Set 174 | func removeWords(words: Set) 175 | ``` 176 | 177 | 还可以直接利用 Swift 数组字面量创建`NSSet`对象,这同样遵循上面提到的桥接规则。将常量或者变量声明为`NSSet`类型,并为其赋值数组字面量时,Swift 将会创建`NSSet`对象,而不是 Swift 集合。 178 | 179 | ```swift 180 | let amenities: NSSet = ["Sauna", "Steam Room", "Jacuzzi"] 181 | // amenities 是一个包含三个元素的 NSSet 对象 182 | ``` 183 | 184 | 185 | ### 字典 186 | 187 | Swift 同样会在`Dictionary`类型和`NSDictionary`类之间桥接。使用轻量泛型的`NSDictionary`对象会被桥接为`[Key: Value]`类型的 Swift 字典。如果`NSDictionary`对象没有使用轻量泛型,那么它会被桥接为`[AnyHashable: Any]`类型的 Swift 字典。 188 | 189 | 例如,思考以下 Objective-C 声明: 190 | 191 | ```objective-c 192 | @property NSDictionary *keyedObjects; 193 | @property NSDictionary *cachedData; 194 | - (NSDictionary *)fileSizesForURLsWithSuffix:(NSString *)suffix; 195 | - (void)setCacheExpirations:(NSDictionary *)expirations; 196 | ``` 197 | 198 | Swift 以如下所示导入它们: 199 | 200 | ```swift 201 | var keyedObjects: [AnyHashable: Any] 202 | var cachedData: [URL: Data] 203 | func fileSizesForURLsWithSuffix(suffix: String) -> [URL: NSNumber] 204 | func setCacheExpirations(expirations: [URL: NSDate]) 205 | ``` 206 | 207 | 还可以直接利用 Swift 字典字面量创建`NSDictionary`对象,这同样遵循上面提到的桥接规则。将常量或者变量声明为`NSDictionary`类型,并为其赋值字典字面量时,Swift 将会创建`NSDictionary`对象,而不是 Swift 字典。 208 | 209 | ```swift 210 | let medalRankings: NSDictionary = ["Gold": "1st Place", "Silver": "2nd Place", "Bronze": "3rd Place"] 211 | // medalRankings 是一个包含三个键值对的 NSDictionary 对象 212 | ``` 213 | 214 | 215 | ## Core Foundation 216 | 217 | Core Foundation 类型会被导入为 Swift 类。无论是否提供了内存管理标注,Swift 都会自动管理 Core Foundation 对象的内存,包括你自己实例化的 Core Foundation 对象。在 Swift,可以将每一对可以桥接的 Fundation 和 Core Foundation 类型互换使用。如果先将 Core Foundation 类型桥接为 Foundation 类型,就可以进一步桥接为 Swift 标准库类型。 218 | 219 | 220 | ### 重映射类型 221 | 222 | Swift 导入 Core Foundation 类型时,会将这些类型的名字重映射,从类型名字的末端移除 *Ref*,所有的 Swift 类都是引用类型,因此该后缀是多余的。 223 | 224 | Core Foundation 的`CFTypeRef`类型会重映射为`Anyobject`类型。以前使用`CFTypeRef`的地方,现在该换成`AnyObject`了。 225 | 226 | 227 | ### 内存受管理的对象 228 | 229 | 对于从带内存管理标注的 API 返回的 Core Foundation 对象,Swift 会自动对其进行内存管理,你不需要再调用`CFRetain`,`CFRelease`,或者`CFAutorelease`函数。 230 | 231 | 如果自定义的 C 函数或 Objective-C 方法返回 Core Foundation 对象,需要用`CF_RETURNS_RETAINED`或者`CF_RETURNS_NOT_RETAINED`宏标注这些函数或方法,从而帮助编译器自动插入内存管理函数调用。还可以使用`CF_IMPLICIT_BRIDGING_ENABLED`和`CF_IMPLICIT_BRIDGING_DISABLED`宏围住那些遵循 Core Foundation 内存管理命名规定的 C 函数声明,从而能够根据命名推导出内存管理策略。 232 | 233 | 如果只调用那些带有内存管理标注并且不会间接返回 Core Foundation 对象的 API,那么可以略过本节的剩余部分了。否则,需要进一步学习有关 Core Foundation 非托管对象的知识。 234 | 235 | 236 | ### 非托管对象 237 | 238 | 对于那些没有内存管理标注的 API,编译器无法自动对返回的 Core Foundation 对象进行内存管理。Swift 将这些返回的 Core Foundation 对象包装在一个`Unmanaged`结构中。所有被间接返回的 Core Foundation 对象也都是非托管对象。例如,这有个不带内存管理标注的 C 函数: 239 | 240 | ```objective-c 241 | CFStringRef StringByAddingTwoStrings(CFStringRef string1, CFStringRef string2) 242 | ``` 243 | 244 | Swift 以如下所示导入它们: 245 | 246 | ```swift 247 | func StringByAddingTwoStrings(_: CFString!, _: CFString!) -> Unmanaged! { 248 | // ... 249 | } 250 | ``` 251 | 252 | 从没有内存管理标注的 API 接收到非托管对象后,在使用它之前,必须将它转换为能够接受内存管理的对象。通过这种方式,Swift 就可以为你对其进行内存管理。`Unmanaged`结构提供了两个方法,用于将一个非托管对象转换为一个可接受内存管理的对象,即`takeUnretainedValue()`方法和`takeRetainedValue()`方法。这两个方法均会返回解包后的原始对象,可以根据 API 返回的是非保留对象还是被保留对象来选择对应的方法。 253 | 254 | 例如,假设上面的 C 函数不会保留`CFString`对象。在使用这个对象前,应该使用`takeUnretainedValue()`函数。 255 | 256 | ```swift 257 | let memoryManagedResult = StringByAddingTwoStrings(str1, str2).takeUnretainedValue() 258 | // memoryManagedResult 是一个接受内存管理的 CFString 259 | ``` 260 | 261 | 当然,也可以对一个非托管对象使用`retain()`,`release()`和`autorelease()`方法,但是这种做法并不推荐。 262 | 263 | 想要了解更多信息,请参阅 [*Memory Management Programming Guide for Core Foundation*](https://developer.apple.com/library/prerelease/ios/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/CFMemoryMgmt.html#//apple_ref/doc/uid/10000127i) 中的 [*Core Foundation Object Lifecycle Management*](https://developer.apple.com/library/prerelease/ios/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Articles/lifecycle.html#//apple_ref/doc/uid/TP40002439-SW1) 小节。 264 | 265 | 266 | ## 统一日志 267 | 268 | 统一日志系统提供了一个 API 来捕获系统各个层级传递的消息,它是 Foundation 框架中`NSLog`函数的替代者。统一日志只在 `iOS 10.0`,`macOS 10.12`,`tvOS 10.0`,`watchOS 3.0`,以及更高的系统平台上可用。 269 | 270 | 在 Swift,你可以通过顶级函数`os_log(_:dso:log:type:_:)`来使用统一日志系统,该函数声明在`os`模块的子模块`log`中。 271 | 272 | ```swift 273 | import os.log 274 | 275 | os_log("This is a log message.") 276 | ``` 277 | 278 | 你可以通过使用`NSString`或`printf`格式化字符串连同单个或多个尾参数来格式化日志信息。 279 | 280 | ```swift 281 | let fileSize = 1234567890 282 | os_log("Finished downloading file. Size: %{iec-bytes}d", fileSize) 283 | ``` 284 | 285 | 你还可以指定一个日志系统中定义的日志级别,例如`Info`,`Debug`,`Error`,从而根据日志事件的重要性来控制日志信息如何被处理。例如,某条信息可能很有帮助,但是该信息并不是排查错误所必需的,那么这条信息就应该记录在信息级别。 286 | 287 | ```swift 288 | os_log("This is additional info that may be helpful for troubleshooting.", type: .info) 289 | ``` 290 | 291 | 为了将某条信息记录到指定子系统,你可以创建一个新的`OSLog`对象,指定子系统和类别,并将其作为参数传入`os_log`函数。 292 | 293 | ```swift 294 | et customLog = OSLog("com.your_company.your_subsystem_name.plist", "your_category_name") 295 | os_log("This is info that may be helpful during development or debugging.", log: customLog, type: .debug) 296 | ``` 297 | 298 | 想要了解更多关于统一日志的信息,请参阅 [Logging](https://developer.apple.com/reference/os/1891852-logging)。 299 | 300 | 301 | ## Cocoa 结构体 302 | 303 | 在 Swift,Coach 和 Foundation 中的一些内建结构体可以和`NSValue`桥接: 304 | 305 | - `CATransform3D` 306 | - `CLLocationCoordinate2D` 307 | - `CGAffineTransform` 308 | - `CGPoint` 309 | - `CGRect` 310 | - `CGSize` 311 | - `CGVector` 312 | - `CMTimeMapping` 313 | - `CMTimeRange` 314 | - `CMTime` 315 | - `MKCoordinateSpan` 316 | - `NSRange` 317 | - `SCNMatrix4` 318 | - `SCNVector3` 319 | - `SCNVector4` 320 | - `UIEdgeInsets` 321 | - `UIOffset` 322 | -------------------------------------------------------------------------------- /02-Interoperability/04-Adopting Cocoa Design Patterns.md: -------------------------------------------------------------------------------- 1 | # 采用 Cocoa 设计模式 2 | 3 | - [代理](#delegation) 4 | - [惰性初始化](#lazy_initialization) 5 | - [错误处理](#error_handling) 6 | - [捕获和处理错误](#catching_and_handling_an_error) 7 | - [将错误转换为可选值](#converting_errors_to_optional_values) 8 | - [抛出错误](#throwing_an_error) 9 | - [处理异常](#handling_exceptions) 10 | - [捕获和处理自定义错误](#catching_and_handling_custom_errors) 11 | - [键值观察](#key_value_observing) 12 | - [目标-动作](#target_action) 13 | - [单例](#singleton) 14 | - [内省](#introspection) 15 | - [序列化](#serializing) 16 | - [本地化](#localization) 17 | - [自动释放池](#autorelease_pools) 18 | - [API 可用性](#API_Availability) 19 | - [处理命令行参数](#Processing_Command-Line_Arguments) 20 | 21 | 使用 Cocoa 既有的设计模式,能帮助开发者开发出设计巧妙、扩展性强的应用程序。这些模式很多都依赖于在 Objective-C 中定义的类。由于 Swift 与 Objective-C 的互用性,你依然可以在 Swift 中使用这些设计模式。在许多情况下,你甚至可以使用 Swift 的语言特性扩展或简化这些 Cocoa 设计模式,使这些设计模式更加强大易用。 22 | 23 | 24 | ## 代理 25 | 26 | 在 Swift 和 Objective-C,代理通常表现为一个定义交互方法的协议和符合协议的代理属性。就像在 Objective-C,在向代理发送可能无法响应的消息之前,应询问代理能否响应消息。在 Swift,可以使用可选链语法在一个可能为`nil`的对象上调用可选的代理方法,并使用`if-let`语法解包可能存在的返回值。下面的代码阐明了这个过程: 27 | 28 | 1. 检查`myDelegate`不为`nil`。 29 | 2. 检查`myDelegate`是否实现了`window:willUseFullScreenContentSize:`方法。 30 | 3. 如果步骤1和步骤2的检查顺利通过,那么调用该方法,将返回值赋值给名为`fullScreenSize`的常量。 31 | 4. 在控制台打印方法的返回值。 32 | 33 | ```swift 34 | class MyDelegate: NSObject, NSWindowDelegate { 35 | func window(_ window: NSWindow, willUseFullScreenContentSize proposedSize: NSSize) -> NSSize { 36 | return proposedSize 37 | } 38 | } 39 | myWindow.delegate = MyDelegate() 40 | if let fullScreenSize = myWindow.delegate?.window(myWindow, willUseFullScreenContentSize: mySize) { 41 | print(NSStringFromSize(fullScreenSize)) 42 | } 43 | ``` 44 | 45 | 46 | ## 惰性初始化 47 | 48 | 惰性属性的值只会在被初次访问时才被初始化。如果属性的初始化过程十分复杂或者代价昂贵,或者属性的初始值无法在实例的构造过程完成前确定时,那么就可以使用惰性属性。 49 | 50 | 在 Objective-C,一个属性可能会覆写其自动合成的读取方法,只在实例变量为`nil`时才初始化实例变量: 51 | 52 | ```objective-c 53 | @property NSXMLDocument *XML; 54 | 55 | - (NSXMLDocument *)XML { 56 | if (_XML == nil) { 57 | _XML = [[NSXMLDocument alloc] initWithContentsOfURL:[[Bundle mainBundle] URLForResource:@"/path/to/resource" withExtension:@"xml"] options:0 error:nil]; 58 | } 59 | 60 | return _XML; 61 | } 62 | ``` 63 | 64 | 在 Swift,可以使用`lazy`修饰符声明一个存储属性,这将使计算初始值的表达式只在属性被初次访问时才进行求值: 65 | 66 | ```swift 67 | lazy var XML: XMLDocument = try! XMLDocument(contentsOf: Bundle.main.url(forResource: "document", withExtension: "xml")!, options: 0) 68 | ``` 69 | 70 | 由于惰性属性只在被初次访问时才进行初始化,此时实例本身已被完全初始化,因此在初始化表达式中可以使用`self`: 71 | 72 | ```swift 73 | var pattern: String 74 | lazy var regex: NSRegularExpression = try! NSRegularExpression(pattern: self.pattern, options: []) 75 | ``` 76 | 77 | 如果还需在初始化的基础上进行额外的设置,可以通过返回属性初始值的自求值闭包给属性赋值: 78 | 79 | ```swift 80 | lazy var currencyFormatter: NumberFormatter = { 81 | let formatter = NumberFormatter() 82 | formatter.numberStyle = .currency 83 | formatter.currencySymbol = "¤" 84 | return formatter 85 | }() 86 | ``` 87 | 88 | > 注意 89 | > 如果一个惰性属性还未被初始化就被多个线程同时访问,那么此时无法保证此惰性属性只被初始化一次。 90 | 91 | 请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [延迟存储属性](http://wiki.jikexueyuan.com/project/swift/chapter2/10_Properties.html#stored_properties) 小节。 92 | 93 | 94 | ## 错误处理 95 | 96 | 在 Cocoa 中,会产生错误的方法将 `NSError` 指针参数作为最后一个参数,当产生错误时,该参数会被 `NSError` 对象填充。Swift 会自动将 Objective-C 中会产生错误的方法转换为根据 Swift 原生错误处理机制抛出错误的方法。 97 | 98 | > 注意 99 | > 某些接受错误的方法,例如委托方法,或者接受一个带有 `NSError` 参数的块作为参数的方法,不会被 Swift 导入为抛出方法。 100 | 101 | 例如,请考虑如下来自于 `NSFileManager` 的 Objective-C 方法: 102 | 103 | ```objective-c 104 | - (BOOL)removeItemAtURL:(NSURL *)URL 105 | error:(NSError **)error; 106 | ``` 107 | 108 | 在 Swift,它会被这样导入: 109 | 110 | ```swift 111 | func removeItem(at: URL) throws 112 | ``` 113 | 114 | 注意 `removeItem(at:)` 方法被 Swift 导入时,返回值类型为 `Void`,没有 `error` 参数,并且还有一个 `throws` 声明。 115 | 116 | 如果 Objective-C 方法的最后一个非块类型的参数是 `NSError **` 类型,Swift 会将之替换为 `throws` 关键字,以表明该方法可以抛出一个错误。如果 Objective-C 方法的错误参数也是它的第一个参数,Swift 会尝试删除选择器的第一部分中的 “WithError” 或 “AndReturnError” 后缀(如果存在)来进一步简化方法名。如果简化后的方法名会和其他方法名冲突,则不会对方法名进行简化。 117 | 118 | 如果产生错误的 Objective-C 方法返回一个用来表示方法调用成功或失败的 `BOOL` 值,Swift 会把返回值转换为 `Void`。同样,如果产生错误的 Objective-C 方法返回一个`nil` 值来表明方法调用失败,Swift 会把返回值转换为非可选类型。 119 | 120 | 否则,如果不能推断任何约定,则该方法保持不变。 121 | 122 | > 注意 123 | > 在一个产生错误的 Objective-C 方法声明上使用 `NS_SWIFT_NOTHROW` 宏可以防止该方法被 Swift 作为抛出方法导入。 124 | 125 | 126 | ### 捕获和处理错误 127 | 128 | 在 Objective-C,错误处理是可选的,这意味着除非提供了一个错误指针,否则方法产生的错误会被忽略。在 Swift,调用一个会抛出错误的方法时必须明确进行错误处理。 129 | 130 | 以下示例演示了在 Objective-C 中调用方法时如何处理错误: 131 | 132 | ```objective-c 133 | NSFileManager *fileManager = [NSFileManager defaultManager]; 134 | NSURL *fromURL = [NSURL fileURLWithPath:@"/path/to/old"]; 135 | NSURL *toURL = [NSURL fileURLWithPath:@"/path/to/new"]; 136 | NSError *error = nil; 137 | BOOL success = [fileManager moveItemAtURL:fromURL toURL:toURL error:&error]; 138 | if (!success) { 139 | NSLog(@"Error: %@", error.domain); 140 | } 141 | ``` 142 | 143 | Swift 中等效的代码如下所示: 144 | 145 | ```swift 146 | let fileManager = FileManager.default 147 | let fromURL = URL(fileURLWithPath: "/path/to/old") 148 | let toURL = URL(fileURLWithPath: "/path/to/new") 149 | do { 150 | try fileManager.moveItem(at: fromURL, to: toURL) 151 | } catch let error as NSError { 152 | print("Error: \(error.domain)") 153 | } 154 | ``` 155 | 156 | 此外,你可以使用 `catch` 子句来匹配特定的错误代码以便区分可能的失败情况: 157 | 158 | ```swift 159 | do { 160 | try fileManager.moveItem(at: fromURL, to: toURL) 161 | } catch CocoaError.fileNoSuchFile { 162 | print("Error: no such file exists") 163 | } catch CocoaError.fileReadUnsupportedScheme { 164 | print("Error: unsupported scheme (should be 'file://')") 165 | } 166 | ``` 167 | 168 | 169 | ### 将错误转换为可选值 170 | 171 | 在 Objective-C 中,当你只关心是否有错误,而不是发生什么特定错误时,你可以传递 `NULL` 作为错误参数。在 Swift,你可以使用 `try?` 关键字将抛出表达式转换为返回可选值的表达式,然后检查返回值是否为 `nil`。 172 | 173 | 例如,`NSFileManager` 的实例方法 `URLForDirectory(_:inDomain:appropriateForURL:create:)` 会返回指定搜索路径和域中的 URL,或者如果适当的 URL 不存在且不能创建,则会产生错误。在 Objective-C 中,此方法成功或是失败可以通过是否返回 URL 对象来判断。 174 | 175 | ```objective-c 176 | NSFileManager *fileManager = [NSFileManager defaultManager]; 177 | 178 | NSURL *tmpURL = [fileManager URLForDirectory:NSCachesDirectory 179 | inDomain:NSUserDomainMask 180 | appropriateForURL:nil 181 | create:YES 182 | error:NULL]; 183 | if (tmpURL != nil) { 184 | // ... 185 | } 186 | ``` 187 | 188 | 在 Swfit 中你可以像下面这样做: 189 | 190 | ```swift 191 | let fileManager = FileManager.default 192 | if let tmpURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { 193 | // ... 194 | } 195 | ``` 196 | 197 | 198 | ### 抛出错误 199 | 200 | 如果在一个 Objective-C 方法中发生错误,则使用错误对象来填充该方法的错误指针参数: 201 | 202 | ```objective-c 203 | // 发生一个错误 204 | if (errorPtr) { 205 | *errorPtr = [NSError errorWithDomain:NSURLErrorDomain 206 | code:NSURLErrorCannotOpenFile 207 | userInfo:nil]; 208 | } 209 | ``` 210 | 211 | 如果在一个 Swift 方法中发生错误,则错误会被抛出,并且自动传播给调用者: 212 | 213 | ```swift 214 | // 发生一个错误 215 | throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil) 216 | ``` 217 | 218 | 如果 Objective-C 代码调用会抛出错误的 Swift 方法,则该错误会自动填充到桥接的 Objective-C 方法的错误指针参数。 219 | 220 | 例如,考虑 `NSDocument` 中的 `read(from:ofType:)` 方法。在 Objective-C 中,此方法的最后一个参数是 `NSError **` 类型。在 Swift 的 `NSDocument` 子类中重写此方法时,该方法会以抛出错误的方式替代错误指针参数。 221 | 222 | ```swift 223 | class SerializedDocument: NSDocument { 224 | static let ErrorDomain = "com.example.error.serialized-document" 225 | 226 | var representedObject: [String: Any] = [:] 227 | 228 | override func read(from fileWrapper: FileWrapper, ofType typeName: String) throws { 229 | guard let data = fileWrapper.regularFileContents else { 230 | throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCannotOpenFile, userInfo: nil) 231 | } 232 | 233 | if case let JSON as [String: Any] = try JSONSerialization.jsonObject(with: data, options: []) { 234 | self.representedObject = JSON 235 | } else { 236 | throw NSError(domain: SerializedDocument.ErrorDomain, code: -1, userInfo: nil) 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | 如果该方法无法使用文档的常规文件内容来创建对象,就会抛出一个 `NSError` 对象。如果该方法在 Swift 中调用,则错误会传播到它的调用域。如果该方法在 Objective-C 中调用,则错误会填充错误指针参数。 243 | 244 | 245 | ### 处理异常 246 | 247 | 在 Objective-C 中,异常与错误不同。Objective-C 异常处理使用 `@try`,`@catch` 和 `@throw` 语法来标明不可恢复的程序错误。这与司空见惯的 Cocoa 错误模式截然不同,后者使用一个尾随的 `NSError` 参数来标明你在开发过程中设计的可恢复错误。 248 | 249 | 在 Swift 中,你可以从使用 Cocoa 错误模式传递的错误中恢复,如前文[错误处理](#error_handling)中所述。然而,没有可靠的方法可以从 Swift 中的 Objective-C 异常中恢复。要处理 Objective-C 异常,则需编写 Objective-C 代码,以便在异常到达任何 Swift 代码之前捕获异常。 250 | 251 | 关于 Objective-C 异常的更多信息,请参阅 [*Exception Programming Topics*](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Exceptions/Exceptions.html#//apple_ref/doc/uid/10000012i)。 252 | 253 | 254 | ### 捕获和处理自定义错误 255 | 256 | Objective-C 框架可以使用自定义错误域和枚举来组织相关的错误类别。 257 | 258 | 下面的例子展示了使用 Objective-C 中的 `NS_ERROR_ENUM` 宏定义的自定义错误类型: 259 | 260 | ```objective-c 261 | extern NSErrorDomain const MyErrorDomain; 262 | typedef NS_ERROR_ENUM(MyErrorDomain, MyError) { 263 | specificError1 = 0, 264 | specificError2 = 1 265 | }; 266 | ``` 267 | 268 | 如下示例展示了如何在 Swift 中使用该自定义错误类型生成错误: 269 | 270 | ```swift 271 | func customThrow() throws { 272 | throw NSError( 273 | domain: MyErrorDomain, 274 | code: MyError.specificError2.rawValue, 275 | userInfo: [ 276 | NSLocalizedDescriptionKey: "A customized error from MyErrorDomain." 277 | ] 278 | ) 279 | } 280 | 281 | do { 282 | try customThrow() 283 | } catch MyError.specificError1 { 284 | print("Caught specific error #1") 285 | } catch let error as MyError where error.code == .specificError2 { 286 | print("Caught specific error #2, ", error.localizedDescription) 287 | // Prints "Caught specific error #2. A customized error from MyErrorDomain." 288 | } let error { 289 | fatalError("Some other error: \(error)") 290 | } 291 | ``` 292 | 293 | 294 | ## 键值观察 295 | 296 | 键值观察是一种能让某个对象在其他对象指定属性变化时得到通知的机制。只要 Swift 类继承自 `NSObject` 类,就可以在 Swift 中通过如下两步实现键值观察。 297 | 298 | 1. 为想要观察的属性添加 `dynamic` 修改符和 `@objc` 属性。关于 `dynamic` 修饰符的更多信息,请参阅[强制动态派发](01-Interacting%20with%20Objective-C%20APIs.md#%E5%BC%BA%E5%88%B6%E5%8A%A8%E6%80%81%E6%B4%BE%E5%8F%91)小节。 299 | 300 | ```swift 301 | class MyObjectToObserve: NSObject { 302 | @objc dynamic var myDate = NSDate() 303 | func updateDate() { 304 | myDate = NSDate() 305 | } 306 | } 307 | ``` 308 | 309 | 2. 为键路径创建一个对应的观察者并调用 `observe(_:options:changeHandler)` 方法。关于键路径的更多信息,请参阅[键和键路径](https://github.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/blob/master/02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md#%E9%94%AE%E5%92%8C%E9%94%AE%E8%B7%AF%E5%BE%84)小节。 310 | 311 | ```swift 312 | class MyObjectToObserve: NSObject { 313 | @objc dynamic var myDate = NSDate() 314 | func updateDate() { 315 | myDate = NSDate() 316 | } 317 | } 318 | 319 | class MyObserver: NSObject { 320 | @objc var objectToObserve: MyObjectToObserve 321 | var observation: NSKeyValueObservation? 322 | 323 | init(object: MyObjectToObserve) { 324 | objectToObserve = object 325 | super.init() 326 | 327 | observation = observe(\.objectToObserve.myDate) { object, change in 328 | print("Observed a change to \(object.objectToObserve).myDate, updated to: \(object.objectToObserve.myDate)") 329 | } 330 | } 331 | } 332 | 333 | let observed = MyObjectToObserve() 334 | let observer = MyObserver(object: observed) 335 | 336 | observed.updateDate() 337 | ``` 338 | 339 | 340 | ## 目标-动作 341 | 342 | 目标-动作是一种常见的 Cocoa 设计模式,可以在特定事件发生时,让某个对象向另一个对象发送消息。Swift 和 Objective-C 的目标-动作模式基本类似。在 Swift,可以使用`Selector`类型引用 Objective-C 的选择器。请参阅 [选择器](01-Interacting%20with%20Objective-C%20APIs.md#%E9%80%89%E6%8B%A9%E5%99%A8) 小节查看在 Swift 中使用目标-动作模式的示例。 343 | 344 | 345 | ## 单例 346 | 347 | 单例模式提供了一个可全局访问的共享对象。可以自己创建在应用程序内共享的单例对象,从而提供一个统一的资源或服务的访问入口,比如一个播放音效的音频通道或发起 HTTP 请求的网络管理者。 348 | 349 | 在 Objective-C,可以用`dispatch_once`函数包裹初始化代码,从而保证在应用程序的生命周期内,块内的代码只会执行一次,这样就确保了只有唯一的实例会被创建: 350 | 351 | ```objective-c 352 | + (instancetype)sharedInstance { 353 | static id _sharedInstance = nil; 354 | static dispatch_once_t onceToken; 355 | dispatch_once(&onceToken, ^{ 356 | _sharedInstance = [[self alloc] init]; 357 | }); 358 | 359 | return _sharedInstance; 360 | } 361 | ``` 362 | 363 | 在 Swift,可以简单地使用静态类型属性来实现单例,它能保证延迟初始化只执行一次,即使在多个线程同时访问的情况下: 364 | 365 | ```swift 366 | class Singleton { 367 | static let sharedInstance = Singleton() 368 | } 369 | ``` 370 | 371 | 如果在初始化的基础上还需要进行额外的设置,那么可以通过调用闭包返回初始值的方式来初始化: 372 | 373 | ```swift 374 | class Singleton { 375 | static let sharedInstance: Singleton = { 376 | let instance = Singleton() 377 | // 设置代码 378 | return instance 379 | }() 380 | } 381 | ``` 382 | 383 | 请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [类型属性](http://wiki.jikexueyuan.com/project/swift/chapter2/10_Properties.html#type_properties) 小节获取更多信息。 384 | 385 | 386 | ## 内省 387 | 388 | 在 Objective-C,使用`isKindOfClass:`方法检查某个对象是否是特定类型的实例,使用`conformsToProtocol:`方法检查某个对象是否符合特定协议。在 Swift,可以使用`is`运算符来检查类型,可以使用`as?`运算符向下转换到指定类型。 389 | 390 | 可以使用`is`运算符检查一个实例是否是指定类或其子类的实例。若是,那么`is`返回结果为`true`,反之为`false`。 391 | 392 | ```swift 393 | if object is UIButton { 394 | // object 是 UIButton 类型 395 | } else { 396 | // object 不是 UIButton 类型 397 | } 398 | ``` 399 | 400 | 也可以使用`as?`运算符尝试向下转换到子类型。`as?`运算符返回可选类型的值,可结合`if-let`语句使用。 401 | 402 | ```swift 403 | if let button = object as? UIButton { 404 | // object 成功转换为 UIButton 并绑定到 button 405 | } else { 406 | // object 无法转换为 UIButton 407 | } 408 | ``` 409 | 请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [类型转换](http://wiki.jikexueyuan.com/project/swift/chapter2/19_Type_Casting.html) 章节获取更多信息。 410 | 411 | 检查协议符合性以及转换到符合协议类型的语法和上述类型检查和转换的语法是完全一样的。如下是使用`as?`检查协议符合性的示例: 412 | 413 | ```swift 414 | if let dataSource = object as? UITableViewDataSource { 415 | // object 符合 UITableViewDataSource 协议并绑定到 dataSource 416 | } else { 417 | // object 不符合 UITableViewDataSource 协议 418 | } 419 | ``` 420 | 421 | 注意,经过转换之后,`dataSource`常量的类型为`UITableViewDataSource`,所以只能访问和调用`UITableViewDataSource`协议定义的属性和方法。想进行其他操作时,必须将其转换为其他类型。 422 | 423 | 请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的 [协议](http://wiki.jikexueyuan.com/project/swift/chapter2/22_Protocols.html) 章节获取更多信息。 424 | 425 | 426 | ## 序列化 427 | 428 | 通过序列化,可以在应用程序中编码和解码对象,将其转换为独立于体系结构的数据形式,例如 JSON 或属性列表,并能从这类数据形式转换回对象。这类数据形式可以写入文件,传给其他本地进程,以及通过网络进行传递。 429 | 430 | 在 Objective-C 中,可以使用 Foundation 框架的 `NSJSONSerialiation` 类或 `NSPropertyListSerialization` 类从 JSON 或者属性列表来实例化对象,通常这种对象会是 `NSDictionary` 类型。 431 | 432 | 在 Swift 中,标准库定义了一套标准化方法来编码和解码数据。若要使用这套方法,你可以让自定义类型遵守 `Encodable` 或 `Decodable` 协议,若要同时遵守这两种协议,更便捷的方式是直接遵守 `Codable` 协议。你可以通过 Foundation 库中的 `JSONEncoder` 和 `PropertyListEncoder` 类将某个实例转化为 JSON 或属性列表数据。与此相似,你可以用 `JSONDecoder` 和 `PropertyListDecoder` 类从 JSON 或属性列表数据解码并初始化实例。 433 | 434 | 例如,一个应用从 web 服务器接收到一些表示食品杂货店商品的 JSON 数据,如下所示: 435 | 436 | ```swift 437 | { 438 | "name": "Banana", 439 | "points": 200, 440 | "description": "A banana grown in Ecuador.", 441 | "varieties": [ 442 | "yellow", 443 | "green", 444 | "brown" 445 | ] 446 | } 447 | ``` 448 | 449 | 如下代码演示了如何编写一个表示食品杂货店商品的 Swift 类型,该类型可以使用任何提供了编码器和解码器的序列化格式: 450 | 451 | ```swift 452 | struct GroceryProduct: Codable { 453 | let name: String 454 | let points: Int 455 | let description: String 456 | let varieties: [String] 457 | } 458 | ``` 459 | 460 | 你可以从 JSON 数据形式创建 `GroceryProduct` 实例,只需创建一个 `JSONDecoder` 实例,然后传入 `GroceryProduct.self` 类型以及相应的 JSON 数据: 461 | 462 | ```swift 463 | let json = """ 464 | { 465 | "name": "Banana", 466 | "points": 200, 467 | "description": "A banana grown in Ecuador.", 468 | "varieties": [ 469 | "yellow", 470 | "green", 471 | "brown" 472 | ] 473 | } 474 | """.data(using: .utf8)! 475 | 476 | let decoder = JSONDecoder() 477 | let banana = try decoder.decode(GroceryProduct.self, from: json) 478 | 479 | print("\(banana.name) (\(banana.points) points): \(banana.description)") 480 | // Prints "Banana (200 points): A banana grown in Ecuador." 481 | ``` 482 | 483 | 关于如何编码和解码更复杂的自定义类型的相关信息,请参阅 [Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)。关于如何编码和解码 JSON 的更多信息,请参阅 [Using JSON with Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types)。 484 | 485 | 486 | ## 本地化 487 | 488 | 在 Objective-C,通常用`NSLocalizedString`系列宏来本地化字符串。这套宏包括`NSLocalizedString`,`NSLocalizedStringFromTable`,`NSLocalizedStringFromTableInBundle`,以及`NSLocalizedStringWithDefaultValue`。而在 Swift,`NSLocalizedString(_:tableName:bundle:value:comment:)`函数就可以实现`NSLocalizedString`系列宏的这些功能。 489 | 490 | Swift 并没有为每个本地化宏单独定义函数,而是为`NSLocalizedString(_:tableName:bundle:value:comment:)`函数的`tableName`,`bundle`,`value`参数提供了默认值,以便可以在需要时重写它们。 491 | 492 | 例如,本地化字符串最常见的形式可能仅仅需要一个本地化键和一句注释: 493 | 494 | ```swift 495 | let format = NSLocalizedString("Hello, %@!", comment: "Hello, {given name}!") 496 | let name = "Mei" 497 | let greeting = String(format: format, arguments: [name as CVarArg]) 498 | print(greeting) 499 | // 打印 "Hello, Mei!" 500 | ``` 501 | 502 | 或者,为了使用单独的包来提供本地化资源,应用可能需要使用更复杂的本地化形式: 503 | 504 | ```swift 505 | if let path = Bundle.main.path(forResource: "Localization", ofType: "strings", inDirectory: nil, forLocalization: "ja"), 506 | let bundle = Bundle(path: path) { 507 | let translation = NSLocalizedString("Hello", bundle: bundle, comment: "") 508 | print(translation) 509 | } 510 | // 打印 "こんにちは" 511 | ``` 512 | 513 | 更多信息请参阅 [*Internationalization and Localization Guide*](https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPInternational/Introduction/Introduction.html#//apple_ref/doc/uid/10000171i)。 514 | 515 | 516 | ## 自动释放池 517 | 518 | 自动释放池块可以让对象放弃所有权而又不会被立即释放。通常,你不需要创建自己的自动释放池块,但是有的情况下则必须创建,例如生成次级线程时,还有一些时候则最好创建,例如通过循环创建大量临时的自动释放对象时。 519 | 520 | 在 Objective-C,自动释放池块使用`@autoreleasepool`标记。在 Swift,你可以使用`autoreleasepool(_:)`函数在自动释放池块中执行一个闭包。 521 | 522 | ```swift 523 | import Foundation 524 | 525 | autoreleasepool { 526 | // 创建自动释放对象的相关代码 527 | } 528 | ``` 529 | 530 | 更多信息请参阅 [*Advanced Memory Management Programming Guide.*](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011)。 531 | 532 | 533 | ## API 可用性 534 | 535 | 一些 API 并非在所有平台的所有版本中都可用。为了确保应用程序能够适应任何功能上的差异,就需要检查这些 API 的可用性。 536 | 537 | 在 Objective-C,使用`respondsToSelector:`和`instancesRespondToSelector:`方法检查一个类或者实例的方法是否可用。否则,调用方法可能会抛出`NSInvalidArgumentException`类型的`unrecognized selector sent to instance`异常。例如,`CLLocationManager`实例的`requestWhenInUseAuthorization`方法从`iOS 8.0`和`macOS10.10`开始才可用: 538 | 539 | ```objective-c 540 | if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)]) { 541 | // 方法可用 542 | } else { 543 | // 方法不可用 544 | } 545 | ``` 546 | 547 | 在 Swift,试图调用项目所支持的最低平台版本不支持的方法将会引发编译时错误。 548 | 549 | 如上例子在 Swift 中如下所示: 550 | 551 | ```swift 552 | let locationManager = CLLocationManager() 553 | locationManager.requestWhenInUseAuthorization() 554 | // error: only available on iOS 8.0 or newer 555 | ``` 556 | 557 | 如果应用程序在版本低于`iOS 8.0`或者`macOS 10.10`的平台上运行,那么`requestWhenInUseAuthorization()`方法将不可用,因此编译器会报告错误。 558 | 559 | Swift 代码可以使用 API 可用性在运行时作为判断条件。可用性检查可以作为控制流语句的一个条件,例如`if`,`guard`,`while`语句。 560 | 561 | 针对之前的例子,可以在`if`语句中检查可用性,当`requestWhenInUseAuthorization()`方法在运行时可用时才去调用: 562 | 563 | ```swift 564 | let locationManager = CLLocationManager() 565 | if #available(iOS 8.0, macOS 10.10, *) { 566 | locationManager.requestWhenInUseAuthorization() 567 | } 568 | ``` 569 | 570 | 或者,可以在`guard`语句中检查可用性,除非当前的平台版本符合指定要求,否则将退出作用域。这种方式简化了处理不同平台功能时的逻辑。 571 | 572 | ```swift 573 | let locationManager = CLLocationManager() 574 | guard #available(iOS 8.0, macOS 10.10, *) else { return } 575 | locationManager.requestWhenInUseAuthorization() 576 | ``` 577 | 578 | 每个平台参数由下面列出的平台名称组成,后面跟着相应的版本号。最后一个参数是一个星号(`*`),用来处理未来可能的平台。 579 | 580 | 平台名称: 581 | 582 | - iOS 583 | - iOSApplicationExtension 584 | - macOS 585 | - macOSApplicationExtension 586 | - watchOS 587 | - watchOSApplicationExtension 588 | - tvOS 589 | - tvOSApplicationExtension 590 | 591 | 所有的 Cocoa API 都提供了可用性信息,因此能确信代码可以在应用所支持的任何平台上如期工作。 592 | 593 | 可以用`@available`特性标注自己的 API 声明来指明其可用性。`@available`特性的语法和`#available`一样,以逗号分隔的参数提供平台版本要求。 594 | 595 | 例如: 596 | 597 | ```swift 598 | @available(iOS 8.0, macOS 10.10, *) 599 | func useShinyNewFeature() { 600 | // ... 601 | } 602 | ``` 603 | 604 | > 注意 605 | > 使用`@available`特性标记的方法可以安全地使用满足指定平台要求的 API 而不用再进行可用性检查。 606 | 607 | 608 | ## 处理命令行参数 609 | 610 | 在`macOS`,通常通过点击 Dock 或者 Launchpad 上的应用程序图标启动应用程序,也可以双击 Finder 中的应用程序图标。然而,也可以使用终端通过编程的方式打开应用程序,并可以为其传递一些命令行参数。 611 | 612 | 可以访问类型属性`CommandLine.arguments`获取应用程序启动时指定的一系列命令行参数。 613 | 614 | ``` 615 | $ /path/to/app --argumentName value 616 | ``` 617 | 618 | ```swift 619 | for argument in CommandLine.arguments { 620 | print(argument) 621 | } 622 | // 打印 /path/to/app 623 | // 打印 --argumentName 624 | // 打印 value 625 | ``` 626 | 627 | `CommandLine.arguments`的第一个元素总是可执行文件的路径。从`Process.arguments[1]`开始才是启动时指定的命令行参数。 628 | 629 | > 注意 630 | > 这等同于访问`ProcessInfo.processInfo`的`arguments`属性。 631 | -------------------------------------------------------------------------------- /02-Interoperability/05-Interacting with C APIs.md: -------------------------------------------------------------------------------- 1 | # 与 C 语言 API 交互 2 | 3 | - [基本类型](#primitive_types) 4 | - [全局常量](#global_constants) 5 | - [导入的常量枚举和结构体](#imported_constant_enumerations_and_structures) 6 | - [函数](#functions) 7 | - [变参函数](#variadic_functions) 8 | - [结构体](#structures) 9 | - [导入函数作为类型成员](#importing_functions_as_type_members) 10 | - [枚举](#enumerations) 11 | - [选项集](#option_sets) 12 | - [联合体](#unions) 13 | - [位字段](#bit_fields) 14 | - [匿名结构体和联合体字段](#unnamed_structure_and_union_fields) 15 | - [指针](#pointer) 16 | - [常量指针](#constant_pointers) 17 | - [可变指针](#mutable_pointers) 18 | - [自动释放指针](#autoreleasing_pointers) 19 | - [函数指针](#function_pointers) 20 | - [缓冲区指针](#buffer_pointers) 21 | - [空指针](#null_pointers) 22 | - [指针运算](#pointer_arithmetic) 23 | - [数据类型大小计算](#data_type_size_calculation) 24 | - [一次性初始化](#one_time_initialization) 25 | - [预处理指令](#preprocessor_directives) 26 | - [简单宏](#simple_macros) 27 | - [复杂宏](#complex_macros) 28 | - [条件编译块](#conditional_compilation_blocks) 29 | 30 | 为了更好地支持与 Objective-C 语言的互用性,Swift 对一些 C 语言的类型和特性保持了兼容性,提供了一些方式来使用常见的 C 语言设计和模式。 31 | 32 | 33 | ## 基本类型 34 | 35 | Swift 提供了一些与 C 语言基本数值类型(例如`char`,`int`,`float`,`double`)对应的类型。然而,这些类型和 Swift 基本数值类型(例如`Int`)之间不能进行隐式转换。因此,只应在必要时才使用这些类型,尽可能地使用`Int`这种原生 Swift 类型。 36 | 37 | | C 类型 | Swift 类型 | 38 | | ------ | ------ | 39 | | bool | CBool | 40 | | char, signed char | CChar | 41 | | unsigned char | CUnsignedChar | 42 | | short | CShort | 43 | | unsigned short | CUnsignedShort | 44 | | int | CInt | 45 | | unsigned int | CUnsignedInt | 46 | | long | CLong | 47 | | unsigned long | CUnsignedLong | 48 | | long long | CLongLong | 49 | | unsigned long long | CUnsignedLongLong | 50 | | wchar_t | CWideChar | 51 | | char16_t | CChar16 | 52 | | char32_t | CChar32 | 53 | | float | CFloat | 54 | | double | CDouble | 55 | 56 | 57 | ## 全局常量 58 | 59 | C 和 Objective-C 的源文件中定义的全局常量会自动被 Swift 编译器导入为 Swift 全局常量。 60 | 61 | 62 | ### 导入的常量枚举和结构体 63 | 64 | 在 Objective-C,常量通常用来为属性或者方法参数提供一系列合适的值。你可以用 `NS_TYPED_ENUM` 或 `NS_TYPED_EXTENSIBLE_ENUM` 宏标注 Objective-C `typedef` 声明,这样 Swift 就会将该类型导入为枚举或结构体,而该类型的各种常量会变成相应的类型成员。使用 `NS_TYPED_ENUM` 宏标注一套不会再扩充新值的常量声明。使用 `NS_TYPED_EXTENSIBLE_ENUM` 宏标注一套可以通过 Swift 扩展来扩充新值的常量声明。 65 | 66 | 表示一组固定值的常量声明在标注 `NS_TYPED_ENUM` 宏之后会以结构体形式导入到 Swift。例如,请考虑如下整形常量类型 `TrafficLightColor` 的 Objective-C 声明: 67 | 68 | ```objective-c 69 | typedef long TrafficLightColor NS_TYPED_ENUM; 70 | 71 | TrafficLightColor const TrafficLightColorRed; 72 | TrafficLightColor const TrafficLightColorYellow; 73 | TrafficLightColor const TrafficLightColorGreen; 74 | ``` 75 | 76 | Swift 会以如下形式导入它们: 77 | 78 | ```swift 79 | struct TrafficLightColor: RawRepresentable, Equatable, Hashable { 80 | typealias RawValue = Int 81 | 82 | init(rawValue: RawValue) 83 | var rawValue: RawValue { get } 84 | 85 | static var red: TrafficLightColor { get } 86 | static var yellow: TrafficLightColor { get } 87 | static var green: TrafficLightColor { get } 88 | } 89 | ``` 90 | 91 | 表示一套可扩充新值的常量的声明在标注 `NS_TYPED_EXTENSIBLE_ENUM` 宏之后也会作为结构体导入到 Swift。例如,考虑以下 Objective-C 声明,它们表示交通信号灯的颜色组合: 92 | 93 | ```objective-c 94 | typedef TrafficLightColor TrafficLightCombo [3] NS_TYPED_EXTENSIBLE_ENUM; 95 | 96 | TrafficLightCombo const TrafficLightComboJustRed; 97 | TrafficLightCombo const TrafficLightComboJustYellow; 98 | TrafficLightCombo const TrafficLightComboJustGreen; 99 | 100 | TrafficLightCombo const TrafficLightComboRedYellow; 101 | ``` 102 | 103 | Swift 会以如下形式导入它们: 104 | 105 | ```swift 106 | struct TrafficLightCombo: RawRepresentable, Equatable, Hashable { 107 | typealias RawValue = (TrafficLightColor, TrafficLightColor, TrafficLightColor) 108 | 109 | init(_ rawValue: RawValue) 110 | init(rawValue: RawValue) 111 | var rawValue: RawValue { get } 112 | 113 | static var justRed: TrafficLightCombo { get } 114 | static var justYellow: TrafficLightCombo { get } 115 | static var justGreen: TrafficLightCombo { get } 116 | static var redYellow: TrafficLightCombo { get } 117 | } 118 | ``` 119 | 120 | 可以看到,使用可扩充形式的常量声明在导入后会额外获得一个构造器,这使得调用者可以在扩充新值时省略参数标签。 121 | 122 | 标注 `NS_TYPED_EXTENSIBLE_ENUM` 宏的常量声明可在 Swift 代码中进行扩充以添加新值: 123 | 124 | ```swift 125 | extension TrafficLightCombo { 126 | static var all: TrafficLightCombo { 127 | return TrafficLightCombo((.red, .yellow, .green)) 128 | } 129 | } 130 | ``` 131 | 132 | > 注意 133 | > 你可能会遇到使用 `NS_STRING_ENUM` 和 `NS_EXTENSIBLE_STRING_ENUM` 旧版宏的 Objective-C 代码,这些宏用于组织字符串常量。组织任意类型的相关常量(包括字符串常量)时,请使用 `NS_TYPED_ENUM` 和 `NS_TYPED_EXTENSIBLE_ENUM`。 134 | 135 | 136 | ## 函数 137 | 138 | Swift 将 C 头文件中的所有函数声明导入为 Swift 全局函数。例如,思考如下 Objective-C 函数声明: 139 | 140 | ```objective-c 141 | int product(int multiplier, int multiplicand); 142 | int quotient(int dividend, int devisor, int *remainder); 143 | 144 | struct Point2D createPoint2D(float x, float y); 145 | float distance(struct Point2D from, struct Point2D to); 146 | ``` 147 | 148 | Swift 会以如下形式导入它们: 149 | 150 | ```swift 151 | func product(_ multiplier: Int32, _ multiplicand: Int32) -> Int32 152 | func quotient(_ dividend: Int32, _ devisor: Int32, _ remainder: UnsafeMutablePointer) -> Int32 153 | 154 | func createPoint2D(_ x: Float, _ y: Float) -> Point2D 155 | func distance(_ from: Point2D, _ to: Point2D) -> Float 156 | ``` 157 | 158 | 159 | ### 变参函数 160 | 161 | 在 Swift,可以调用 C 中的可变参数函数,例如`vasprintf`函数,调用这种函数需使用 `getVaList(_:)` 或 `withVaList(_:_:)` 函数。`getVaList(_:)`函数接受一个`CVarArgType`类型的数组,返回一个`CVaListPointer`类型的值。`withVaList(_:_:)`函数则会在闭包体中通过闭包参数来提供该值,而不是直接返回它。最终,`CVaListPointer`类型的值会传递给接受可变参数的 C 函数的`va_list`参数。 162 | 163 | 例如,如下示例代码演示了如何在 Swift 中调用`vasprintf`函数: 164 | 165 | ```swift 166 | func swiftprintf(format: String, arguments: CVarArg...) -> String? { 167 | return withVaList(arguments) { va_list in 168 | var buffer: UnsafeMutablePointer? = nil 169 | return format.withCString { CString in 170 | guard vasprintf(&buffer, CString, va_list) != 0 else { 171 | return nil 172 | } 173 | 174 | return String(validatingUTF8: buffer!) 175 | } 176 | } 177 | } 178 | print(swiftprintf(format: "√2 ≅ %g", arguments: sqrt(2.0))!) 179 | // 打印 "√2 ≅ 1.41421" 180 | ``` 181 | 182 | > 注意 183 | > 可选指针不能传递给`withVaList(_:invoke:)`函数。相反,使用`Int.init(bitPattern:)`构造器将可选指针转化为`Int`值,在所有支持的平台上,`Int`类型的 C 可变函数调用约定和指针类型一样。 184 | 185 | 186 | ## 结构体 187 | 188 | Swift 将 C 头文件中的所有结构体声明导入为 Swift 结构体。导入后的结构体会用存储属性表示结构体中的字段,并且有一个参数对应各个存储属性的构造器。如果所有被导入的结构体成员都有默认值,Swift 还会提供一个无参数的默认构造器。例如,思考如下 C 结构体声明: 189 | 190 | ```objective-c 191 | struct Color { 192 | float r, g, b; 193 | }; 194 | typedef struct Color Color; 195 | ``` 196 | 197 | 相对应的 Swift 结构体如下所示: 198 | 199 | ```swift 200 | public struct Color { 201 | var r: Float 202 | var g: Float 203 | var b: Float 204 | 205 | init() 206 | init(r: Float, g: Float, b: Float) 207 | } 208 | ``` 209 | 210 | 211 | ### 导入函数作为类型成员 212 | 213 | C API,例如 Core Foundation 框架,通常会提供一些函数用于创建、访问、修改结构体。你可以在自己的代码中使用`CF_SWIFT_NAME`宏来让 Swift 将这些 C 函数导入为相应结构体的成员函数。例如,对于如下 C 函数声明: 214 | 215 | ```objective-c 216 | Color ColorCreateWithCMYK(float c, float m, float y, float k) CF_SWIFT_NAME(Color.init(c:m:y:k:)); 217 | 218 | float ColorGetHue(Color color) CF_SWIFT_NAME(getter:Color.hue(self:)); 219 | void ColorSetHue(Color color, float hue) CF_SWIFT_NAME(setter:Color.hue(self:newValue:)); 220 | 221 | Color ColorDarkenColor(Color color, float amount) CF_SWIFT_NAME(Color.darken(self:amount:)); 222 | 223 | extern const Color ColorBondiBlue CF_SWIFT_NAME(Color.bondiBlue); 224 | 225 | Color ColorGetCalibrationColor(void) CF_SWIFT_NAME(getter:Color.calibration()); 226 | Color ColorSetCalibrationColor(Color color) CF_SWIFT_NAME(setter:Color.calibration(newValue:)); 227 | ``` 228 | 229 | Swift 会将其导入为结构体的成员函数: 230 | 231 | ```swift 232 | extension Color { 233 | init(c: Float, m: Float, y: Float, k: Float) 234 | 235 | var hue: Float { get set } 236 | 237 | func darken(amount: Float) -> Color 238 | 239 | static var bondiBlue: Color 240 | 241 | static var calibration: Color 242 | } 243 | ``` 244 | 245 | 传入`CF_SWIFT_NAME`宏的参数使用和`#selector`表达式相同的语法。实例方法对应的`CF_SWIFT_NAME`宏中的参数`self`引用着方法的调用者。 246 | 247 | > 注意 248 | > 你无法使用`CF_SWIFT_NAME`宏改变方法的参数顺序或参数数量,要实现该需求,只能提供一个新的 Swift 函数,并在函数实现中调用相关函数。 249 | 250 | 251 | ## 枚举 252 | 253 | Swift 会将用`NS_ENUM`宏标注的 C 语言枚举导入为原始值类型为`Int`的 Swift 枚举。无论是系统框架还是自己代码中的枚举,导入到 Swift 后,它们的前缀名均会被移除。 254 | 255 | 例如,一个 Objective-C 枚举的声明如下: 256 | 257 | ```objective-c 258 | typedef NS_ENUM(NSInteger, UITableViewCellStyle) { 259 | UITableViewCellStyleDefault, 260 | UITableViewCellStyleValue1, 261 | UITableViewCellStyleValue2, 262 | UITableViewCellStyleSubtitle 263 | }; 264 | ``` 265 | 266 | 在 Swift,它被导入为如下形式: 267 | 268 | ```swift 269 | enum UITableViewCellStyle: Int { 270 | case `default` 271 | case value1 272 | case value2 273 | case subtitle 274 | } 275 | ``` 276 | 277 | 需要使用一个枚举值时,使用以点(`.`)开头的枚举变量名: 278 | 279 | ```swift 280 | let cellStyle: UITableViewCellStyle = .default 281 | ``` 282 | 283 | > 注意 284 | > 对于导入到 Swift 的 C 枚举,使用原始值进行初始化时,即使原始值不匹配任何枚举值,初始化也不会失败。这是为了兼容 C 枚举的特性,即枚举可以存储任何值,包括一些只在内部使用而没有在头文件中暴露出来的值。 285 | 286 | Swift 会将未使用`NS_ENUM`或`NS_OPTIONS`宏标注的 C 语言枚举导入为结构体。每个枚举成员会被导入为属性类型是该结构体类型的全局只读计算型属性,而不是作为结构体本身的属性。 287 | 288 | 例如,下面是个未使用`NS_ENUM`宏声明的 C 语言枚举: 289 | 290 | ```objective-c 291 | typedef enum { 292 | MessageDispositionUnread = 0, 293 | MessageDispositionRead = 1, 294 | MessageDispositionDeleted = -1, 295 | } MessageDisposition; 296 | ``` 297 | 298 | 在 Swift,它被导入为如下形式: 299 | 300 | ```swift 301 | struct MessageDisposition: RawRepresentable, Equatable {} 302 | 303 | var MessageDispositionUnread: MessageDisposition { get } 304 | var MessageDispositionRead: MessageDisposition { get } 305 | var MessageDispositionDeleted: MessageDisposition { get } 306 | ``` 307 | 308 | 被导入到 Swift 的 C 语言枚举会自动遵守`Equatable`协议。 309 | 310 | 311 | ## 选项集 312 | 313 | Swfit 会将使用`NS_OPTIONS`宏标注的 C 语言枚举导入为 Swfit 选项集。选项集会像枚举一样把前缀移除,只剩下选项值名称。 314 | 315 | 例如,一个 Objective-C 选项集的声明如下: 316 | 317 | ```objective-c 318 | typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) { 319 | UIViewAutoresizingNone = 0, 320 | UIViewAutoresizingFlexibleLeftMargin = 1 << 0, 321 | UIViewAutoresizingFlexibleWidth = 1 << 1, 322 | UIViewAutoresizingFlexibleRightMargin = 1 << 2, 323 | UIViewAutoresizingFlexibleTopMargin = 1 << 3, 324 | UIViewAutoresizingFlexibleHeight = 1 << 4, 325 | UIViewAutoresizingFlexibleBottomMargin = 1 << 5 326 | }; 327 | ``` 328 | 329 | 在 Swift,它被导入为如下形式: 330 | 331 | ```swift 332 | public struct UIViewAutoresizing : OptionSet { 333 | public init(rawValue: UInt) 334 | 335 | public static var flexibleLeftMargin: UIViewAutoresizing { get } 336 | public static var flexibleWidth: UIViewAutoresizing { get } 337 | public static var flexibleRightMargin: UIViewAutoresizing { get } 338 | public static var flexibleTopMargin: UIViewAutoresizing { get } 339 | public static var flexibleHeight: UIViewAutoresizing { get } 340 | public static var flexibleBottomMargin: UIViewAutoresizing { get } 341 | } 342 | ``` 343 | 344 | 在 Objective-C,一个选项集是一些整数值的位掩码。可以使用按位或操作符(`|`)组合选项值,使用按位与操作符(`&`)检查选项值。可以使用常量值或者表达式创建一个选项集。一个空的选项集使用常数`0`表示。 345 | 346 | 在 Swift,选项集用一个遵守`OptionSet`协议的结构体表示,每个选项值都是结构体的一个静态变量。可以使用一个数组字面量创建一个选项集,访问选项值像枚举一样也用点(`.`)开头。创建空选项集时,可以使用一个空数组字面量,也可以调用默认构造器。 347 | 348 | > 注意 349 | > 在导入标注`NS_OPTIONS`宏的 C 枚举时,Swift 会忽略值为`0`的枚举成员,因为 Swift 会使用空选项集表示这种选项。 350 | 351 | 选项集类似于 Swift 的集合类型`Set`,可以用`insert(_:)`或者`formUnion(_:)`方法添加选项值,用`remove(_:)`或者`subtract(_:)`方法移除选项值,用`contains(_:)`方法检查选项值。 352 | 353 | ```swift 354 | let options: Data.Base64EncodingOptions = [ 355 | .lineLength64Characters, 356 | .endLineWithLineFeed 357 | ] 358 | let string = data.base64EncodedString(options: options) 359 | ``` 360 | 361 | 362 | ## 联合体 363 | 364 | Swift 将 C 联合体导入为 Swift 结构体。尽管 Swift 不支持联合体,但 C 联合体导入为 Swift 结构体后仍将保持类似 C 联合体的行为。例如,思考如下名为`SchroedingersCat`的 C 联合体,它拥有`isAlive`和`isDead`两个字段: 365 | 366 | ```swift 367 | union SchroedingersCat { 368 | bool isAlive; 369 | bool isDead; 370 | }; 371 | 372 | 它被导入到 Swift 后如下所示: 373 | 374 | struct SchroedingersCat { 375 | var isAlive: Bool { get set } 376 | var isDead: Bool { get set } 377 | 378 | init(isAlive: Bool) 379 | init(isDead: Bool) 380 | 381 | init() 382 | } 383 | ``` 384 | 385 | 由于 C 联合体所有字段共享同一块内存,因此联合体作为结构体导入到 Swift 后,所有计算属性也会共享同一块内存。这将导致修改任意计算属性的值都会改变其他计算属性的值。 386 | 387 | 在上述例子中,修改结构体`SchroedingersCat`的计算属性`isAlive`的值也会改变计算属性`isDead`的值: 388 | 389 | ```swift 390 | var mittens = SchroedingersCat(isAlive: false) 391 | 392 | print(mittens.isAlive, mittens.isDead) 393 | // 打印 "false false" 394 | 395 | mittens.isAlive = true 396 | print(mittens.isDead) 397 | // 打印 "true" 398 | ``` 399 | 400 | 401 | ## 位字段 402 | 403 | Swift 会将结构体中的位字段导入为结构体的计算型属性,例如 Foundation 中的`NSDecimal`类型。使用位字段相对应的计算属性时,Swift 会处理好这些值和其兼容的 Swift 类型之间的转换工作。 404 | 405 | 406 | ## 匿名结构体和联合体字段 407 | 408 | C `struct`和`union`类型既可以定义匿名字段,也可以定义具有匿名类型的字段。匿名字段由内部所嵌套的拥有命名字段的`struct`或`union`类型构成。 409 | 410 | 例如,在如下这个 C 结构体`Cake`中,`layers`和`height`两个字段嵌套在匿名`union`类型中,`toppings`字段则是一个匿名`struct`类型: 411 | 412 | ```objective-c 413 | struct Cake { 414 | union { 415 | int layers; 416 | double height; 417 | }; 418 | 419 | struct { 420 | bool icing; 421 | bool sprinkles; 422 | } toppings; 423 | }; 424 | ``` 425 | 426 | 导入到 Swift 后,可以像如下这样创建和使用它: 427 | 428 | ```swift 429 | var simpleCake = Cake() 430 | simpleCake.layers = 5 431 | print(simpleCake.toppings.icing) 432 | ``` 433 | 434 | `Cake`结构体被导入后会拥有一个逐一成员构造器,可以通过该构造器将结构体的字段初始化为自定义的值,就像下面这样: 435 | 436 | ```swift 437 | let cake = Cake( 438 | .init(layers: 2), 439 | toppings: .init(icing: true, sprinkles: false) 440 | ) 441 | 442 | print("The cake has \(cake.layers) layers.") 443 | // 打印 "The cake has 2 layers." 444 | print("Does it have sprinkles?", cake.toppings.sprinkles ? "Yes." : "No.") 445 | // 打印 "Does it have sprinkles? No." 446 | ``` 447 | 448 | 因为`Cake`结构体第一个字段是匿名的,因此构造器的第一个参数没有标签。由于`Cake`结构体的字段是匿名类型,因此使用`.init`构造器,这将借助类型推断来为结构体的每个匿名字段设置初始值。 449 | 450 | 451 | ## 指针 452 | 453 | Swift 尽可能地避免直接使用指针。不过,Swift 也提供了多种指针类型以供直接操作内存。如下表格使用`Type`作为类型名称的占位符来表示相应的映射语法。 454 | 455 | 对于返回类型,变量和参数,遵循如下映射: 456 | 457 | | C 语法 | Swift 语法 | 458 | | ------ | ------ | 459 | | const Type * | UnsafePointer\ | 460 | | Type * | UnsafeMutablePointer\ | 461 | 462 | 对于类类型,遵循如下映射: 463 | 464 | | C 语法 | Swift 语法 | 465 | | ------ | ------ | 466 | | Type * const * | UnsafePointer\ | 467 | | Type * __strong * | UnsafeMutablePointer\ | 468 | | Type ** | AutoreleasingUnsafeMutablePointer\ | 469 | 470 | 对于指向原始内存的无类型指针,遵循如下映射: 471 | 472 | | C 语法 | Swift 语法 | 473 | | ------ | ------ | 474 | | const void * | UnsafeRawPointer | 475 | | void * | UnsafeMutableRawPointer | 476 | 477 | Swift 还提供了用于操作缓冲区的指针类型,如[缓冲区指针](buffer_pointers)所述。 478 | 479 | 如果指针的类型在 Swift 中无法表示,例如某个不完备的结构体类型,Swift 会将之导入为`OpaquePointer`。 480 | 481 | 482 | ### 常量指针 483 | 484 | 如果函数接受`UnsafePointer`参数,那么该函数参数可以是下列任意一种类型: 485 | 486 | * `UnsafePointer`,`UnsafeMutablePointer`,`AutoreleasingUnsafeMutablePointer`类型的值,后两种类型会转换成`UnsafePointer`。 487 | * 一个`String`类型的值,如果`Type`是`Int8`或者`UInt8`。`String`类型的值会被自动转换为 UTF8 形式到一个缓冲区内,该缓冲区的指针会被传递给函数。 488 | * 一个左操作数为`Type`类型的`inout`表达式,左操作数的内存地址作为函数参数传入。 489 | * 一个`[Type]`类型的值,将作为该数组的起始指针传递给函数。 490 | 491 | 传递给函数的指针仅保证在函数调用期间内有效,不要试图保留指针并在函数返回之后继续使用。 492 | 493 | 如果定义了一个类似下面这样的函数: 494 | 495 | ```swift 496 | func takesAPointer(_ p: UnsafePointer) { 497 | // ... 498 | } 499 | ``` 500 | 501 | 那么可以通过以下任意一种方式来调用该函数: 502 | 503 | ```swift 504 | var x: Float = 0.0 505 | takesAPointer(&x) 506 | takesAPointer([1.0, 2.0, 3.0]) 507 | ``` 508 | 509 | 如果函数接受`UnsafeRawPointer`参数,那么该函数参数可以是任意类型的`UnsafePointer`。 510 | 511 | 如果定义了一个类似下面这样的函数: 512 | 513 | ```swift 514 | func takesAVoidPointer(_ p: UnsafeRawPointer?) { 515 | // ... 516 | } 517 | ``` 518 | 519 | 那么可以通过以下任意一种方式来调用该函数: 520 | 521 | ```swift 522 | var x: Float = 0.0, y: Int = 0 523 | takesAVoidPointer(&x) 524 | takesAVoidPointer(&y) 525 | takesAVoidPointer([1.0, 2.0, 3.0] as [Float]) 526 | let intArray = [1, 2, 3] 527 | takesAVoidPointer(intArray) 528 | ``` 529 | 530 | 531 | ### 可变指针 532 | 533 | 如果函数接受`UnsafeMutablePointer`参数,那么该函数参数可以是下列任意一种类型: 534 | 535 | * 一个`UnsafeMutablePointer`类型的值。 536 | * 一个左操作数为`Type`类型的`inout`表达式,左操作数的内存地址作为函数参数传入。 537 | * 一个`inout [Type]`类型的值,将作为该数组的起始指针传入,其生命周期会延续到本次调用结束。 538 | 539 | 如果定义了一个类似下面这样的函数: 540 | 541 | ```swift 542 | func takesAMutablePointer(_ p: UnsafeMutablePointer) { 543 | // ... 544 | } 545 | ``` 546 | 547 | 那么可以通过以下任意一种方式来调用该函数: 548 | 549 | ```swift 550 | var x: Float = 0.0 551 | var a: [Float] = [1.0, 2.0, 3.0] 552 | takesAMutablePointer(&x) 553 | takesAMutablePointer(&a) 554 | ``` 555 | 556 | 如果函数接受`UnsafeMutableRawPointer`参数,那么该函数参数可以是任意类型的`UnsafeMutablePointer`。 557 | 558 | 如果定义了一个类似下面这样的函数: 559 | 560 | ```swift 561 | func takesAMutableVoidPointer(_ p: UnsafeMutableRawPointer?) { 562 | // ... 563 | } 564 | ``` 565 | 566 | 那么可以通过以下任意一种方式来调用该函数: 567 | 568 | ```swift 569 | var x: Float = 0.0, y: Int = 0 570 | var a: [Float] = [1.0, 2.0, 3.0], b: [Int] = [1, 2, 3] 571 | takesAMutableVoidPointer(&x) 572 | takesAMutableVoidPointer(&y) 573 | takesAMutableVoidPointer(&a) 574 | takesAMutableVoidPointer(&b) 575 | ``` 576 | 577 | 578 | ### 自动释放指针 579 | 580 | 如果函数接受`AutoreleasingUnsafeMutablePointer`参数,那么该函数参数可以是下列任意一种类型: 581 | 582 | * 一个`AutoreleasingUnsafeMutablePointer`类型的值。 583 | * 一个`inout`表达式,其操作数首先被拷贝到一个无主临时缓冲区,缓冲区的地址会作为函数参数传入。函数调用结束时,读取并保留缓冲区中的值,然后重新赋值给操作数。 584 | 585 | 注意,上述列表中没有包含数组。 586 | 587 | 如果定义了一个类似下面这样的函数: 588 | 589 | ```swift 590 | func takesAnAutoreleasingPointer(_ p: AutoreleasingUnsafeMutablePointer) { 591 | // ... 592 | } 593 | ``` 594 | 595 | 那么可以通过以下方式来调用该函数: 596 | 597 | ```swift 598 | var x: NSDate? = nil 599 | takesAnAutoreleasingPointer(&x) 600 | ``` 601 | 602 | 指针指向的类型不会被桥接。例如,`NSString **`转换到 Swift 后,会是`AutoreleasingUnsafeMutablePointer`,而不是`AutoreleasingUnsafeMutablePointer`。 603 | 604 | 605 | ### 函数指针 606 | 607 | Swift 将 C 函数指针导入为沿用其调用约定的闭包,使用`@convention(c)`特性来表示。例如,一个类型为`int (*)(void)`的 C 函数指针,会以`@convention(c) () -> Int32`的形式导入到 Swift。 608 | 609 | 调用一个以函数指针为参数的函数时,可以传递一个顶级的 Swift 函数作为其参数,也可以传递闭包字面量,或者`nil`。也可以传递一个泛型类型的闭包属性或者泛型方法,只要闭包的参数列表或闭包体内没有引用泛型类型的参数。例如,Core Foundation 的`CFArrayCreateMutable(_:_:_:)`函数接受一个`CFArrayCallBacks`结构体作为参数,这个`CFArrayCallBacks`结构体使用一些函数指针进行初始化: 610 | 611 | ```swift 612 | func customCopyDescription(_ p: UnsafeRawPointer?) -> Unmanaged? { 613 | // 返回一个 Unmanaged? 值 614 | } 615 | 616 | var callbacks = CFArrayCallBacks( 617 | version: 0, 618 | retain: nil, 619 | release: nil, 620 | copyDescription: customCopyDescription, 621 | equal: { (p1, p2) -> DarwinBoolean in 622 | // 返回布尔值 623 | } 624 | ) 625 | 626 | var mutableArray = CFArrayCreateMutable(nil, 0, &callbacks) 627 | ``` 628 | 629 | 上面的例子中,结构体`CFArrayCallBacks`的构造器使用`nil`作为参数`retain`和`release`的值,使用函数`customCopyDescription`作为参数`copyDescription`的值,最后使用一个闭包字面量作为参数`equal`的值。 630 | 631 | > 注意 632 | > 只有使用 C 函数指针调用约定的 Swift 函数才可以作为函数指针参数。如同 C 函数指针,使用`@convention(c)`特性的 Swift 函数同样不具有捕获周围作用域上下文的能力。 633 | 634 | 更多信息请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 的[类型特性](http://wiki.jikexueyuan.com/project/swift/chapter3/06_Attributes.html#type_attributes)小节。 635 | 636 | 637 | ### 缓冲区指针 638 | 639 | 可以利用缓冲区指针以底层方式访问内存区域。例如,你可以使用缓冲区指针来高效地在应用程序与服务器之间通讯和处理数据。 640 | 641 | Swift 有如下缓冲区指针类型: 642 | 643 | - `UnsafeBufferPointer` 644 | - `UnsafeMutableBufferPointer` 645 | - `UnsafeRawBufferPointer` 646 | - `UnsafeMutableRawBufferPointer` 647 | 648 | `UnsafeBufferPointer`和`UnsafeMutableBufferPointer`是类型化的缓冲区指针类型,能让你将一块连续的内存作为集合来访问或修改,每一个元素都是缓冲区类型的`Element`泛型类型参数所表示的类型的实例。 649 | 650 | `UnsafeRawBufferPointer`和`UnsafeMutableRawBufferPointer`是原始缓冲区指针类型,能让你将一块连续的内存作为`UInt8`值的集合来访问或修改,每个元素都对应着一字节的内存。这些类型让你能使用底层编程模式,例如抛弃编译器类型检查带来的安全性,直接操作原始内存,或者将同一块内存转换为各种不同的类型。 651 | 652 | 653 | ### 空指针 654 | 655 | 在 Objective-C,指针类型声明可以使用`_Nullable`或`_Nonnull`来标注其值是否可以为`nil`或`NULL`。在 Swift,空指针通过值为`nil`的可选指针类型表示。通过整数表示的内存地址创建指针类型的构造器是可失败构造器。此外,不能将`nil`赋值给非可选指针类型。 656 | 657 | 如下表格阐明了映射关系: 658 | 659 | | Objective-C 语法 | Swift 语法 | 660 | | ------ | ------ | 661 | | const Type * _Nonnull | UnsafePointer\ | 662 | | const Type * _Nullable | UnsafeMutablePointer\? | 663 | | const Type * \_Null_unspecified | UnsafeMutablePointer\! | 664 | 665 | > 注意 666 | > 在 Swift 3 之前,可空指针和不可空指针都表示为非可选不安全指针类型。在将代码迁移到最新版本 Swift 的过程中,你需要将通过`nil`字面量初始化的指针标记为可选类型。 667 | 668 | 669 | ### 指针运算 670 | 671 | 使用不透明的数据类型时,你可能需要执行不安全的指针操作。你可以对 Swift 指针值使用算术运算符来创建位于指定偏移位置的新指针。 672 | 673 | ```swift 674 | let pointer: UnsafePointer 675 | let offsetPointer = pointer + 24 676 | // offsetPointer 是个前进了 24 的新指针 677 | ``` 678 | 679 | 680 | ## 数据类型大小计算 681 | 682 | 在 C 语言,`sizeof`和`alignof`运算符会返回任意变量或数据类型的大小和对齐。在 Swift,可以使用`MemoryLayout`通过其`size`,`stride`,`alignment`属性来获取`T`类型的内存布局信息。例如,获取 Darwin 中的`timeval`结构体的内存布局信息: 683 | 684 | ```swift 685 | print(MemoryLayout.size) 686 | // 打印 "16" 687 | print(MemoryLayout.stride) 688 | // 打印 "16" 689 | print(MemoryLayout.alignment) 690 | // 打印 "8" 691 | ``` 692 | 693 | 在 Swift 中使用接受某个类型或值的大小作为参数的 C 函数时就会用到这些信息。例如,`setsockopt(_:_:_:_:_:)`函数可以为套接字指定一个`timeval`值作为接收超时选项(`SO_RCVTIMEO`),需要传入`timeval`值的指针及其长度: 694 | 695 | ```swift 696 | let sockfd = socket(AF_INET, SOCK_STREAM, 0) 697 | var optval = timeval(tv_sec: 30, tv_usec: 0) 698 | let optlen = socklen_t(MemoryLayout.size) 699 | if setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &optval, optlen) == 0 { 700 | // ... 701 | } 702 | ``` 703 | 704 | 更多信息请参阅 [MemoryLayout API reference](https://developer.apple.com/reference/swift/memorylayout)。 705 | 706 | 707 | ## 一次性初始化 708 | 709 | 在 C 语言中,POSIX 的`pthread_once()`函数,以及 GCD 的`dispatch_once()`和`dispatch_once_f()`函数,都能提供一种机制来保证初始化代码只会执行一次。在 Swift,全局常量和存储型类型属性可以确保相应的初始化过程只执行一次,即使从多个线程同时进行访问。Swift 从语言特性层面提供了这一功能,因此并未暴露相应的 POSIX 以及 GCD 函数的调用过程。 710 | 711 | 712 | ## 预处理指令 713 | 714 | Swift 编译器不包含预处理器,它能利用编译期特性,条件编译块,以及语言特性来实现相同的功能。因此,预处理指令不会导入到 Swift。 715 | 716 | 717 | ### 简单宏 718 | 719 | 在 C 和 Objective-C,通常使用`#define`指令来定义一个简单的常量。在 Swift,可以使用全局常量来代替。例如,对于常量定义`#define FADE_ANIMATION_DURATION 0.35`,在 Swift 可以表示为`let FADE_ANIMATION_DURATION = 0.35`。由于定义常量的简单宏会被直接映射成 Swift 全局常量,因此 Swift 编译器会自动导入 C 或 Objective-C 源文件中定义的简单宏。 720 | 721 | 722 | ### 复杂宏 723 | 724 | C 和 Objective-C 的复杂宏在 Swift 中没有相对应的东西。复杂宏是那些不是用来定义常量的宏,包括带括号的函数式宏。在 C 和 Objective-C 可以使用复杂宏来避开类型检查限制或避免重复键入大量样板代码。然而,宏也会让调试和重构变得更加困难。在 Swift,可以使用函数和泛型达到同样效果。因此,C 和 Objective-C 源文件中定义的复杂宏无法在 Swift 使用。 725 | 726 | 727 | ### 条件编译块 728 | 729 | 区别于 C 以及 Objective-C,Swift 代码以不同的方式进行条件编译。Swift 可以使用条件编译块来进行条件编译。例如,如果你使用 `swift -D DEBUG_LOGGING` 设置了条件编译标志 `DEBUG_LOGGING`,那么编译器就会编译条件编译块中的代码,如下所示: 730 | 731 | ```swift 732 | #if DEBUG_LOGGING 733 | print("Flag enabled.") 734 | #endif 735 | ``` 736 | 737 | 编译条件可以包含 `true` 和 `false` 字面值,自定义的条件编译标志(使用 `-D <#flag#>` 来指定),以及下表中列出的平台条件。 738 | 739 | | 平台条件 | 有效参数 | 740 | | --- | --- | 741 | | `os()` | `macOS`, `iOS`, `watchOS`, `tvOS`, `Linux` | 742 | | `arch()` | `i386`, `x86_64`, `arm`, `arm64` | 743 | | `swift()` | `>=`紧接版本号 | 744 | | `canImport()` | 模块名 | 745 | | `targetEnvironment()` | `simulator` | 746 | 747 | 关于平台条件的更多信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift) 中的[条件编译块](http://wiki.jikexueyuan.com/project/swift/chapter3/10_Statements.html#build_config_statements)部分。 748 | 749 | > 注意 750 | > 平台条件 `arch(arm)` 在 ARM 64 位的设备上不会返回 `true`。`arch(i386)` 在32位的 iOS 模拟器上会返回 `true`。 751 | 752 | 你可以使用 `&&` 和 `||` 运算符来组合编译条件,使用 `!` 运算符来对编译条件取反,使用 `#elseif` 和 `#else` 条件编译指令添加新的条件编译分支。你还可以在条件编译块中嵌套其他条件编译块。 753 | 754 | ```swift 755 | #if arch(arm) || arch(arm64) 756 | #if swift(>=3.0) 757 | print("Using Swift 3 ARM code") 758 | #else 759 | print("Using Swift 2.2 ARM code") 760 | #endif 761 | #elseif arch(x86_64) 762 | print("Using 64-bit x86 code.) 763 | #else 764 | print("Using general code.") 765 | #endif 766 | ``` 767 | 768 | 与 C 语言预处理器的条件编译不同的是,Swift 的条件编译块中的语句必须是独立且语法有效的代码,因为所有的 Swift 代码都会进行语法检查,即使某些代码不会被编译。不过也有例外情况,就是当编译条件中包含 `swift()` 平台条件时:只有编译器的 Swift 版本与平台条件中指定的 Swift 版本一致时,条件编译块中的代码才会被解析。这个例外能确保旧版本编译器不会去试图解析新版本的 Swift 语法。 769 | -------------------------------------------------------------------------------- /02-Interoperability/Attributes Inspector@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/02-Interoperability/Attributes Inspector@2x.png -------------------------------------------------------------------------------- /02-Interoperability/Identity Inspector@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/02-Interoperability/Identity Inspector@2x.png -------------------------------------------------------------------------------- /02-Interoperability/coredataeditor_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/02-Interoperability/coredataeditor_2x.png -------------------------------------------------------------------------------- /02-Interoperability/coredatanamespace_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/02-Interoperability/coredatanamespace_2x.png -------------------------------------------------------------------------------- /03-Mix and Match/DAG_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/03-Mix and Match/DAG_2x.png -------------------------------------------------------------------------------- /03-Mix and Match/Swift and Objective-C in the Same Project.md: -------------------------------------------------------------------------------- 1 | # 在项目中同时使用 Swift 和 Objective-C 2 | 3 | - [混搭概述](#mix_and_match_overview) 4 | - [在应用程序 target 中导入代码](#importing_code_from_within_the_same_app_target) 5 | - [将 Objective-C 代码导入到 Swift](#importing_objective_c_into_swift) 6 | - [将 Swift 代码导入到 Objective-C](#importing_swift_into_objective_c) 7 | - [在框架 target 中导入代码](#importing_code_from_within_the_same_framework_target) 8 | - [将 Objective-C 代码导入到 Swift](#importing_objective_c_into_swift_2) 9 | - [将 Swift 代码导入到 Objective-C](#importing_swift_into_objective_c_2) 10 | - [导入外部框架](#importing_external_frameworks) 11 | - [在 Objective-C 中使用 Swift](#using_swift_from_objective-c) 12 | - [在 Objective-C 头文件中引用 Swift 类或协议](#referencing_a_swift_class_or_protocol_in_an_objective_c_header) 13 | - [声明可被 Objective-C 类遵守的 Swift 协议](#declaring_a_swift_protocol_that_can_be_adopted_by_an_objective_c_class) 14 | - [在 Objective-C 的实现文件中采用 Swift 协议](#adopting_a_swift_protocol_in_an_objective_c_implementation) 15 | - [声明可在 Objective-C 中使用的 Swift 错误类型](#declaring_a_swift_error_type_that_can_be_used_from_objective_c) 16 | - [为 Objective-C 接口提供 Swift 命名](#overriding_swift_names_for_objective_c_interfaces) 17 | - [类工厂方法](#class_factory_methods) 18 | - [枚举](#enumerations) 19 | - [让 Objective-C 接口在 Swift 中不可用](#making_objective_c_interfaces_unavailable_in_swift) 20 | - [优化 Objective-C 声明](#refining_objective_c_declarations) 21 | - [将可用性信息添加到 Objective-C API](#adding_availability_information_to_objective_c_apis) 22 | - [为产品模块命名](#naming_your_product_module) 23 | - [故障排除贴士](#troubleshooting_tips_and_reminders) 24 | 25 | 由于 Swift 与 Objective-C 的兼容性,你可以在项目中同时使用两种语言,开发基于混合语言的应用程序。利用这种特性,你可以用 Swift 的最新语言特性实现应用程序的部分功能,并无缝并入现有的 Objective-C 代码中。 26 | 27 | 28 | ## 混搭概述 29 | 30 | Objective-C 和 Swift 文件可以在项目中并存,无论这个项目原本是基于 Objective-C 还是 Swift。还可以直接往现有项目中添加另一种语言的源文件。这种自然的工作流程使得创建基于混合语言的应用程序或框架变得和单独使用一种语言一样简单。 31 | 32 | 基于混合语言编写应用程序或框架的过程还是有些区别的。下图展示了同时使用两种语言时,在同一 target 中导入代码的基本原理,后续小节会介绍更多细节。 33 | 34 | ![](DAG_2x.png) 35 | 36 | 37 | ## 在应用程序 target 中导入代码 38 | 39 | 在编写基于混合语言的应用程序时,可能需要在 Swift 代码中使用 Objective-C 代码,或者反过来。下面描述的流程适用于非框架类型的 target 。 40 | 41 | 42 | ### 将 Objective-C 代码导入到 Swift 43 | 44 | 在应用程序 target 中导入一系列 Objective-C 文件供 Swift 代码使用时,需要依靠 Objective-C 桥接头文件将这些文件暴露给 Swift。添加 Swift 文件到现有的 Objective-C 项目时(或者反过来),Xcode 会自动创建 Objective-C 桥接头文件。 45 | 46 | ![](bridgingheader_2x.png) 47 | 48 | 如果选择创建,Xcode 会随着源文件的创建生成 Objective-C 桥接头文件,并用产品模块名拼接上`"-Bridging-Header.h"`作为 Objective-C 桥接头文件的文件名。关于产品模块名的具体介绍,请参阅[为产品模块命名](#naming_your_product_module)小节。 49 | 50 | 或者,可以选择`File > New > File > (iOS,watchOS,tvOS,macOS) > Source > Header File`来手动创建 Objective-C 桥接头文件。 51 | 52 | 你可以编辑这个 Objective-C 桥接头文件将 Objective-C API 暴露给 Swift。 53 | 54 | ##### 在 target 中将 Objective-C 代码导入到 Swift 55 | 56 | 1. 在 Objective-C 桥接头文件中,导入希望暴露给 Swift 的 Objective-C 头文件。例如: 57 | 58 | ```objective-c 59 | #import "XYZCustomCell.h" 60 | #import "XYZCustomView.h" 61 | #import "XYZCustomViewController.h" 62 | ``` 63 | 64 | 2. 确保在`Build Settings > Swfit Compiler - Code Generation > Objective-C Bridging Header`中设置了 Objective-C 桥接头文件的路径。该路径相对于项目,类似`Info.plist`在`Build Settings`中指定的路径。Xcode 自动生成 Objective-C 桥接头文件时会自动设置该路径,因此大多数情况下你不需要专门去设置它。 65 | 66 | 在 Objective-C 桥接头文件中导入的所有 Objective-C 头文件都会暴露给 Swift。target 中所有 Swift 文件都可以使用这些 Objective-C 头文件中的内容,不需要任何导入语句。不但如此,你还能用 Swift 语法使用这些自定义的 Objective-C 代码,就像使用系统的 Swift 类一样。 67 | 68 | ```swift 69 | let myCell = XYZCustomCell() 70 | myCell.subtitle = "A custom cell" 71 | ``` 72 | 73 | 74 | ### 将 Swift 代码导入到 Objective-C 75 | 76 | 将 Swift 代码导入到 Objective-C 时,需要依靠 Xcode 自动生成的头文件将这些 Swift 文件暴露给 Objective-C(此头文件不是上小节描述的 Objective-C 桥接头文件,该头文件在项目目录下不可见,但是可以跳转进去查看)。这个自动生成的头文件是一个 Objective-C 头文件,声明了 target 中的一些 Swift 接口。可以将这个 Objective-C 头文件看作 Swift 代码的保护伞头文件。该头文件以产品模块名拼接上`"-Swift.h"`来命名。关于产品模块名的具体介绍,请参阅[为产品模块命名](#naming_your_product_module)小节。 77 | 78 | 默认情况下,这个头文件包含所有标记`public`或`open`修饰符的 Swift 声明。如果 target 中有 Objective-C 桥接头文件的话,它还会包含标记`internal`修饰符的 Swift 声明。标记`private`或`fileprivate`修饰符的 Swift 声明不会出现在这个头文件中。私有声明不会暴露给 Objective-C,除非它们被标记`@IBAction`,`@IBOutlet`,`@objc`。如果应用程序的 target 启用了单元测试,在单元测试的 target 中导入应用程序的 target 时,在`import`语句前加上`@testable`特性,就可以在单元测试的 target 中访问应用程序的 target 中任何标记`internal`修饰符的声明,犹如它们标记了`public`修饰符一般。 79 | 80 | 关于访问级别修饰符的更多信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的[访问控制](http://wiki.jikexueyuan.com/project/swift/chapter2/24_Access_Control.html)章节。 81 | 82 | 这个头文件会由 Xcode 自动生成,可直接导入到 Objective-C 代码中使用。注意,如果这个头文件中的 Swift 接口使用了自定义的 Objective-C 类型,在导入这个自动生成的头文件前,必须先将相关的自定义 Objective-C 类型对应的 Objective-C 头文件导入。 83 | 84 | ##### 在 target 中将 Swift 代码导入到 Objective-C 85 | 86 | - 在 target 中的 Objective-C `.m`文件中,用如下语法导入 Swift 代码: 87 | 88 | ```objective-c 89 | #import "ProductModuleName-Swift.h" // 将 ProductModuleName 替换为产品模块名 90 | ``` 91 | 92 | target 中的一些 Swift 声明会暴露给包含这个导入语句的 Objective-C `.m`文件。关于如何在 Objective-C 中使用 Swift,请参阅[在 Objective-C 中使用 Swift](#using_swift_from_objective-c)小节。 93 | 94 | | | 导入到 Swift | 导入到 Objective-C | 95 | | :-------------:|:-----------:|:------------:| 96 | | Swift 代码 | 不需要导入语句 | #import "ProductModuleName-Swift.h" | 97 | | Objective-C 代码 | 不需要导入语句;需要 Objective-C 桥接头文件 | #import "Header.h" | 98 | 99 | 100 | ## 在框架 target 中导入代码 101 | 102 | 在编写基于混合语言的框架时,往往需要在 Swift 代码中访问 Objective-C 代码,或者反过来。 103 | 104 | 105 | ### 将 Objective-C 代码导入到 Swift 106 | 107 | 若要将 Objective-C 文件导入到 target 中的 Swift 代码中,需要将这些 Objective-C 文件导入到 Objective-C 的保护伞头文件中。 108 | 109 | > 译者注 110 | > 此处的“保护伞头文件”指的是带有框架版本号和版本字符串声明的那个头文件。 111 | 112 | ##### 在 target 中将 Objective-C 代码导入到 Swift 113 | 114 | 1. 确保将 target 的`Build Settings > Packaging > Defines Module`设置为`Yes`。 115 | 116 | 2. 在保护伞头文件中导入希望暴露给 Swift 的 Objective-C 头文件。例如: 117 | 118 | ```objective-c 119 | #import 120 | #import 121 | #import 122 | ``` 123 | 124 | 保护伞头文件中导入的 Objective-C 头文件都会暴露给 Swift。target 中所有 Swift 文件都可以使用这些 Objective-C 头文件中的内容,不需要任何导入语句。不但如此,你还能用 Swift 语法使用这些自定义的 Objective-C 代码,就像使用系统的 Swift 类一样。 125 | 126 | ```swift 127 | let myOtherCell = XYZCustomCell() 128 | myOtherCell.subtitle = "Another custom cell" 129 | ``` 130 | 131 | 132 | ### 将 Swift 代码导入到 Objective-C 133 | 134 | 若要将 Swift 文件导入到 target 中的 Objective-C 代码中,不需要导入任何东西到保护伞头文件,只需将 Xcode 为 Swift 代码自动生成的头文件导入到要使用 Swift 代码的 Objective-C `.m`文件。 135 | 136 | 由于这个自动生成的头文件是框架公共接口的一部分,因此只有标记`public`或`open`修饰符的 Swift 声明才会出现在这个自动生成的头文件中。 137 | 138 | 对于继承自 Objective-C 类的 Swift 子类中标记`internal`修饰符的方法和属性,它们只会暴露给 Objective-C 运行时系统,这意味着它们不会出现在 Swift 代码的头文件中,也无法在编译期访问它们。 139 | 140 | > 译者注 141 | > 使用这些标记`internal`的 Swift 声明时,编译器会报错提示符号未定义,研究了一下发现可以采取如下方式解决: 142 | > 143 | ```swift 144 | // Swift 代码部分 145 | @objc(Foo) 146 | class Foo: NSObject { 147 | func hello() { print("Hello!") } 148 | } 149 | ``` 150 | > 151 | ```objective-c 152 | // Objective-C 代码部分 153 | // 为了解决符号未定义的错误,手动写个接口声明,注意 Swift 代码中的 @objc(Foo),否则会导致类名不匹配 154 | @interface Foo : NSObject 155 | - (void)hello; 156 | @end 157 | ``` 158 | 159 | 关于访问级别修饰符的更多信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的[访问控制](http://wiki.jikexueyuan.com/project/swift/chapter2/24_Access_Control.html)章节。 160 | 161 | ##### 在 target 中将 Swift 代码导入到 Objective-C 162 | 163 | 1. 确保将 target 的`Build Settings > Packaging > Defines Module`设置为`Yes`。 164 | 165 | 2. 使用如下语法将 Swift 代码导入到 target 中的 Objective-C `.m`文件: 166 | 167 | ```objective-c 168 | // 分别用产品名和产品模块名替换 ProductName 和 ProductModuleName 169 | #import 170 | ``` 171 | 172 | target 中的一些 Swift 声明会暴露给包含这个导入语句的 Objective-C `.m`文件。关于如何在 Objective-C 中使用 Swift,请参阅[在 Objective-C 中使用 Swift](#using_swift_from_objective-c)小节。 173 | 174 | | | 导入到 Swift | 导入到 Objective-C | 175 | | :-------------:|:-----------:|:------------:| 176 | | Swift 代码 | 不需要导入语句 | #import | 177 | | Objective-C 代码 | 不需要导入语句;需要 Objective-C 保护伞头文件 | #import "Header.h" | 178 | 179 | 180 | 181 | ## 导入外部框架 182 | 183 | 你可以导入位于其他 target 中的外部框架,无论它是基于 Objective-C,Swift,还是基于混合语言的。导入流程都是一样的,只需确保被导入框架的`Build Setting > Pakaging > Defines Module`设置为`Yes`。 184 | 185 | 使用如下语法将外部框架导入到 Swift 文件: 186 | 187 | ```swift 188 | import FrameworkName 189 | ``` 190 | 191 | 使用如下语法将外部框架导入到 Objective-C `.m`文件: 192 | 193 | ```objective-c 194 | @import FrameworkName; 195 | ``` 196 | 197 | | | 导入到 Swift | 导入到 Objective-C | 198 | | :-------------:|:-----------:|:------------:| 199 | | 任意语言框架 | import FrameworkName | @import FrameworkName; | 200 | 201 | 202 | ## 在 Objective-C 中使用 Swift 203 | 204 | 将 Swift 代码导入到 Objective-C 后,就可用标准 Objective-C 语法使用 Swift 类。 205 | 206 | ```objective-c 207 | MySwiftClass *swiftObject = [[MySwiftClass alloc] init]; 208 | [swiftObject swiftMethod]; 209 | ``` 210 | 211 | 只有继承自 Objective-C 类的 Swift 类才能在 Objective-C 中使用。想要了解 Swift 如何将接口导入到 Objective-C 以及在 Objective-C 中能使用哪些 Swift 特性,请参阅[Swift 类型兼容性](../02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md#swift_type_compatibility)小节。 212 | 213 | 214 | ### 在 Objective-C 头文件中引用 Swift 类或协议 215 | 216 | 为避免循环引用,不要将 Swift 代码导入到 Objective-C 头文件(`.h`文件),而应该使用前向声明来引用一个 Swift 类或者协议。 217 | 218 | ```objective-c 219 | // MyObjcClass.h 220 | @class MySwiftClass; 221 | @protocol MySwiftProtocol; 222 | 223 | @interface MyObjcClass : NSObject 224 | - (MySwiftClass *)returnSwiftClassInstance; 225 | - (id )returnInstanceAdoptingSwiftProtocol; 226 | // ... 227 | @end 228 | ``` 229 | 230 | Swift 类和协议的前向声明只能用于声明方法和属性。 231 | 232 | 233 | ### 声明可被 Objective-C 类遵守的 Swift 协议 234 | 235 | 将 Swift 协议标记`@objc`特性,从而让 Objective-C 类可以遵守该协议。 236 | 237 | ```swift 238 | @objc public protocol MySwiftProtocol { 239 | func requiredMethod() 240 | 241 | @objc optional func optionalMethod() 242 | } 243 | ``` 244 | 245 | 为了遵守协议,Objective-C 类必须实现协议中声明的所有构造器,属性,下标,方法。可选的协议要求必须标记`@objc`特性和`optional`修饰符。 246 | 247 | 248 | ### 在 Objective-C 的实现文件中采用 Swift 协议 249 | 250 | Objective-C 类可以在实现文件(`.m`文件)中导入 Xcode 自动生成的 Swift 头文件,然后使用类扩展来采用 Swift 协议。 251 | 252 | ```objective-c 253 | // MyObjcClass.m 254 | #import "ProductModuleName-Swift.h" 255 | 256 | @interface MyObjcClass () 257 | // ... 258 | @end 259 | 260 | @implementation MyObjcClass 261 | // ... 262 | @end 263 | ``` 264 | 265 | 266 | ### 声明可在 Objective-C 中使用的 Swift 错误类型 267 | 268 | 遵守`Error`协议并标记`@objc`特性的 Swift 枚举会在 Swift 头文件中生成一个`NS_ENUM`枚举声明,并会为错误域生成相应的`NSString`字符串常量。例如,有如下 Swift 枚举声明: 269 | 270 | ```swift 271 | @objc public enum CustomError: Int, Error { 272 | case a, b, c 273 | } 274 | ``` 275 | 276 | Swift 头文件中相应的 Objective-C 声明如下所示: 277 | 278 | ```objective-c 279 | // Project-Swift.h 280 | typedef SWIFT_ENUM(NSInteger, CustomError) { 281 | CustomErrorA = 0, 282 | CustomErrorB = 1, 283 | CustomErrorC = 2, 284 | }; 285 | static NSString * const CustomErrorDomain = @"Project.CustomError"; 286 | ``` 287 | 288 | 289 | ## 为 Objective-C 接口提供 Swift 命名 290 | 291 | Swift 编译器自动将 Objective-C 代码作为常规 Swift 代码导入,它将 Objective-C 类工厂方法导入为 Swift 构造器,还会缩短 Objective-C 枚举值的命名。 292 | 293 | 代码中也许会存在无法自动处理的特殊情况。对于 Objective-C 方法,枚举值,或者选项集的值,可以使用`NS_SWIFT_NAME`宏来自定义它们导入到 Swift 后的命名。 294 | 295 | 296 | ### 类工厂方法 297 | 298 | 如果 Swift 编译器无法识别类工厂方法,可以使用`NS_SWIFT_NAME`宏来指定类工厂方法导入为 Swift 构造器后的方法名,从而能够将其正确导入。例如: 299 | 300 | ```objective-c 301 | + (instancetype)recordWithRPM:(NSUInteger)RPM NS_SWIFT_NAME(init(RPM:)); 302 | ``` 303 | 304 | 如果 Swift 编译器错误地将一个普通的类方法识别为类工厂方法,可以使用`NS_SWIFT_NAME`宏来指定类方法导入为 Swift 类方法后的方法名,从而能够将其正确导入。例如: 305 | 306 | ```objective-c 307 | + (id)recordWithQuality:(double)quality NS_SWIFT_NAME(record(quality:)); 308 | ``` 309 | 310 | 311 | ### 枚举 312 | 313 | 默认情况下,Swift 导入枚举时,会将枚举值的名称前缀截断。如果需要自定义枚举值的名称,可以使用`NS_SWIFT_NAME`宏来指定枚举值导入到 Swift 后的命名。例如: 314 | 315 | ```objective-c 316 | typedef NS_ENUM(NSInteger, ABCRecordSide) { 317 | ABCRecordSideA, 318 | ABCRecordSideB NS_SWIFT_NAME(FlipSide), 319 | }; 320 | ``` 321 | 322 | 323 | ## 让 Objective-C 接口在 Swift 中不可用 324 | 325 | 一些 Objective-C 接口可能不适合或者没必要暴露给 Swift,为了防止 Objective-C 接口导入到 Swift,可以使用`NS_SWIFT_UNAVAILABLE`宏来传达一个提示信息,从而指引 API 使用者使用其他替代方式。 326 | 327 | 例如,一个 Objective-C 类提供了一个接收一些键值对作为可变参数的便利构造器,可以建议 Swift 用户使用字典字面量作为替代: 328 | 329 | ```objective-c 330 | + (instancetype)collectionWithValues:(NSArray *)values 331 | forKeys:(NSArray *)keys NS_SWIFT_UNAVAILABLE("使用字典字面量替代"); 332 | ``` 333 | 334 | 试图在 Swift 中调用`+collectionWithValues:forKeys:`方法将导致编译错误。 335 | 336 | 337 | ## 优化 Objective-C 声明 338 | 339 | 可以使用`NS_REFINED_FOR_SWIFT`宏标记 Objective-C 方法的声明,然后在 Swift 中通过扩展提供一个优雅的 Swift 接口,通过该接口去调用方法的原始实现。例如,接收一个或者多个指针参数的 Objective-C 方法可以优化为一个返回元组值的 Swift 方法。使用该宏标记的方法导入到 Swift 后会做如下处理: 340 | 341 | - 对于初始化方法,在第一个外部参数名前加双下划线(`__`)。 342 | - 对于对象下标方法,只要设值或取值方法被标记`NS_REFINED_FOR_SWIFT`宏,这对下标方法就会变为 Swift 中的普通方法。被标记宏的下标方法会在方法名前加双下划线(`__`)。 343 | - 对于其他方法,在方法名前加双下划线(`__`)。 344 | 345 | 思考如下 Objective-C 声明: 346 | 347 | ```objective-c 348 | @interface Color : NSObject 349 | 350 | - (void)getRed:(nullable CGFloat *)red 351 | green:(nullable CGFloat *)green 352 | blue:(nullable CGFloat *)blue 353 | alpha:(nullable CGFloat *)alpha NS_REFINED_FOR_SWIFT; 354 | 355 | @end 356 | ``` 357 | 358 | 可以通过 Swift 扩展来提供一个优化后的接口: 359 | 360 | ```swift 361 | extension Color { 362 | var RGBA: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { 363 | var r: CGFloat = 0.0 364 | var g: CGFloat = 0.0 365 | var b: CGFloat = 0.0 366 | var a: CGFloat = 0.0 367 | __getRed(&r, green: &g, blue: &b, alpha: &a) 368 | return (red: r, green: g, blue: b, alpha: a) 369 | } 370 | } 371 | ``` 372 | 373 | 374 | ## 将可用性信息添加到 Objective-C API 375 | 376 | 在 Swift 中为特定平台编译应用时,你可以使用 `@available` 特性来控制一个声明是否可用。同样,你可以使用可用性条件 `#available` 根据所需平台和版本有条件地执行代码。 377 | 378 | 两种可用性说明符在 Objective-C 中都有提供,使用以下示例中展示的相应语法即可。 379 | 380 | 如下示例展示了如何为 Swift 声明添加可用性信息: 381 | 382 | ```swift 383 | @available(iOS 11, macOS 10.13, *) 384 | func newMethod() { 385 |    // 使用 iOS 11 API 386 | } 387 | ``` 388 | 389 | 如下示例展示了如何在 Objective-C 中添加相同的可用性信息: 390 | 391 | ```objective-c 392 | @interface MyViewController : UIViewController 393 | - (void) newMethod API_AVAILABLE(ios(11), macosx(10.13)); 394 | @end 395 | ``` 396 | 397 | 如下示例展示了如何在 Swift 条件语句中使用可用性信息: 398 | 399 | ```swift 400 | if #available(iOS 11, *) { 401 |    // 使用 iOS 11 API 402 | } else { 403 | // 早期版本的 iOS 的替代代码 404 | } 405 | ``` 406 | 407 | 如下示例展示了如何在 Objective-C 中使用相同的可用性信息: 408 | 409 | ```objective-c 410 | if (@available(iOS 11, *)) { 411 | // 使用 iOS 11 API 412 | } else { 413 | // 早期版本的 iOS 的替代代码 414 | } 415 | ``` 416 | 417 | 关于指定平台可用性的更多信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift) 中的[声明特性](http://wiki.jikexueyuan.com/project/swift/chapter3/06_Attributes.html#declaration_attributes)部分。 418 | 419 | 420 | ## 为产品模块命名 421 | 422 | 无论是 Xcode 为 Swift 代码自动生成的头文件,还是 Objective-C 桥接头文件,都会根据产品模块名来命名。默认情况下,产品模块名和产品名一样。然而,如果产品名包含特殊字符(只能是字母、数字、下划线),例如点号(`.`),作为产品模块名时,这些特殊字符会被下划线(`_`)替换。如果产品名以数字开头,作为产品模块名时,第一个数字也会被下划线替换。 423 | 424 | 也可以为产品模块名提供一个自定义的名称,Xcode 会根据这个名称来命名桥接头文件和自动生成的头文件,只需修改`Build setting > Packaging > Product Module Name`中的名称即可。 425 | 426 | > 注意 427 | > 无法改变框架的产品模块名。 428 | 429 | 430 | ## 故障排除贴士 431 | 432 | - 将 Swift 和 Objective-C 文件看作同一代码集,注意命名冲突。 433 | 434 | - 如果使用框架,确保框架的`Build setting > Packaging > Defines Module`(`DEFINES_MODULE`)设置为`Yes`。 435 | 436 | - 如果使用 Objective-C 桥接头文件,确保`Build setting > Swift Compiler > Code Generation`(`SWIFT_OBJC_BRIDGING_HEADER`)中的头文件路径是头文件自身相对于项目的路径,例如`MyApp/MyApp-Bridging-Header.h`。 437 | 438 | - Xcode 会根据产品模块名(`PRODUCT_MODULE_NAME`),而不是 target 的名称(`TARGET_NAME`)来命名 Objective-C 桥接头文件以及为 Swift 代码自动生成的头文件。详情请参阅[为产品模块命名](#naming_your_product_module)小节。 439 | 440 | - 只有继承自 Objective-C 类的 Swift 类,以及标记了`@objc`(包括各种被隐式标记的情况)的 Swift 声明,才能在 Objective-C 中使用。 441 | 442 | - 将 Swift 代码导入到 Objective-C 时,注意 Objective-C 无法转化 Swift 的独有特性。详情请参阅[在 Objective-C 中使用 Swift](#using_swift_from_objective-c)小节。 443 | 444 | - 如果在 Swift 代码中使用了自定义的 Objective-C 类型,在 Objective-C 中使用这部分 Swift 代码时,确保先导入相关的 Objective-C 类型的头文件,然后再将 Xcode 为 Swift 代码自动生成的头文件导入。 445 | 446 | - 标记`private`或`fileprivate`修饰符的 Swift 声明不会出现在自动生成的头文件中,因为私有声明不会暴露给 Objective-C,除非它们被标记`@IBAction`,`@IBOutlet`或者`@objc`。 447 | 448 | - 对于应用程序的 target,当存在 Objective-C 桥接头文件时,标记`internal`修饰符的 Swift 声明也会出现在自动生成的头文件中。 449 | 450 | - 对于框架的 target,只有标记`public`或`open`修饰符的声明才会出现在自动生成的头文件中,不过依然可以在框架内部的 Objective-C 代码中使用标记`internal`修饰符的 Swift API,只要它们所属的类继承自 Objective-C 类。关于访问级别修饰符的更多信息,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的[访问控制](http://wiki.jikexueyuan.com/project/swift/chapter2/24_Access_Control.html)章节。 451 | -------------------------------------------------------------------------------- /03-Mix and Match/bridgingheader_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/949478479/Using-Swift-with-Cocoa-and-Objective-C-in-Chinese/abaae4dd6a110a8eb4021a1652f31b93e2adef96/03-Mix and Match/bridgingheader_2x.png -------------------------------------------------------------------------------- /04-Migration/Migrating Your Objective-C Code to Swift.md: -------------------------------------------------------------------------------- 1 | # 迁移 Objective-C 代码到 Swift 2 | 3 | - [为 Objective-C 代码做好迁移准备](#preparing_your_objective-c_code_for_migration) 4 | - [迁移过程](#the_migration_process) 5 | - [准备迁移](#before_you_start) 6 | - [开始迁移](#as_you_work) 7 | - [完成迁移](#after_you_finish) 8 | - [故障排除贴士](#troubleshooting_tips_and_reminders) 9 | 10 | 迁移工作提供了一个重新审视现有的 Objective-C 应用程序的机会,通过用 Swift 代码替换 Objective-C 代码来改善程序的架构,逻辑以及性能。简而言之,通过之前学习的工具,即混搭和互用来对应用程序进行增量迁移。在决定哪些特性和功能用 Swift 实现,哪些依然用 Objective-C 实现时,混搭和互用会让这一切变得简单可行。通过这些工具,可以一步步探索 Swift 广泛的功能并整合到现有的 Objective-C 应用程序中,而不必立刻使用 Swift 重写整个应用程序。 11 | 12 | 13 | ## 为 Objective-C 代码做好迁移准备 14 | 15 | 在开始迁移代码之前,请确保 Objective-C 和 Swift 代码间有着最佳兼容性。这意味着你可能需要整理现有项目,并将 Objective-C 现代化特性应用其中。为了更好地与 Swift 无缝交互,现有的 Objective-C 代码需要遵循现代编码实践。在开始前,这有个简短的实践列表,请参阅 [*Adopting Mordern Objective-C*](https://developer.apple.com/library/prerelease/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html#//apple_ref/doc/uid/TP40014150)。 16 | 17 | 18 | ## 迁移过程 19 | 20 | 迁移代码到 Swift 的最有效的方式是逐个文件迁移,即一次迁移一个类。由于无法在 Objective-C 中继承 Swift 类,因此最好选择一个没有子类的类开始。使用单个`.swift`文件替换对应的`.m`和`.h`文件,实现和接口将直接放进这个 Swift 文件。你也不用创建头文件,Xcode 会在你需要引用头文件的时候自动生成头文件。 21 | 22 | 23 | ### 准备迁移 24 | 25 | * 在 Xcode 中,选择`File > New > File > (iOS,watchOS,tvOS,macOS) > Source > Swift File`为对应的 Objective-C `.m`和`.h`文件创建一个 Swift 类。可以使用相同或者不同的类名。类前缀在 Swift 中不是必须的。 26 | 27 | * 导入相关的系统框架。 28 | 29 | * 如果需要在 Swift 文件中使用同一 target 中的 Objective-C 代码,可以填写一个 Objective-C 桥接头文件。具体的操作步骤,请参阅[在应用程序的 target 中导入代码](../03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md#importing_code_from_within_the_same_app_target)小节。 30 | 31 | * 为了让 Swift 类能在 Objective-C 中使用,Swift 类必须继承自 Objective-C 类。如果想为 Swift 类指定在 Objective-C 中的类名,可以使用`@objc(name)`特性,`name`就是 Swift 类在 Objective-C 中的类名。关于`@objc`的更多信息,请参阅[Swift 类型兼容性](../02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md#swift_type_compatibility)小节。 32 | 33 | 34 | ### 开始迁移 35 | 36 | * 可以通过继承 Objective-C 类,采用 Objective-C 协议,或者其他方式,来让 Swift 类集成 Objective-C 特性。更多信息请参阅[使用 Objective-C 特性编写 Swift 类和协议](../02-Interoperability/02-Writing%20Swift%20Classes%20and%20Protocols%20with%20Objective-C%20Behavior.md)章节。 37 | 38 | * 使用 Objective-C API 的时候,你需要知道 Swift 是怎样转化某些 Objective-C 语言特性的。更多信息请参阅[与 Objective-C API 交互](../02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md)章节。 39 | 40 | * 使用 Cocoa 框架的代码时,记住某些类型已经被桥接,这意味着可以使用 Swift 类型去替代 Objective-C 类型。更多信息请参阅[使用 Cocoa 框架](../02-Interoperability/03-Working%20with%20Cocoa%20Frameworks.md)章节。 41 | 42 | * 使用 Cocoa 设计模式的时候,请参阅[采用 Cocoa 设计模式](../02-Interoperability/04-Adopting%20Cocoa%20Design%20Patterns.md)章节。 43 | 44 | * 对于如何将属性从 Objective-C 转换到 Swift,请参阅 [*The Swift Programming Language 中文版*](http://wiki.jikexueyuan.com/project/swift/) 中的[属性](http://wiki.jikexueyuan.com/project/swift/chapter2/10_Properties.html)章节。 45 | 46 | * 在需要的时候,使用`@objc(name)`特性为 Swift 中的属性或方法提供 Objective-C 命名。例如,可以像下面这样将`enabled`属性的取值方法在 Objective-C 中的命名更改为`isEnabled`: 47 | 48 | ```swift 49 | var enabled: Bool { 50 | @objc(isEnabled) get { 51 | // ... 52 | } 53 | } 54 | ``` 55 | 56 | * 分别用`func`和`class func`来声明实例方法(`-`)和类方法(`+`)。 57 | 58 | * 将简单宏声明为全局常量,将复杂宏声明为函数。 59 | 60 | 61 | ### 完成迁移 62 | 63 | * 将 Objective-C 代码中对应的导入语句更改为`#import "ProductModuleName-Swift.h"`,更多信息请参阅[在应用程序的 target 中导入代码](../03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md#importing_code_from_within_the_same_app_target)小节。 64 | 65 | * 将原始的 Objective-C `.m`文件在`Target Membership`选择框中的勾选取消,从而将其从 target 中移除。不要立刻删除`.m`和`.h`文件,以备解决问题时使用。 66 | 67 | * 如果为 Swift 类起了一个新的类名,在相关代码中请使用新的 Swift 类名代替原来的 Objective-C 类名。 68 | 69 | 70 | ## 故障排除贴士 71 | 72 | 尽管对于不同的项目,迁移过程是不尽相同的,但仍有一些通用的办法能解决代码迁移时遇到的问题: 73 | 74 | * 无法在 Objective-C 中继承 Swift 类。因此,被迁移的类不能有任何 Objective-C 子类。 75 | 76 | * 迁移一个类到 Swift 时,必须从 target 中移除相关的`.m`文件,避免编译时出现符号重复的错误。 77 | 78 | * 为了能在 Objective-C 中使用,Swift 类必须是一个 Objective-C 类的子类。 79 | 80 | * 在将 Swift 代码导入 Objective-C 代码时,切记 Objective-C 不能转化某些 Swift 的独有特性。详细列表请参阅[在 Objective-C 中使用 Swift](../03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md#using_swift_from_objective-c)小节。 81 | 82 | * 可以在 Objective-C 代码中通过`Commond + 单击`一个 Swift 类名的方式来查看 Xcode 为它自动生成的头文件。 83 | 84 | * 可以`Option + 单击`一个符号来查看它的详细信息,比如它的类型,特性以及文档注释等。 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | #### 开始 4 | - [基本设置](01-Getting%20Started/Basic%20Setup.md) 5 | 6 | #### 交互 7 | - [与 Objective-C API 交互](02-Interoperability/01-Interacting%20with%20Objective-C%20APIs.md) 8 | - [使用 Objective-C 特性编写 Swift 类和协议](02-Interoperability/02-Writing%20Swift%20Classes%20and%20Protocols%20with%20Objective-C%20Behavior.md) 9 | - [使用 Cocoa 框架](02-Interoperability/03-Working%20with%20Cocoa%20Frameworks.md) 10 | - [采用 Cocoa 设计模式](02-Interoperability/04-Adopting%20Cocoa%20Design%20Patterns.md) 11 | - [与 C 语言 API 交互](02-Interoperability/05-Interacting%20with%20C%20APIs.md) 12 | 13 | #### 混搭 14 | - [在项目中同时使用 Swift 和 Objective-C](03-Mix%20and%20Match/Swift%20and%20Objective-C%20in%20the%20Same%20Project.md) 15 | 16 | #### 迁移 17 | - [迁移 Objective-C 代码到 Swift](04-Migration/Migrating%20Your%20Objective-C%20Code%20to%20Swift.md) 18 | --------------------------------------------------------------------------------