在Presentation Controls中更新Model[翻译]

[翻译]原文链接:MODEL UPDATES IN PRESENTATION CONTROLS

本文是上一篇文章的续集:<简化View Controllers:MVVM 还是 Presentation Controls>(1 in 2)

这篇文章我会解释在iOS中如何使用Presentation Controls处理更新.这是上一篇文章(<简化View Controllers:MVVM 还是 Presentation Controls?>)的延伸,那篇文章我介绍了如何使用Presentation Controls代替MVVM,或者与MVVM结合使用.

在上一篇文章中我没有介绍如何处理更新.但是在屏幕上显示的内容是经常变化的.一下的情况会使这种变化发生,从服务器取到了新的数据,用户的交互操作,或者随着时间自动触发.为了更新屏幕上显示的内容,我们需要通知Presentation Controls,告诉它model对象的发生了改变.

我们继续使用上一篇文章中的Trip对象:

struct Trip {

    let departure: NSDate
    let arrival: NSDate
    let duration: NSTimeInterval

    var actualDeparture: NSDate
    var delay: NSTimeInterval {
        return self.actualDeparture.timeIntervalSinceDate(self.departure)
    }
    var delayed: Bool {
        return delay > 0
    }

    init(departure: NSDate, arrival: NSDate, actualDeparture: NSDate? = nil) {
        self.departure = departure
        self.arrival = arrival
        self.actualDeparture = actualDeparture ?? departure

        // calculations
        duration = self.arrival.timeIntervalSinceDate(self.departure)
    }
}

在computed properties中计算和设置 delay 和delayer属性,代替之前在init中做这些事情.因为接下来的例子中我们要改变actualDeparture属性,而且要显示delay属性的最新值.

如何获取Trip改变的通知呢?一个很好的方法是通过绑定.你可以使用ReactiveCocoa来到这个目的,但是为了保持本文的简洁,我将使用Dynamic类,Srdan Rasic在他的文章< Bindings, Generics, Swift and MVVM >中介绍过这个类(本文的许多事情都是受到他文章的启发,最好去读一下这篇超赞的文章).Dynamic类看起来是这个样子:

class Dynamic {
  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
  }
}

这个类允许我们注册一个value改变的事件监听,下面是一个这个类的简单例子:

let delay = Dynamic("+5 minutes")
delay.bindAndFire {
    print("Delay: \($0)")
}

delay.value = "+6 minutes" // will print 'Delay: +6 minutes'

我们的Presentation Control通过TripViewViewModel类获取所有需要在view中显示的值.这些属性都是简单的常量,比如String,Bool.我们用Dynamic属性来代替这些不可改变值的属性. 在实际开发中,我们可能需要创建所有的dynmiac属性,从server获取新的Trip数据,然后通过Trip数据设置Dynamic属性的值,但是在我们的例子中我们只改变了actualDeparture属性,并且只为delay和delayed属性创建了dynamic属性.你马上回看到这样做的效果.
我们新的TripViewViewModel现在看起来是这个样子:

class TripViewViewModel {

    let date: String
    let departure: String
    let arrival: String
    let duration: String

    private static let durationShortFormatter: NSDateComponentsFormatter = {
        let durationFormatter = NSDateComponentsFormatter()
        durationFormatter.allowedUnits = [.Hour, .Minute]
        durationFormatter.unitsStyle = .Short
        return durationFormatter
    }()

    private static let durationFullFormatter: NSDateComponentsFormatter = {
        let durationFormatter = NSDateComponentsFormatter()
        durationFormatter.allowedUnits = [.Hour, .Minute]
        durationFormatter.unitsStyle = .Full
        return durationFormatter
    }()

    let delay: Dynamic<String?>
    let delayed: Dynamic

    var trip: Trip

    init(_ trip: Trip) {
        self.trip = trip

        date = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle)
        departure = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle)
        arrival = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle)

        duration = TripViewViewModel.durationShortFormatter.stringFromTimeInterval(trip.duration)!

        delay = Dynamic(trip.delayString)
        delayed = Dynamic(trip.delayed)
    }

    func changeActualDeparture(delta: NSTimeInterval) {
        trip.actualDeparture = NSDate(timeInterval: delta, sinceDate: trip.actualDeparture)

        self.delay.value = trip.delayString
        self.delayed.value = trip.delayed
    }

}

extension Trip {

    private var delayString: String? {
        return delayed ? String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), TripViewViewModel.durationFullFormatter.stringFromTimeInterval(delay)!) : nil
    }
}

通过changeActualDeparture方法我们可以增加或者减少trip.actualDeparture的值.因为trip的delay和delayed属性现在是computed properties,他们的返回值也会被更新.我们通过他们给TripViewViewModel类中的 Dynamic delay 和 Dynamic delayed 属性设置新的值.格式化delay字符串的逻辑也挪到了Trip类的extension中,避免code复制.

现在我们所需要做的工作是是在TripPresentationControl类中做绑定:

class TripPresentationControl: NSObject {

    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var departureTimeLabel: UILabel!
    @IBOutlet weak var arrivalTimeLabel: UILabel!
    @IBOutlet weak var durationLabel: UILabel!
    @IBOutlet weak var delayLabel: UILabel!

    var tripModel: TripViewViewModel! {
        didSet {
            dateLabel.text = tripModel.date
            departureTimeLabel.text = tripModel.departure
            arrivalTimeLabel.text  = tripModel.arrival
            durationLabel.text = tripModel.arrival

            tripModel.delay.bindAndFire { [unowned self] in
                self.delayLabel.text = $0
            }

            tripModel.delayed.bindAndFire { [unowned self] delayed in
                self.delayLabel.hidden = !delayed
                self.departureTimeLabel.textColor = delayed ? .redColor() : UIColor(red: 0, green: 0, blue: 0.4, alpha: 1.0)
            }
        }
    }
}

虽然所有代码可以正常编译了,但是我们的工作还没结束.我们需要一些改变delay(晚点)的方法. 增加两个buttons在我们的view可以实现这个目标.一个button用来增加delay1分钟,另外一个用来减少delay一分钟.处理buttons的tap事件在view controller中,我们不希望Presentation Control做任何用户交互的工作.我们最终的view controller现在看起来是这个样子:

class ViewController: UIViewController {

    @IBOutlet var tripPresentationControl: TripPresentationControl!

    let tripModel = TripViewViewModel(Trip(departure: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493)))

    override func viewDidLoad() {
        super.viewDidLoad()

        tripPresentationControl.tripModel = tripModel
    }

    @IBAction func increaseDelay(sender: AnyObject) {
        tripModel.changeActualDeparture(60)
    }

    @IBAction func decreaseDelay(sender: AnyObject) {
        tripModel.changeActualDeparture(-60)
    }
}

当我们点击页面上按钮的时候,现在可以以优雅的方式去更新view了.我们的view controller把model的改变告诉TripViewViewModel, TripViewViewModel吧model的变化告诉TripPresentationControl,TripPresentationControl利用model的变化更新UI.  这种方式中,Presentation Control不知道任何用户交互的内容,用户交互操作后view controller不知道任何UI组件的更新.

希望这篇文章能让你对Presentation Controls 和 MVVM的使用有更好的理解.就想在上一篇文章中提到的,

我建议你读Ash Furrow的 Introduction to MVVM , 和 Srdan Rasic 的From MVC to MVVM in Swift

最好加入 11月9日 在 Amsterdam 的 do {iOS} 研讨会 ,在这里Natasha “the Robot” Murashev(代码女神)讲做一个关于 Protocol-oriented MVVM 的讲座.如果你喜欢本文,你肯定想知道她关于MVVM的讲解.

 

系列文章:

Simplification of iOS View Controllers: MVVM or Presentation Controls? (1 in 2)

在Presentation Controls中更新Model(2 in 2)


 

本菜感觉Presentation虽然可以降低VM的复杂度但是关系的复杂度提升了,需要维护关系的成本大大提高了.总体不如单纯使用MVVM简洁.请指教.