[譯] 重寫 loadView() 方法使 Swift 視圖代碼更加簡潔

究竟選擇使用 Storyboards 仍是純代碼書寫 view 是很是主觀的事情。在對兩種方式都進行了嘗試以後,我我的支持使用純代碼書寫 view 來完成項目,這樣可以容許多人編輯相同的類而不產生討厭的衝突,也更方便進行代碼審查。html

在最開始練習純代碼寫 view 的時候,人們廣泛遇到的一個問題是最開始不知道將代碼放在哪裏。若是你採用普通 storyboard 的方式,將全部相關代碼都放進你的 ViewController 之中,這樣很容易會最終產生一個巨大的上帝類:前端

final class MyViewController: UIViewController {
    private let myButton: UIButton = {
    	//
    }()
  
  	private let myView: UIView = {
    	//
    }()
  
  	// 其餘 10 個左右的 view
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupViews()
    }
  
  	private func setupViews() {
    	setupMyButton()
      	setupMyView()
      	// 設置其餘的 view
    }
  
  	private func setupMyButton() {
  	    view.addSubview(myButton)
    	// 十行約束代碼
    }
  
    private func setupMyView() {
  	    view.addSubview(myView)
    	// 十行約束代碼
    }
  
  	// 全部其餘的設置
  
  	// 全部 ViewModel 的邏輯
  
  	// 全部 Button 的點擊邏輯等東西...
}
複製代碼

你能夠經過把 view 移動到不一樣的文件並添加引用到原來的 ViewController 之中來改善這樣的狀況,可是你仍然須要用本不該該在 ViewController 中的內容填滿 ViewController,就好比約束代碼和其餘設置 view 的代碼 — 更不用說你如今有兩個 view 屬性(myView 和原生 view)在 ViewController 之中,而這沒有任何好處。android

final class MyViewController: UIViewController {
    
	let myView = MyView()
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupMyView()
    }
  
  	private func setupMyView() {
  	    view.addSubview(myView)
    	// 10 行左右的約束代碼
    	myView.delegate = self
    	// 如今咱們同時有了 view 和 MyView...
    }
}
複製代碼

臃腫的 ViewController 以及邏輯過多的 ViewController 都很是難以管理和維護。在像 MVVM 這樣的架構下,ViewController 應該主要做爲自身的 View 以及 ViewModel 之間的路由器 -- 設置而且約束 View 並非它們的職責,ViewController 只應該起到先後傳遞信息的路由做用ios

在一個大部分代碼都是關於自身 View 的視圖代碼項目中,可以清晰地拆分你的架構中各部分的職責,對於一個便於維護的項目來講很是重要。你要讓你真正構建視圖部分的代碼徹底和你的 ViewController 分離 -- 幸運的是有一個簡單的方法,就是重寫 UIViewController 中原生的 View 屬性。這樣作容許你在分離的文件中管理你的多個 View,同時也仍能保證你的 ViewController 不用去設置任何 View。git

loadView()

loadView()UIViewController 中並不常見的一個方法,但它是 ViewController 的生命週期中很是重要的一部分,由於它承擔着最開始加載出 view 屬性的責任。當使用 Storyboard 的時候,它會加載出 nib 並將其附加給 view,但當手動初始化 ViewController 時,這個方法所作的一切就是建立出一個空的 UIView。你能夠重寫這個方法並改變它的行爲,而且在 ViewController 的 view 上添加任何類型的 view。github

final class MyViewController: UIViewController {
	override func loadView() {
	    let myView = MyView()
	    myView.delegate = self
        view = myView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
		print(view) // 一個 MyView 的實例
	}
}
複製代碼

注意 view 會自動的約束本身到 ViewController 的邊界,因此並不須要爲 myView 設置外部約束!swift

如今,view 成爲了我自定義的 view(在本例中爲 MyView)的一個引用。你能夠在這個 view 獨立的文件內部構建其全部功能,而且 ViewController 對此毫無權限。太棒了!後端

爲了獲取 MyView 中的內容,你能夠將 View 強制轉換爲你本身的類型:bash

var myView: MyView {
    return view as! MyView
}
複製代碼

這樣看起來有點奇怪,但這是由於 view 將仍然被定義爲 UIView 類型,而不是你爲它定義的類型。架構

爲了不個人 ViewController 中重複出現這樣的代碼,我喜歡建立一個 CustomView 協議,並在其中定義包含關聯類型的行爲:

/// HasCustomView 協議爲 UIViewController 定義了一個 customView 屬性,它是爲了去代替普通的 view 屬性。
/// 爲了實現這些,你必須在 loadView() 方法時爲你的 UIViewController 提供一個自定義的 View。
public protocol HasCustomView {
    associatedtype CustomView: UIView
}

extension HasCustomView where Self: UIViewController {
    /// UIViewController 的自定義 view。
    public var customView: CustomView {
        guard let customView = view as? CustomView else {
            fatalError("Expected view to be of type \(CustomView.self) but got \(type(of: view)) instead")
        }
        return customView
    }
}
複製代碼

最終會:

final class MyViewController: UIViewController, HasCustomView {
	typealias CustomView = MyView

	override func loadView() {
	    let customView = CustomView()
	    customView.delegate = self
        view = customView
    }

    override func viewDidLoad() {
    	super.viewDidLoad()
    	customView.render() // 一些 MyView 的方法
	}
}
複製代碼

若是每次都定義這個 CustomView 類型別名會讓你有點煩,那麼你能夠進一步在泛型類中定義這些行爲:

class CustomViewController<CustomView: UIView>: UIViewController {
    var customView: CustomView {
        return view as! CustomView // 由於咱們正在重寫 view,因此永遠不會解析失敗。
    }

    override func loadView() {
        view = CustomView()
    }
}

final class MyViewController: CustomViewController<MyView> {
	override func loadView() {
		super.loadView()
	    customView.delegate = self
    }
}
複製代碼

我我的不太喜歡泛型的方式,由於編譯器並不容許泛型類具備的 @objc 方法的擴展,這會禁止你在擴展中擁有 UITableViewDataSource 之類的協議。可是,除非你須要作一些特殊的事情(好比設置委託),它會容許你跳太重寫 loadView() 這一步,從而能保持 ViewController 的整潔。

結論

重寫 loadView() 是一個讓你的視圖代碼項目更加易於理解、易於維護的好方法,而且我已經使用 HasCustomView 方法得到了很是良好的效果,特別是在最近幾個項目中。編寫視圖部分的代碼也許不是你的選擇,可是它帶來了不少顯而易見的好處。嘗試一下吧,看看它是否是更適合你。

若是你有更好的定義 view 而且不須要 storyboard 的方法,或者你可能有一些疑問、意見或者反饋,請讓我知道。

參考文獻和推薦閱讀

蘋果官方文檔:loadView()

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索