我們都看過這樣的 code:
let textField = UITextField()
textField.text = "abc"
textField.backgroundColor = .red
textField.delegate = self
self.view.addSubview(textField)
簡單、明瞭,但當中卻有些問題,讓這段 code 不是那麼的好看。
甚麼問題呢?
textField
這個詞重複出現了五次。- 整段程式碼沒有階層,只能靠 comment 與空行去跟其它的程式碼做區隔。
這整段程式碼做的其實是語意上相關聯的一組事件。如果我們用白話來解釋的話,就是:
我要在我的 view 底下加一個新的、經過設定的 text field。
如果我們直接把它轉譯成 Swift 語法的話,概念上大概會變成這樣:
self.view.addSubview(UITextField(text: "abc,
backgroundColor: .red
delegate: self))
雖然看起來很簡單,但實際上要實作的話,就得要去寫 convenience init
才行:
extension UITextField {
convenience init(text: String, backgroundColor: UIColor, delegate: UITextFieldDelegate?) {
self.text = text
self.backgroundColor = backgroundColor
self.delegate = delegate
}
}
這樣多麻煩啊?何況還可能會有更多的 property 可以設定。然而,不想要寫 init
的話,就沒辦法用 parameter list 去寫設定了。
另一個常見的方法,是寫一個無名的 function(也就是一個 closure)來當作 factory,去製造出所要的物件:
let textField: UITextField = {
let textField = UITextField()
textField.text = "abc"
textField.backgroundColor = .red
textField.delegate = self
return textField
}()
self.view.addSubview(textField)
這種方法讓階層出來了,我們可以很清楚地看到整段 code 就是在做兩件事:設定 textField
與 addSubview
。這個方法雖然比一開始還多了三行,但它讓整個程式碼的架構更清楚易懂、更容易整理,已經算是進步了。再者,它也不需要另外寫 init
或 function,增加的程式碼其實不算多。更甚者,它還可以這樣用:
class ViewController: UIViewController {
lazy var textField: UITextField = {
let textField = UITextField()
textField.text = "abc"
textField.backgroundColor = .red
textField.delegate = self
return textField
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textField)
}
}
如此一來,我們就可以把大量的程式碼從 viewDidLoad
裡面移走,還他一個乾淨。
可是,我們還是沒有解決一個問題:它的重複性太高了。看看上面那段程式碼,光是 textField
就出現了 7 次,UITextField
也出現了兩次。有什麼辦法可以減少它的重複性呢?
有的。我們先想想看把重複的東西都拿掉之後,它應該變成什麼樣子:
class ViewController: UIViewController {
lazy var textField = UITextField(
text: "abc"
backgroundColor: .red
delegate: self
)
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textField)
}
}
也就是說,接近我們的第一個解決方法:寫一個 convenience init
。然而,這個做法的缺點就是要把要設定的東西一項一項寫在 init
裡面,太麻煩了。這樣的話,有沒有辦法不用把要設定的東西都寫出來呢?有的,我們可以用 closure 來達到類似的效果:
extension UITextField {
convenience init(configureHandler: (UITextField) -> Void) {
self.init()
configureHandler(self)
}
}
然後我們就可以:
let textField = UITextField {
$0.text = "abc"
$0.backgroundColor = .red
$0.delegate = self
}
self.view.addSubview(textField)
這樣是不是就解決了我們一開始列出的兩個問題了呢?我們既把設定 textField
的 code 放在同一個 block 裡面,也用 $0
去取代 textField
,讓程式碼更簡潔好看。
然而,我們還是得針對每個 class 都去寫它的 convenience init
,因為它的 configureHandler
所接收的類型都不同。即使我們這樣寫:
extension NSObject {
convenience init(configureHandler: (NSObject) -> Void) {
self.init()
configureHandler(self)
}
}
我們也會需要在實際使用的時候去 downcast,像這樣:
let textField = UITextField {
let textField = $0 as! UITextField
textField.text = "abc"
textField.backgroundColor = .red
textField.delegate = self
}
這樣好像又回到用無名 function 的方法了。不過還好,我們還有 generic 這個 Swift 的秘密武器,可以用 Self
去代表自己的型別。只是 ⋯⋯ class 本身並不支援使用 Self
,只有 protocol 可以。
那沒關係,我們就用 protocol 來寫吧:
protocol Declarative { }
extension Declarative where Self: NSObject {
init(configureHandler: (Self) -> Void) {
self.init()
configureHandler(self)
}
}
extension NSObject: Declarative { }
注意到玄機了嗎?這個 Declarative
protocol 什麼需求都沒有,只有在 extension 裡面才有東西。也就是說,這個 protocol 不是拿來當介面使用的(如 UITableViewDataSource
之類的東西),而是拿來幫既有的 class 加 function 用的。
簡單來說,我們把剛剛寫在 NSObject
的 extension 的 convenience init
整個搬到一個 protocol 的 extension 裡面,再讓 NSObject
去遵從 (conform) 這個 protocol。如此一來,我們就可以使用 Self
了。
之所以用 NSObject
,是因為這樣的設定法幾乎只有 class 才用得到,而在 UIKit
底下,所有的 class 都繼承自 NSObject
。不過,我們也可以用另一種「更 Swift」的寫法:
protocol Declarative: AnyObject {
init()
}
extension Declarative {
init(configureHandler: (Self) -> Void) {
self.init()
configureHandler(self)
}
}
extension NSObject: Declarative { }
extension YourOwnObject: Declarative { }
如此一來,即使自訂的、非繼承自 NSObject
的 class 也可以使用這個 convenience init
了。
那麼,實際寫出來是什麼樣子的呢?
class ViewController: UIViewController {
lazy var textField = UITextField {
$0.text = "abc"
$0.backgroundColor = .red
$0.delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(textField)
}
}
是不是讓程式碼變得簡潔許多,架構也更清楚了呢?