UIKit 開發久了,最煩的事情之一就是初始化一個 View:
1
2
3
4
5
| let subView: UIView = .init(frame: .zero)
subView.translatesAutoresizingMaskIntoConstraints = false
subView.clipsToBounds = true
subView.layer.cornerRadius = 10
subView.backgroundColor = .black
|
五個屬性,五行,subView 重複出現五次。這還只是一個 View,整個畫面十幾個元件疊下來,viewDidLoad 就已經半條命了。
核心想法
Swift 的 WritableKeyPath 可以讓你用 \.propertyName 的語法指向某個屬性,配合 @discardableResult 讓方法回傳 self,就能串起來一路點下去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| extension NSObjectProtocol {
@discardableResult
func setup<Value>(_ keypath: WritableKeyPath<Self, Value>, value: Value) -> Self {
var result = self
result[keyPath: keypath] = value
return result
}
@discardableResult
func setup<Value>(_ keypath: WritableKeyPath<Self, Value>, condition: () -> Value) -> Self {
var result = self
result[keyPath: keypath] = condition()
return result
}
}
|
兩個多載:
- 第一個處理固定值
- 第二個接受 closure,用來處理條件判斷
使用起來長這樣
1
2
3
4
5
6
7
| let subView: UIView = .init(frame: .zero)
.setup(\.translatesAutoresizingMaskIntoConstraints, value: false)
.setup(\.clipsToBounds, value: true)
.setup(\.layer.cornerRadius, value: 10)
.setup(\.backgroundColor) {
Bool.random() ? .black : .white
}
|
subView 只出現一次,後面全部是屬性設定,視覺上很清楚這整塊都是在初始化同一個物件。
為什麼用 NSObjectProtocol 而不是 NSObject
因為 NSObjectProtocol 的覆蓋範圍更廣,UIView、CALayer、UIBarButtonItem 這些都進得來。用 NSObject 的話有些類別不繼承它,反而漏掉。
限制
WritableKeyPath 要求屬性是 var,let 屬性無法用這個方式設定,這是語言層面的限制,繞不過去。
這個技巧在 Swift 4.x / 5.x 初期的 UIKit 專案裡很流行,寫起來比 Builder Pattern 輕,比裸寫乾淨。雖然 SwiftUI 時代用的機會少了,但只要還在寫 UIKit,這個 extension 就值得放在 toolbox 裡。
本文使用 Claude 共同完成