From MVC to MVVM in Swift


原文連接
編程


過去一年半的時間我一直在作一個項目,它由一個簡單的手機上的新聞閱讀類應用充分發展成適用於手機和平板的虛擬報紙應用。一開始跟從蘋果公司的建議,堅持使用MVC設計模式彷佛是一個好主意。可是在這個應用持續發展的狀況下,它裏面的一些邏輯開始變得複雜,修改代碼時老是伴隨着一種憂慮的感受,修改一部分代碼的問題時懼怕引發其餘部分代碼產生bug。把這僅僅歸咎於MVC是不公平的,很顯然一些問題是由壞的編程習慣、缺少經驗和項目最後期限引發的,可是MVC也有很差的地方。只想着模型、視圖和控制器,你可能會錯過一些有用的機會來把職責進一步劃分,依賴關係圖簡化。swift

這篇文章中的「view」指的是一個用來展現一組數據的屏幕或者它的一部分,例如一個由imageView和label組成的collectionViewCell,或者一個由標題label、日期label和文本內容textView組成的故事板屏幕。換句話說,「view」指的不是UIView。設計模式

展現一個例子,假如咱們須要一個視圖來展現一片新聞。包含文章主題,標題和發佈日期。咱們先無論佈局。安全

MVC狀況下,定義控制器和模型(article)。在控制器裏建立視圖和模型的屬性,在屬性didSet方法裏給視圖設置值(注意:在構造方法裏設置屬性時,屬性觀察不會被調用)。bash

設想另外一種狀況,咱們須要支持另外一種結構的數據模型一個字典(這只是一個假設,用字典作模型是壞的選擇)。網絡

怎麼進行呢?能夠直接定義另外一個屬性,articleAsDictionary。閉包

千萬不要這麼作。由於任何和模型的交互都須要判斷好多if else來肯定用哪一個數據模型,這樣會致使雜亂的和過於複雜的代碼,難以閱讀和發展,容易出bug。架構

你可能會想着把字典articleAsDictionary轉換成模型Article,可是若是Article是core data中一個受管理的對象呢?沒有一個受管理對象環境咱們不能進行初始化。定義一個全部文章模型都遵照的協議也不太適用於咱們的字典,也不支持咱們隨後介紹的一個特點。mvc

開始解決以前,考慮另外一件事情。咱們的控制器裏目前須要作的一些事情:把NSDate轉換成string來顯示,須要經過網絡下載圖片處理圖片讓imageView來顯示,監聽屏幕方向改變來調整視圖,處理按鈕點擊和視圖滑動,處理手勢,關心狀態保持,控制狀態欄導航欄,管理內存。把模型處理邏輯的負擔添加給控制器是應該避免的。這是在MVVM模式裏第二個咱們將會處理的問題。異步

在架構設計中你的目標應該是簡單的部件(類)。要達到目的最終通常會限制類的角色和輸入。這就是咱們接下來將要對控制器類作的。咱們將會把全部模型處理的邏輯提取出來,而且明確的定義輸入須要的類型。最優雅的實現方式是定義一個協議來講明view(controller)真正地展現什麼。那就是咱們的viewModel。

protocol ArticleViewViewModel {
  var title: String { get }
  var body: String { get }
  var date: String { get }
}
複製代碼

如你所見,協議中全部類型都是String,可以直接展現。而且這些屬性只有獲取方法,沒有設置方法。

MVVM能夠看作是Model-View-ViewModel,可是咱們這裏所構建的看作Model-View-ViewController-ViewMode更合適。因爲view和controller如此緊密的被viewController聯繫在一塊兒,咱們把viewController也當作「view」。

這樣一來,controller裏面只須要把viewmodel的屬性賦值給界面元素。咱們讓view controller獨立於model,咱們扔掉了全部醜陋的事情,好比說日期文字轉換,模型適配。這樣很棒,可是咱們仍然須要真正地數據,這將會在一個實實在在的viewmodel裏實現。

咱們建立一個新類ArticleViewViewModelFromArticle來遵照ArticleViewViewModel協議,它須要一個article對象做爲輸入

class ArticleViewViewModelFromArticle: ArticleViewViewModel {
  let article: Article
  let title: String
  let body: String
  let date: String
  
  init(_ article: Article) {
    self.article = article
    
    self.title = article.title
    self.body = article.body
    
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
    self.date = dateFormatter.stringFromDate(article.date)
  }
}複製代碼
let article: Article = /* some article */
let viewModel = ArticleViewViewModelFromArticle(article)
let viewController = ArticleViewController()
viewController.viewModel = viewModel複製代碼

控制器不須要知道model,它所接受的是遵照viewmodel協議的一些類型,全部的模型處理過程在viewmodel裏面,viewController能夠接受任何種類的viewmodel對象,只要他們遵照viewmodel協議。

咱們實現的這個解決方案只適用於特定的場景。爲何?由於咱們的viewmodel類裏面的屬性都設置爲了常量,而不是變量,是不可改變的。若是咱們把這些屬性都設置成變量,也無濟於事。讓咱們經過給article view添加一個縮略圖imageview來澄清這件事。這樣就須要一個image做爲數據,可是咱們只有一個url,怎麼作?咱們不關心。咱們須要image類型的對象,所以咱們的viewmodel就添加一個image屬性。咱們擴展一下viewmodel協議,讓它可以提供image。同時擴展viewController。

protocol ArticleViewViewModel {
  var title: String { get }
  var body: String { get }
  var date: String { get }
  var thumbnail: UIImage? { get }
}複製代碼
class ArticleViewController: UIViewController {
  var bodyTextView: UITextView
  var titleLabel: UILabel
  var dateLabel: UILabel
  var thumbnailImageView: UIImageView

  var viewModel: ArticleViewViewModel {
    didSet {
      titleLabel.text = viewModel.title
      bodyTextView.text = viewModel.body
      dateLabel.text = viewModel.date
      thumbnailImageView.image = viewModel.thumbnail
    }
  }
}複製代碼

很簡單。注意咱們把image聲明爲了一個可選類型。待一下子就能知道緣由。(若是你但願有的文章沒有縮略圖,你也會這麼作的,但這並非咱們這麼作的緣由)

誰提供這個圖片?是一個實體viewmodel,咱們擴展這個viewmodel,讓它從模型article提供的url下載圖片。

class ArticleViewViewModelFromArticle {
  let article: Article
  let title: String
  let body: String
  let date: String
  var thumbnail: UIImage?
  
  init(_ article: Article) {
    self.article = article
    
    self.title = article.title
    self.body = article.body
    
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
    self.date = dateFormatter.stringFromDate(article.date)
    
    let downloadTask = NSURLSession.sharedSession().downloadTaskWithURL(article.thumbnail) {
      [weak self] location, response, error in
      if let data = NSData(contentsOfURL: location) {
        if let image = UIImage(data: data) {
          self?.thumbnail = image
        }
      }
    }

    downloadTask.resume()
  }
}複製代碼

棒極了!這管用嗎?NO!由於異步下載圖片須要必定的時間,當圖片下載完成並給viewmodel的屬性賦值的時候,咱們的viewController早已經把viewmodel的值賦給自身的視圖了。咱們須要一種方式來傳遞這個改變。

咱們能夠發通知給viewcontroller,可是那樣的話沒有可擴展性,使得代碼難以閱讀和跟進,而且須要額外注意註冊和註銷監聽的時機。另外一種方式是爲viewmodel定義一個代理協議,讓viewcontroller去遵照它,這樣更安全些,可是它引入了沒必要要的依賴,並且比發送通知更加缺少可擴展性(想象一下給viewmodel裏的每一個對象定義一個通知方法,並在viewcontroller裏面實現他們那種痛苦)。固然,這些方式咱們都不會用。咱們須要的是一種在屬性層面的綁定機制。每當屬性值改變的時候,咱們都須要收到通知。

這樣的機制叫作綁定。有幾種方法能夠實現。你能夠用KVO機制來實現,可是它並非swift原生的,會產生許多樣板代碼,而且難於debug。viewmodel類必須繼承NSObject,還須要很是注意註冊監聽和註銷監聽。

另外一個解決方案是ReactiveCocoa,它是一個啓發自函數響應式編程的範例,綁定是它的核心內容。可是它是以OC的編程思想建立的(雖然swift也能使用)。除非你也使用它作其餘的一些事情,不然引入它就顯得太多的。

爲了讓viewmodel不可變,咱們將要嘗試一些更加swift的東西。下一篇文章,利用泛型和閉包的力量,咱們將實現一個簡單的綁定機制。

相關文章
相關標籤/搜索