[譯] 重寫 loadView() 方法使 Swift 檢視程式碼更加簡潔

RickeyBoy發表於2018-08-07

究竟選擇使用 Storyboards 還是純程式碼書寫 view 是非常主觀的事情。在對兩種方式都進行了嘗試之後,我個人支援使用純程式碼書寫 view 來完成專案,這樣能夠允許多人編輯相同的類而不產生討厭的衝突,也更方便進行程式碼審查。

在最開始練習純程式碼寫 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 之中,而這沒有任何好處。

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 只應該起到前後傳遞資訊的路由作用

在一個大部分程式碼都是關於自身 View 的檢視程式碼專案中,能夠清晰地拆分你的架構中各部分的職責,對於一個便於維護的專案來說非常重要。你要讓你真正構建檢視部分的程式碼完全和你的 ViewController 分離 -- 幸運的是有一個簡單的方法,就是重寫 UIViewController 中原生的 View 屬性。這樣做允許你在分離的檔案中管理你的多個 View,同時也仍能保證你的 ViewController 不用去設定任何 View。

loadView()

loadView()UIViewController 中並不常見的一個方法,但它是 ViewController 的生命週期中非常重要的一部分,因為它承擔著最開始載入出 view 屬性的責任。當使用 Storyboard 的時候,它會載入出 nib 並將其附加給 view,但當手動初始化 ViewController 時,這個方法所做的一切就是建立出一個空的 UIView。你可以重寫這個方法並改變它的行為,並且在 ViewController 的 view 上新增任何型別的 view。

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 設定外部約束!

現在,view 成為了我自定義的 view(在本例中為 MyView)的一個引用。你可以在這個 view 獨立的檔案內部構建其所有功能,並且 ViewController 對此毫無許可權。太棒了!

為了獲取 MyView 中的內容,你可以將 View 強制轉換為你自己的型別:

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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章