Swift 程式碼教學:好好設定物件 讓程式碼更簡潔清晰
我們都看過這樣的 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) } }
是不是讓程式碼變得簡潔許多,架構也更清楚了呢?