[譯]Bindings, Generics, Swift and MVVM

本文是譯文。原文連接html


上一篇文章我已經介紹了MVVM設計模式做爲一種對MVC的發展,可是最終我提出的解決方案只覆蓋了特定的場景----不可變的modelviewmodel。爲了覆蓋剩餘的場景,咱們須要可變的viewmodel來把變化傳遞給views或者是viewcontrollersios

這篇文章我將經過使用Swift泛型和閉包來實現觀察模式,從而展現簡單的綁定機制。swift

基本數據類型對象和原始類型的值都沒有給咱們提供觀察他們改變的方法。爲了這麼作咱們必須控制它們的setter(或者設置它們的方式),在那裏通知那些對它感興趣的對象。很幸運,Swift足夠機智,不容許那樣作。擁有那種程度的自由度將會快速地致使出錯。然而建立咱們本身的數據類型而且以咱們但願的方式定製它是可行的。咱們可讓它們包含基礎數據和原始類型。這將會使得修改被包含類型的值時須要經過咱們的類型的接口,那就是咱們能夠作一些知足咱們需求的事情的地方。讓咱們以一個基本的String類型嘗試作一下。咱們把這個新類型叫作DynamicString設計模式

class DynamicString {
  var value: String {
    didSet {
      println("did set value to \(value)")
    }
  }
  
  init(_ v: String) {
    value = v
  }
}複製代碼

咱們給value的屬性觀察器附上了一些代碼。下面是一個它怎麼工做的例子:數組

let name = DynamicString("Steve")   // does not print anything
println(name.value)  // prints: Steve
name.value = "Tim"   // prints: did set value to Tim
println(name.value)  // prints: Tim複製代碼

如你所見,給value賦新值觸發了它的屬性觀察,打印了那個值。這就是咱們身披銀甲所向披靡的騎士。改變發生時咱們將會使用屬性觀察通知感興趣的團體,讓咱們把它叫作listenerslisteners是什麼呢?在咱們上篇文章MVVM的例子裏它是viewcontrollerviewcontrollerviewmodel的改變感興趣,這樣它可以對本身包含的視圖進行對應的更新。可是咱們想要從每一個咱們建立的自定義string對象引用viewcontroler嗎?我不但願這樣。也許咱們能夠建立一個listener協議,讓每一個listener來聽從它。這行不通----listeners可能想要監聽許多其餘對象(屬性)。咱們須要另外一個騎士。swift就有,它叫閉包(Object-C裏叫block,其餘語言裏叫lambda)。咱們能夠把listener定義爲一個接受String類型參數的閉包。bash

class DynamicString {
  typealias Listener = String -> Void
  var listener: Listener?

  func bind(listener: Listener?) {
    self.listener = listener
  }

  var value: String {
    didSet {
      listener?(value)
    }
  }
  
  init(_ v: String) {
    value = v
  }
}複製代碼

咱們用typealias命令生成了一個新的類型,Listener,它是一個接受String類型參數並無返回值得閉包。聲明瞭一個Listener類型的屬性,它是可選類型,所以並非必須設置的(咱們的DynamicString類型並非必須有一個收聽者)。而後咱們給listener創造了一個setter,只是爲了讓語法更漂亮些。最後咱們修改了屬性觀察器,當新值被設置時調用那個listener閉包。就是這了,讓咱們看看例子:閉包

let name = DynamicString("Steve")

name.bind({
  value in
  println(value)
})

name.value = "Tim"  // prints: Tim
name.value = "Groot" // prints: Groot複製代碼

這樣,每次咱們給DynamicString對象設置新值的時候,listener被觸發,打印了那個值。注意一下咱們的綁定語法看起來並不太好。很幸運,Swift充滿了語法糖,其中有兩個能夠幫助咱們。第一個,若是一個函數的最後一個參數是一個閉包,這個閉包表達式能夠被定義在圓括號調用參數以後。這樣的閉包叫作尾隨閉包。另外,若是函數只有一個參數,那麼圓括號能夠徹底省略。第二個語法糖是,Swift自動給內聯閉包提供縮寫的參數名,能夠用$0,$1,$2這樣的名稱來引用閉包的參數值。利用這些知識咱們獲得了這個:app

name.bind {
  println($0)
}複製代碼

這樣更漂亮些!咱們能夠隨意實現listener的閉包。除了打印新值,咱們可讓它更新label的文字。mvvm

let name = DynamicString("Steve")
let nameLabel = UILabel()

name.bind {
  nameLabel.text = $0
}

println(nameLabel.text)  // prints: nil

name.value = "Tim"
println(nameLabel.text)  // prints: Tim

name.value = "Groot"
println(nameLabel.text)  // prints: Groot複製代碼

如你所見,每次name值改變的時候,label的文字都會更新,可是第一次呢?Steve去哪了?咱們不該該忘記他。若是你思考一小會兒,你就會注意到bind方法只是設置了收聽者,可是並無觸發它。咱們能夠實現另外一個方法來實現它。咱們把它稱做bindAndFire函數

class DynamicString {
  ...
  func bindAndFire(listener: Listener?) {
    self.listener = listener
    listener?(value)
  }
  ...
}複製代碼

若是咱們用這個方法來修改咱們的例子,咱們就把Steve找回來了。

...
name.bindAndFire {
  nameLabel.text = $0
}

println(nameLabel.text)  // prints: Steve
...複製代碼

很棒啊,咱們走過了很長一大段路。咱們引入了一個新的string類型,它容許咱們給它綁定一個收聽者來觀察值的變化,咱們已經展現了它如何執行指定的動做例如更新label文字。

可是String類型並非咱們要使用的惟一一種類型,所以讓咱們繼續用這個方法來擴展到其餘類型。咱們能夠建立一些類似的類,對於Integer...嗯...而後是 Float, Double and Boolean?那麼還有NSDate, UIView or dispatch_queue_t?這些彷佛至關痛苦啊……的確,若是就這麼作咱們會瘋掉的!

相反,咱們將請出Swift最強大的特性之一----泛型。它讓咱們可以寫出靈活的可複用的函數和類型,它們能夠運用於任何類型。若是你不熟悉泛型,就打開這個連接Generics去摟一眼吧。而後咱們會把DynamicString類型重寫爲Dynamic這個泛型。

看起來大概這個樣子:

class Dynamic<T> {
  typealias Listener = T -> Void
  var listener: Listener?
  
  func bind(listener: Listener?) {
    self.listener = listener
  }
  
  func bindAndFire(listener: Listener?) {
    self.listener = listener
    listener?(value)
  }

  var value: T {
    didSet {
      listener?(value)
    }
  }
  
  init(_ v: T) {
    value = v
  }
}複製代碼

咱們把DynamicString類重命名爲Dynamic,經過在類名後面添加<T>把它標記爲一個泛型類而且把全部的String類型名改成T。如今咱們的Dynamic類型能夠包括全部其餘類型,而且給它擴展了收聽者機制。

這裏是一些🌰:

let name = Dynamic<String>("Steve")
let alive = Dynamic<Bool>(false)
let products = Dynamic<[String]>(["Macintosh", "iPod", "iPhone"])複製代碼

吃不吃驚。意不意外。它能夠變得更好。Swift編譯器如此強大,它能夠從函數的(這個例子裏是構造器的)參數推斷類型,所以,只要寫成這樣就好了:

let name = Dynamic("Steve")
let alive = Dynamic(false)
let products = Dynamic(["Macintosh", "iPod", "iPhone"])複製代碼

綁定照常運行。收聽者閉包裏的參數類型就是泛型列表裏指定的那一個(或者列表忽略時編譯器推斷出來)。例如:

products.bindAndFire {
  println("First product is \($0.first)")
}複製代碼

這就是咱們的綁定機制。很簡單,也很強大。它適用於任何類型,你能夠綁定任何你須要的邏輯。而且你不須要經歷註冊和註銷監聽的痛苦。你僅僅是綁定一個閉包。然而仍是有個限制----你只能有一個收聽者。對於咱們的MVVM例子和大多數狀況來講這已經足夠了,可是你須要經過改進這個想法好比擁有一個收聽者數組來支持多個收聽者嗎----這是可行的可是可能引入其餘的一些後果。

最後,讓咱們修復上一篇中的MVVM例子讓縮略圖可以傳遞給imageView。咱們能夠從新定義viewmodel協議,讓它的屬性支持綁定,也就是說Dynamic。咱們能夠把它們都這樣作,展現一下是怎麼完成的。

protocol ArticleViewViewModel {
  var title: Dynamic<String> { get }
  var body: Dynamic<String> { get }
  var date: Dynamic<String> { get }
  var thumbnail: Dynamic<UIImage?> { get }
}複製代碼

記着那裏的可選類型。被包裹的類型是可選的,而不是Dynamic這個包裹!下面咱們繼續修改viewmodel

class ArticleViewViewModelFromArticle: ArticleViewViewModel {
  let article: Article
  let title: Dynamic<String>
  let body: Dynamic<String>
  let date: Dynamic<String>
  let thumbnail: Dynamic<UIImage?>
  
  init(_ article: Article) {
    self.article = article
    
    self.title = Dynamic(article.title)
    self.body = Dynamic(article.body)
    
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
    self.date = Dynamic(dateFormatter.stringFromDate(article.date))
    
    self.thumbnail = Dynamic(nil)
    
    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.value = image
        }
      }
    }
    
    downloadTask.resume()
  }
}複製代碼

這應該是很直觀的,可是請注意一些事情。全部的屬性仍然是常量(定義爲let)。這很重要,由於咱們一旦咱們給它們賦一次值,就不能改變。Dynamic的值改變時,收聽者會收到通知,可是並非Dynamic它本身改變的時候。這意味着咱們必須在構造器裏(初始化方法)初始化它們全部。這裏有一條黃金法則:那些不能再構造器裏初始化爲真實值的Dynamics必須包裹可選類型。就像thumbnail包裹了可選的UIImage。在這種狀況下,咱們用nil來初始化Dynamic,而後當真實值或者新值可用時再更新它----好比當thumbnail下載完成的時候。

接下來要作的就是在viewcontroller裏綁定全部的屬性:

class ArticleViewController {
  var bodyTextView: UITextView
  var titleLabel: UILabel
  var dateLabel: UILabel
  var thumbnailImageView: UIImageView
  
  var viewModel: ArticleViewViewModel {
    didSet {
      viewModel.title.bindAndFire {
        [unowned self] in
        self.titleLabel.text = $0
      }
      
      viewModel.body.bindAndFire {
        [unowned self] in
        self.bodyTextView.text = $0
      }
      
      viewModel.date.bindAndFire {
        [unowned self] in
        self.dateLabel.text = $0
      }
      
      viewModel.thumbnail.bindAndFire {
        [unowned self] in
        self.thumbnailImageView.image = $0
      }
    }
  }
}複製代碼

就是這樣!咱們的視圖會反射任何viewmodel的變化。注意使用閉包時不要循環引用。在閉包里老是使用unowned或者weak self。我麼這裏使用unowned就行,由於viewcontrollerviewmodel的擁有者,viewmodel不會存活的比viewcontroller更長。

相關文章
相關標籤/搜索