如何假装写过 Swift
总结了笔者日常使用 Swift 的一些小 Tips。
Safe & Fast
1. 能用 let,尽量不用 var
把代码里的 var 全改成 let,只保留不能编译通过的。
ObjC 的 Foundation 层几乎都是继承 NSObject
实现的,平时都在操作指针,所以要区分 Mutable 和 Imutable 的设计,比如 NSString
和 NSMutableString
。
Swift 使用了 let 和 var 关键字直接用于区分是否可变。可变会更容易出错,所以尽量采用不可变设计,等到需要改变才改为 var 吧。
2. 尽量不用 !
!遇到 nil 时会 crash(包括 as!
进行强制转换)。可以使用 if let
/guard let
/case let
配合 as?
将可选值消化掉。可能返回 nil 的 API,为什么要自己骗自己呢?
当遇到 ObjC 代码暴露给 Swift 使用时,给接口 .h 文件加上 NS_ASSUME_NONNULL_BEGIN
和 NS_ASSUME_NONNULL_END
并检查接口参数是否可以为 nil 吧。
3. 多定义 struct,少定义 class
struct 是值类型,class 是引用类型。类类型分配在堆区,默认浅拷贝,容易被不经意间被改变,而值类型分配在栈区,默认深拷贝。并且 Swift 还有写时复制(copy on write)。
即使是使用 class 时,也仅在必要时(如桥接到 ObjC,使用 Runtime 一些特性)继承自 NSObject
。
4. 能用 Swift 标准库类型,尽量不用对应的 Foundation 类型
多使用 String
、Array
、Dictionary
、Int
、Bool
,少使用 Foundation 里面的 NSString
、NSArray
、NSDictionary
、NSNumber
。Cocoa Foundation 里面的都是类类型,而 Swift 标准库的是值类型,有很多标准库的方便方法。
还有用 print
代替 NSLog
。
5. 优先使用内置高阶函数
forEach
,map
,conpactMap
,flatMap
,zip
,reduce
是好帮手,代替一些使用变量并在循环中处理的例子吧。用上高阶函数,不仅代码更清晰,还能将状态控制在更小的作用域内。
6. 使用 try catch 捕获错误
和 ObjC 基本都在函数的回调中返回 NSError
不一样,Swift 函数可以使用 throw
关键字抛出错误。
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n22" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> func test() throws {
//...
}
do {
try test()
} catch {
print(error)
}
// 如果对错误不敏感
try? test()</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n35" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> // no bad
let flag:Bool = false
// better
let flag = false
// not bad
view.contentMode = UIView.ContentMode.center
// better
view.contentMode = .center</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n38" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> // not bad
func test() {
//...
}
func test(param1:String) {
//...
}
func test(param2:String) {
//...
}
// better
func test(param1:String = "", param2:String = "") {
//...
}</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n40" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> let _ = [0].removeLast()
[0].forEach{ _ in print("hh")}</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n44" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> let default
= A()
func default
() {}</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="objective-c" cid="n47" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> __weak typeof(self) weak_self = self;
typeof(self) strong_self = weak_self;</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> test(){ [weak self] in
guard let self = self else { return }
// self is strong without retain cycle
}</pre>
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="swift" cid="n53" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: normal; position: relative !important; padding: 10px 10px 10px 0px; width: inherit; background-position: initial initial; background-repeat: initial initial;"> struct GameService {
enum APIError {
enum ResultError {
case noResult
}
}
// use APIError
}
// use GameService.APIError</pre>
参考链接
如有错误,欢迎交流&指出。
最后
关于 SwiftUI 和 Combine 的介绍,可以参考 WWDC 2019 相关 Session。也可以参考笔者翻译的 文章 1,文章 2。
在 WWDC19 推出的 Swift Only 的库,SwiftUI 有着类似 React 的声明式 UI 开发框架,配合实时调试,在 Demo 和简单页面,跨 Apple 平台应用适配时有一定优势。而 Combine 时类似 RxSwift 的响应式编程框架,能使事件流更统一。
35. SwiftUI&Combine
在 Swift4 加入 Codable 协议后,JSON 等通用结构转模型,终于有了原生的支持。详情见 文章。
34. Codable
同理,Swift 的 GCD API 也是专门经过 Swift 化的,也更加简洁好用。
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n103" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">scrollObserver = observe(.scrollView!.contentOffset, options: [.new], changeHandler: { object, change in
//...
})</pre>
在 ObjC 中,开发者更习惯用类似 FBKVOController 等第三方库进行 KVO 的监听,那是因为原生的写法太难用了。Swift 为 KVO 增加了闭包的 API,更简洁好用。
33. KVO
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n100" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">URLSession.shared.dataTask(with: request) { result in
switch result {
case .success(let (data, _)):
handle(data: data)
case .failure(let error):
handle(error: error)
}
}</pre>
比如对网络请求回调进行改造:
Result
是一个枚举类型,包含成功或者失败的枚举值,并持有相应成功或者失败的值,通过范型确定类型信息。因为成功和失败是互斥的,这样就可以避免多个可选参数返回。
32. Result 类型
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n96" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">typealias NewName<D> = ClassA<D>&ProtocolA&ProtocolB</pre>
typealias
可以用来命名闭包类型、协议类型、范型类型,还支持组合。更多用法见 文章。
31. typealias 关键字
where
关键字可以对范围进行限定,详情见这篇 文章。
30. where 关键字
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n91" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">lazy var view = UIView(frame:.zero)</pre>
懒加载不需要像 ObjC 一样重写 getter 方法,并判空了,在属性前面加上 lazy
关键字就可以实现了。
29. lazy 关键字
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n88" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">fileprivate extension Date {
var toString: String {
//...
}
}
Date().toString</pre>
和 ObjC 的 Categories 类似,拓展可以添加类的方法。Swift 的 Extension 还能拓展值类型,枚举的方法,且不需要新建文件编写和支持权限访问关键字。通过 Extension,还能给 Protocol 增加默认实现。也能在 Extension 中遵循协议,让方法划分更加清晰。
28. 尝试 Extension
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n85" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">enum State<Value> {
case pending
case fulfill(value:Value)
case reject(reason:Error)
mutating func update(to state:State){
guard case .pending = self else {
return
}
self = state
}
}</pre>
比起 ObjC 那和 C 语言差不多的枚举,Swift 的枚举更强大。Swift 的枚举不一定需要 Int
作为枚举的原始值,可以不需要原始值,也可以使用 String
、Float
、Boolean
作为原始值。能在枚举值上关联值,实现很多有趣的功能(Rx,Promise 的状态机)。能给枚举编写函数,能给枚举增加 Extension。
27. 尝试枚举
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n82" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">protocol View {
associatedtype Model
func update(model:Model)
}</pre>
比起 ObjC 仅支持在集合类型里使用轻量级范型,Swift 的范型更强大,除了集合,还支持类、枚举、协议(Associate Type)。
26. 尝试范型
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n79" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">typealias Pair<T> = (T, T)
let pair = Pair(1, 2)</pre>
元组(Tuple)是个包含多个值的简单对象,使用元组,可以简单的用来函数返回多参数,也可以在集合类型中存取一对对的值。
25. 尝试元组
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n76" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">guard let a = a as? String else { return }
// 下面的 a 就是 string 并且 non-nil 的了</pre>
guard
是 if
的反义词,可以提前将异常情况 return。配合 guard let
使用,可以在正常分支下使用正确的条件。
24. 优先使用 guard
Swift 在设计上,为协议做了很多强大的功能。Swift 标准库里大量的方法和类都使用了协议进行抽象。在编写代码时优先考虑使用协议进行逻辑的抽象,详情可以参考 Apple WWDC 2015 Session 408 - Protocol-Oriented Programming in Swift。
23. 更 POP(Protocol Oriented Programming,面向协议编程)
Syntactic sugar
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n70" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">let a = 2
print("(a) is 2")</pre>
除了常规的字符串插值,Swift5 还增加了更强大可自定义的字符串插值系统,详情见 文章。
22. 使用字符串插值
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n67" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">// not bad
var name:String?
if let aName = dic["name"] as? String {
name = aName
} else {
name = ""
}
// better
let name = dic["name"] as? String ?? ""</pre>
21. 使用 ?? 返回默认值
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n65" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">enum Event {
enum Name {
static let login = "event.name.login"
}
}
// use
Event.Name.login
// not allow
Event()</pre>
定义一些常量时,用命名空间做隔离是最好的,Swift 的 Enum 比较适合用于命名空间的定义,能嵌套,且不存在初始化方法不会被用于其他作用。
20. Enum 用于命名空间声明
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n62" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">// not bad
func updateWithView(view:UIView)
updateWithView(view:viewA)
// better
func updateWithView(_ view:UIView)
updateWith(view:viewA)
// not bad
func didSelectAtIndex(index:Int)
didSelectAtIndex(index:2)
// better
func didSelect(at index:Int)
didSelect(at:2)</pre>
和 ObjC 不同,有形参实参的 Swift,可以在调用和编写的时候都有更合适简洁的表达。
19. 使用更简洁的函数实参和形参
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">let someView: UIView = {
let view = UIView(frame:.zero)
view.backgroundColor = .red
return view
}()</pre>
有时候初始化时一个对象时还需要赋值其中的一些属性,这个时候就可以使用闭包代码块的整合。
18. 使用闭包做初始化
<pre spellcheck="false" class="md-fences mock-cm md-end-block md-fences-with-lineno" lang="swift" cid="n56" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; margin-top: 0px; margin-bottom: 20px; background-color: rgb(51, 51, 51); font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; white-space: pre-wrap; position: relative !important; padding: 10px 10px 10px 8px; width: inherit; background-position: initial initial; background-repeat: initial initial;">func big(){
func small() {
//...
}
small()
}</pre>
有时候某一块逻辑只需要在方法内复用或者做逻辑分割,可以在方法内定义方法,这样访问域会更清晰。
17. func 嵌套
类型嵌套用于在类型里定义类型,让类型的命名空间的精细化程度更高。
16. 类型嵌套
Swift 在闭包中可以使用 weak
和 unowned
指定闭包对值的捕获,配合 guard
就可以实现同样的功能。
尽管很多人会采用宏来简化,但重复的宏定义又会冲突,且 ObjC 没有访问权限关键字。
比起 ObjC 里需要每次需要写
15. Strong-Weak Dance 很简单
比如系统有个 default
关键字,而你也希望使用这个命名时就能派上用场。
14. 使用 `` 来定义和关键字重名的方法和属性
而对于自己设计的接口,如果返回值无关紧要,只是附加功能的话,可以使用 @discardableResult
进行标注。
13. 使用 _ 表示不使用的返回值
在设计接口时,不再需要为每一个形参是否需要而编写一个方法了,减少方法数吧。
12. 使用默认形参,简化接口设计
11. 能推导的类型不用显式编写
直接使用 ClassA()
,代替 ClassA.init()
,代码更简洁。
10. 省略 init()
只在闭包内、函数实参和成员变量名字相同和方法形参需要自身时使用。闭包内 self
是强制的,并且可以提醒注意循环引用问题。函数调用时实参名和成员变量相同时,函数作用域内会优先使用函数实参,所以访问成员变量是需要 self
。
对应访问成员变量,方法时,都不用像 ObjC 那些写 self
了。
9. 省略 self
尽量只有在需要桥接给 ObjC 时,才使用 @objc(前缀 + 类名)
进行别名声明。
Swift 有着 framework 级别的命名空间,所以命名重复时可以通过 framework 名确定,不用担心重复命名问题。
8. 文件名字去掉前缀
Clean Code
Swift 里面的 String
的 index 和 count 不是一一对应的(兼容 Unicode),所以 stirng.count == 0
的效率不如 string.isEmpty
。