MVVM(Model View ViewModel)是MVC(Model View Controller)的變種,用來解決MVC中龐大復雜的控制器難以維護的問題。壹般來說,MVVM有幾個要求:
MVVM和MVC有許多相似的特點,主要區別是:
另壹方面,MVVM默認視圖和視圖控制器之間存在壹對壹的關系。壹般我們把這兩個看成壹個整體,會以。swift文件和故事板。
視圖模型的工作是處理顯示數據的所有邏輯。如果模型中有NSDate對象,將使用NSDateFormatter設置視圖模型中日期的顯示形式。
視圖模型不能接觸用戶界面的任何部分,並且視圖模型文件不應該導入UIKit。視圖控制器將觀察視圖模型,以知道何時顯示新數據(通過KVO或FRP(功能反應式編程))。
MVVM和MVC有同樣的弱點:沒有明確的定義把網絡請求部分放在哪裏。在實際操作過程中,我會把網絡請求放在視圖模型文件中,但是後來我打算把網絡請求放在自己的獨立類中,視圖模型文件就會有這個對象。
下面主要說說MVVM在實際應用中的壹些挑戰:
比如妳要構造這樣壹個通用的界面,在屏幕的頂部有壹個segment控件,屏幕的其他部分是壹個集合視圖。選擇不同的片段將顯示不同風格的集合視圖和元素的排列順序。我們定義壹個枚舉來枚舉所有的排列樣式:
那麽在MVVM模式下,這個枚舉應該放在哪裏呢?因為這個enum決定了數據排列的順序,每個單元格中的文本和按鈕的標題都屬於顯示的邏輯,所以看起來這個enum應該放在視圖模型中。
但是,這些布局不會改變要顯示的數據,而只是決定要顯示的數據的排列和順序。從這個角度來看,enum應該放在視圖控制器中。
我的解決方案是將enum放在視圖模型中,然後在視圖模型中添加壹個外部可觀察對象或信號來指示使用哪個布局。基於用戶選擇的段,視圖模型更新該值。然後根據視圖控制器中相應的布局改變集合視圖的樣式,視圖控制器也可以根據這個值決定使用哪個單元格重用標識符。
對於iOS開發人員來說,用MVVM和FRP編寫應用程序時最常見的問題可能是ViewModel如何向ViewController呈現數據。當模型層中的數據發生變化和更新時,需要通知ViewController,然後進行相應的UI更新。我們通常使用兩種機制:
第壹個選項很有吸引力,因為您可以決定如何選擇在視圖控制器中觀察那些屬性。但是,我不推薦在Swift中使用第壹個選項,因為Swift在KVO沒有類型檢查,妳需要多次強制轉換AnyObject的類型。
第二個選擇是對比Swift。基於Swift的泛型特性,信號、序列和可觀察值可以支持編譯期間的類型檢查。
但有時很難將這些信號或可觀測量添加到視圖模型中。Swift的初始化方法對於何時給屬性賦值有非常明確的規則。信號或可觀測量需要使用視圖模型內部的狀態,所以只能在super.init()之後創建,但另壹方面,我們保證在調用super.init()之前,所有屬性都已經賦值,包括那些信號/可觀測量屬性。
這是壹個雞還是蛋的問題。
我采用壹個更簡單的解決方案:將其定義為var的隱式可選類型,這樣就可以在super.init()之後給屬性賦值。這不是壹個完美的解決方案。我們可以用lazy var屬性的閉包賦值來代替上面的方法。在Swift不斷改進和更新的過程中,還可以探索其他更好的方法。
舉壹個非常常見的例子,用戶點擊集合視圖中的壹個單元格,然後跳轉到詳細信息頁面。用戶點擊的操作要在視圖控制器中處理,具體內容是顯示新的詳情頁。但是視圖控制器不能直接接觸模型。如何用MVVM模式實現這樣的用戶交互?
我的解決方案是使用Swift閉包。首先,在視圖模型中定義壹個閉包:
然後將屬性添加到視圖模型中:
然後我需要調用閉包,在視圖模型中定義壹個視圖控制器可以調用的函數。這個函數的參數可以決定使用什麽數據。壹般來說,索引路徑是常用的:
現在,當用戶選擇壹個單元格時,視圖模型中的這個函數將被調用,索引路徑參數將被傳入。視圖模型決定使用哪些數據,並調用視圖控制器中定義的閉包,例如:
最後壹個問題是如何創建這個視圖模型。我們需要給視圖模型的初始化器傳遞壹個閉包,然後使用惰性加載來調用視圖模型的初始化器。