Swift 開發中的一些小的技巧
剛開始的時候, 特別好奇大廠是怎么搞的, 他們的項目長什么樣子, 他們用哪些庫...想在巨人的肩膀上開發, 免得浪費時間在那些已經有很好解決方案的事情上。
四年前,我和團隊中很多很厲害的人討論過一些編程實踐。今天就分享一些東西吧。
歡迎指正!??
濫用引用類型
只有“動態”對象才使用引用類型。這里的“動態”對象是什么呢?看下面的代碼:
struct Car {
let model: String
}
class CarManager {
private(set) var cars: [Car]
func fetchCars() {}
func registerCar(_ car: Car) {}
}
?? 在這里只是一個值。他代表的就是一些數據。就像 1
、2
、3
。 這種數據是“靜態”的數據(死的)。 它不會處理任何東西, 所以它也沒有必要是“動態”的, 也就是說, 沒必要把它定義成引用類型。
另一方面:
CarManager
就需要是一個“動態”的對象。因為這個對象會發起網絡請求, 然后將請求結果保存起來。在值類型對象中是不能執行異步任務的, 因為他們是“靜態”的數據。我們需要的 CarManager
對象在一定的范圍內是應該是動態的, 他會請求數據, 也會注冊新的 Car
。
這個主題完全可以寫一篇文章來深入。推薦看看 Andy Matuschak 的文章, 和 WWDC
隱式解包可選類型(!
)
默認不要隱式解包可選類型。 在大多數場景中你都可能會忘掉這件事情。但是在一些特殊情況下應該這樣做來減少編譯器的壓力。而且我們也需要去理解這件事情背后的邏輯。
基本上, 如果這個屬性在初始化的過程中必須為 nil
但是之后就會被賦值, 就可以定義這個屬性為 optional。因為你肯定不會在賦值之前訪問這個屬性, 如果編譯器一直警告這個值可能為 nil
真的挺討厭的。
看看xib中拖出來的屬性:
class SomeView: UIView {
@IBOutlet let nameLabel: UILabel
}
如果這樣定義的話, 編譯器就會讓你在初始化方法中給nameLabel
賦值。因為這行代碼告訴編譯器這個 View
無論什么時候都有 nameLabel
。 但是, 有病啊!肯定不能這么干啊。因為其實在 initWithCoder
中已經幫我們實現了 xib
中的 label
和這個屬性之間的關聯。明白了嗎? 這個值永遠都不可能為空, 就沒有必要判斷這個東西是不是存在了。所以也不需要去賦值了啊。
你:這玩意兒肯定不可能是空, 別瞎幾把報錯了
編譯器: 好的!
class SomeView: UIView {
@IBOutlet var nameLabel: UILabel!
}
Q: 在dequeue一個tableviewCell 的時候能不能(!
)?
A: 還是不要吧!至少給一個 Crash 啊
guard let cell = tableView.dequeueCell(...) else {
fatalError("Cannot dequeue cell with identifier \(cellID)")
}
濫用 AppDelegate
AppDelegate
不是拿來給你做保存全局變量的容器的(全局屬性、工具方法、管理類等等。)他只是一個用來實現一些協議的類而已。放過它吧!
在 applicationDidFinishLaunching
方法里肯定都會做一些很重要的事情, 但是當項目不斷變大的時候這種情況很容易變的很恐怖。創建新的類(文件)來做這些事情吧!
?? Don’t:
let persistentStoreCoordinator: NSPersistentStoreCoordinator
func rgb(r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor { ... }
func appDidFinishLaunching... {
Firebase.setup("3KDSF-234JDF-234D")
Firebase.logLevel = .verbose
AnotherSDK.start()
AnotherSDK.enableSomething()
AnotherSDK.disableSomething()
AnotherSDK.anotherConfiguration()
persistentStoreCoordinator = ...
return true
}
?? Do:
func appDidFinishLaunching... {
DependencyManager.configure()
CoreDataStack.setup()
return true
}
默認參數
給一個方法的某些參數設置默認值是非常方便的事情。如果沒有這個特性的話, 可能就需要給同一個功能寫好幾個方法了。像下面一樣:
func print(_ string: String, options: String?) { ... }
func print(_ string: String) {
print(string, options: nil)
}
如果有默認參數值, 就可以是這樣的:
func print(_ string: String, options: String? = nil) {...}
很簡單對吧! 給自定義 UI 組件設置默認顏色、提供默認的參數、給網絡請求添加默認的超時時間等等。但是, 使用這個語法糖在遇到依賴注入的時候就要小心了。
看下面的例子:
class TicketsViewModel {
let service: TicketService
let database: TicketDatabase
init(service: TicketService,
database: TicketDatabase) { ... }
}
在 App target:
let model = TicketsViewModel(
service: LiveTicketService()
database: LiveTicketDatabase()
)
在 Test target:
let model = TicketsViewModel(
service: MockTicketService()
database: MockTicketDatabase()
)
在這里使用協議的原因就是把這些功能從具體的類中抽象出來。這就使得你可以向這個 viewModel
中注入任何你想要的具體實現。 如果這里你把 LiveTicketService
作為默認的參數, 這就使得TicketsViewModel
依賴了 LiveTicketService
這么一個具體的類型。這跟最初想要達到的目的有了一些沖突。
現在沒那么方便了吧?
想象一下在你 App 還有 Test 兩個 target 中。 TicketsViewModel
會被同時添加到兩個 target 中, 然后把 LiveTicketService
和 MockTicketService
分別添加。如果 TicketsViewModel
添加了對 LiveTicketService
的依賴。 Test target 肯定就編譯不過了。
可變參數函數
這... 就是很爽啊!
func sum(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}
sum(1,2) // 3
sum(1,2,3) // 6
sum(1,2,3,4) // 10
使用類型嵌套
Swift 支持內部類。所以有用就可以這么做:
?? Don’t:
enum PhotoCollectionViewCellStyle {
case default
case photoOnly
case photoAndDescription
}
這個枚舉可能在 PhotoCollectionViewCell
之外就不會再使用到了。沒理由把這個枚舉聲明成全局的。
?? Do:
class PhotoCollectionViewCell {
enum Style {
case default
case photoOnly
case photoAndDescription
}
let style: Style = .default
}
這很容易理解, 畢竟 Style
本來就是用來標記 PhotoCollectionViewCell
的。而且還少了23個字符呢。
使用 final 關鍵字 ??
如果你不需要拓展某些類, 也不希望這些類被拓展, 使用 final
修飾它。不用擔心犯錯, 比如 PhotoCollectionViewCell
這個類, 你還有可能繼承它嗎?
而且:這么做可以節約編譯時間。
給常量命名空間
在 OC 中是通過在全局的常量前面加 PFX
或者 k
來給這些常量命名空間的。但是 Swift 可不這樣。
?? Don’t:
static ket kAnimationDuration: TimeInterval = 0.3
static let kLowAlpha = 0.2
static let kAPIKey = "13511-5234-5234-59234"
?? Do:
enum Constant {
enum UI {
static let animationDuration: TimeInterval = 0.3
static let lowAlpha: CGFloat = 0.2
}
enum Analytics {
static let apiKey = "13511-5234-5234-59234"
}
}
我個人的偏好是使用 C
來代替 Constant
, 他已經夠清晰了。這個可以看你自己喜歡了。
Before: kAnimationDuration
或者 kAnalyticsAPIKey
After: C.UI.animationDuration
或者 C.Analytics.apiKey
_
的使用
_
是對沒有使用到的變量的占位符。他就是告訴編譯器"這個值是什么不重要"。 不然編譯器會有警告??。
?? Don't
if let _ = name {
print("Name is not nil.")
}
optional
就像一個盒子。可以直接看他是不是空的, 沒必要每次都把里面的東西拿出來。
?? Do:
- 判空
if name != nil {
print("Name is not nil.")
}
- 返回值沒用
_ = manager.removeCar(car) // 成功返回true
- ConpletionHandler
service.fetchItems {data, error , _ in
// 第三個參數我不在乎他是什么
}
方法命名
這點適用于所有需要人類去閱讀的語言。代碼總是不那么容易理解的, 不要浪費別人的精力。
driver.driving()
這是在干什么?
- 是把
driver
標記成driving
狀態? - 還是檢查
driver
是不是driving
狀態, 并且返回一個bool
值?
如果要點進去看才知道這方法是干什么的, 這個命名就是失敗了。多人協同開發或者處理遺留項目的時候, 你讀別人代碼的時間比你寫代碼的時間都要長。所以在命名的時候想著別讓看你代碼的人痛苦。
關于 print
很嚴肅的說, 不要得到一個 error
或者 response
就在控制臺打印出來。你這么做還不如不打印呢!搞得控制臺一堆亂七八糟的東西看起來真的很爽嗎?
Do:
- 在
framework
中使用error
級的log level
。 - 使用一些能夠讓你有不同輸出級別的 log 庫。XGGLogger、SwiftyBeaver
- 不要用 log 來 debug 了。Xcode 有很多有用的工具Debugging: A Case Study
沒用的代碼
經常在一些老項目里面見到被注釋掉的代碼, 但是出來沒有通過把這些代碼打開來解決過問題。所以, 既然這些代碼都沒有什么用了, 就刪了它! 還能增加代碼的可讀性, 看起來整潔的代碼總要讓人舒服一些。