如何讓 iOS 程式碼更美觀:物件的設定

Li-Heng Hsu

--

我們都看過這樣的 code:

let textField = UITextField()
textField.text = "abc"
textField.backgroundColor = .red
textField.delegate = self
self.view.addSubview(textField)

簡單、明瞭,但有些問題,讓這段 code 不是那麼的好看。

什麼問題呢?

  1. textField」這個詞重複出現了五次。
  2. 整段程式碼沒有階層,只能靠 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 就是在做兩件事:設定 textFieldaddSubview。這個方法雖然比一開始還多了三行,但它讓整個程式碼的架構更清楚易懂、更容易整理,已經算是進步了。再者,它也不需要另外寫 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)
}

}

是不是讓程式碼變得簡潔許多,架構也更清楚了呢?

--

--

Responses (1)