Swift 程式碼教學:好好設定物件 讓程式碼更簡潔清晰


本篇原文(標題:如何讓 iOS 程式碼更美觀:物件的設定)刊登於作者 Medium,由 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)
    }

}

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

本篇原文(標題:如何讓 iOS 程式碼更美觀:物件的設定)刊登於作者 Medium,由 Li-Heng Hsu 所著並授權轉載。
作者簡介:Li-Heng Hsu,iOS 開發者、寫作者、filmmaker。現正負責開發 Storyboards by narrativesaw 此一故事板文件 app 中。深深認同 Swift 對於程式碼易讀性的重視。個人網站:lihenghsu.com。電郵:[email protected]

此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This