[译]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更长。

相关文章
相关标签/搜索