收藏swift

如何假装写过 Swift

2020-01-03  本文已影响0人  Nemocdz

总结了笔者日常使用 Swift 的一些小 Tips。

Safe & Fast

1. 能用 let,尽量不用 var

把代码里的 var 全改成 let,只保留不能编译通过的。

ObjC 的 Foundation 层几乎都是继承 NSObject 实现的,平时都在操作指针,所以要区分 Mutable 和 Imutable 的设计,比如 NSStringNSMutableString

Swift 使用了 let 和 var 关键字直接用于区分是否可变。可变会更容易出错,所以尽量采用不可变设计,等到需要改变才改为 var 吧。

2. 尽量不用 !

!遇到 nil 时会 crash(包括 as! 进行强制转换)。可以使用 if let/guard let/case let 配合 as? 将可选值消化掉。可能返回 nil 的 API,为什么要自己骗自己呢?

当遇到 ObjC 代码暴露给 Swift 使用时,给接口 .h 文件加上 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 并检查接口参数是否可以为 nil 吧。

3. 多定义 struct,少定义 class

struct 是值类型,class 是引用类型。类类型分配在堆区,默认浅拷贝,容易被不经意间被改变,而值类型分配在栈区,默认深拷贝。并且 Swift 还有写时复制(copy on write)。

即使是使用 class 时,也仅在必要时(如桥接到 ObjC,使用 Runtime 一些特性)继承自 NSObject

4. 能用 Swift 标准库类型,尽量不用对应的 Foundation 类型

多使用 StringArrayDictionaryIntBool,少使用 Foundation 里面的 NSStringNSArrayNSDictionaryNSNumber。Cocoa Foundation 里面的都是类类型,而 Swift 标准库的是值类型,有很多标准库的方便方法。

还有用 print 代替 NSLog

5. 优先使用内置高阶函数

forEachmapconpactMapflatMapzipreduce 是好帮手,代替一些使用变量并在循环中处理的例子吧。用上高阶函数,不仅代码更清晰,还能将状态控制在更小的作用域内。

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 作为枚举的原始值,可以不需要原始值,也可以使用 StringFloatBoolean 作为原始值。能在枚举值上关联值,实现很多有趣的功能(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>

guardif 的反义词,可以提前将异常情况 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 在闭包中可以使用 weakunowned 指定闭包对值的捕获,配合 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

7. 对 String 判空时优先采用 isEmpty

上一篇下一篇

猜你喜欢

热点阅读