[译]Bindings, Generics, Swift and MVVM

2,000 阅读6分钟

本文是译文。原文链接


上一篇文章我已经介绍了MVVM设计模式作为一种对MVC的发展,但是最终我提出的解决方案只覆盖了特定的场景----不可变的modelviewmodel。为了覆盖剩余的场景,我们需要可变的viewmodel来把变化传递给views或者是viewcontrollers

这篇文章我将通过使用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类型参数的闭包。

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这样的名称来引用闭包的参数值。利用这些知识我们得到了这个:

name.bind {
  println($0)
}

这样更漂亮些!我们可以随意实现listener的闭包。除了打印新值,我们可以让它更新label的文字。

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更长。