Mvce 5.0.0

Mvce 5.0.0

陈泉安 维护。



Mvce 5.0.0

  • 作者
  • 陈泉安

Mvce — 事件驱动 MVC

Mvce 可以发音为 /myo͞oz/

一个用于将割离的模型、视图和控制器的 MVC 库,用于 UIKit/AppKit。简洁、简单且不影响。

本文档同时提供简体中文版

原因

UIKit/AppKit 主要关于视图。不要被 ControllerUIViewController/NSViewController 及其子类中的使用误导,它们都是视图,应避免归真正控制器所有的事情,比如网络、模型更新。

如何连接视图、模型和控制器取决于您,UIKit/AppKit 对此没有强制选项。通常,正如(糟糕的)官方示例所展示的,我们定义一个模型,在 UIViewController/NSViewController 中引用它,然后直接操作模型。它像...魔法一样工作吗?

不,这是没有 C 的 MVC,它是紧密耦合的,不是可重用的(对于跨 UIKit 和 AppKit),如果您在乎,它也是不可测试的。

如何

MVC 的关键思想是将模型、视图和控制器分离。为了连接它们,Mvce 提供了一种替代方法。

让我们先尝尝 Mvce,以下是一个简单的计数器应用

iOS Sample App

以下所有代码显示在这里(整个项目 在这里

// CounterModel.swift:
// Model to represent count
final class CounterModel: NSObject {
  @objc dynamic var count = 0
}

// CounterController.swift:
// Event to represent behavior for button ++ and --
enum CounterEvent {
  case increment
  case decrement
}

// Controller to represent how to update model
struct CounterController: Mvce.Controller {
  typealias Model = CounterModel
  typealias Event = CounterEvent

  func update(model: Model, for event: Event, dispatcher: Dispatcher<Event>) {
    switch event {
    case .increment:
      model.count += 1
    case .decrement:
      model.count -= 1
    }
  }
}

// ViewContorller.swift:
// View to represent model state, and emit event to notify controller to update model
final class ViewController: UIViewController {
  @IBOutlet weak var label: UILabel!
  @IBOutlet weak var incrButton: UIButton!
  @IBOutlet weak var decrButton: UIButton!

  override func viewDidLoad() {
    super.viewDidLoad()
    Mvce.glue(model: CounterModel(), view: self, controller: CounterController())
  }
}

extension ViewController: View {
  typealias Model = CounterModel
  typealias Event = CounterEvent

  func bind(model: Model, dispatcher: Dispatcher<Event>) -> View.BindingDisposer {
    let observation = model.bind(\CounterModel.count, to: label, at: \UILabel.text) { String(format: "%d", $0) }
    let action = ButtonAction(sendEvent: dispatcher.send(event:))
    incrButton.addTarget(action, action: #selector(action.incr(_:)), for: .touchUpInside)
    decrButton.addTarget(action, action: #selector(action.decr(_:)), for: .touchUpInside)
    let key: StaticString = #function
    objc_setAssociatedObject(self, key.utf8Start, action, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // Need to retain target
    return observation.invalidate
  }
}

// ButtonAction.swift:
// Helper for button actions
final class ButtonAction: NSObject {
  let sendEvent: (CounterEvent) -> Void

  init(sendEvent: @escaping (CounterEvent) -> Void) {
    self.sendEvent = sendEvent
  }

  @objc func incr(_ sender: Any?) {
    sendEvent(.increment)
  }

  @objc func decr(_ sender: Any?) {
    sendEvent(.decrement)
  }
}

解耦视图和模型

仔细查看我们的 ViewController,其中没有对模型的任何引用!只需采用 View 协议并将模型计数绑定到 func bind(model: Model, dispatcher: Dispatcher) -> View.BindingDisposer 内的标签。然后将事件分配器绑定到增加和减少按钮。

您可以使用 KVO(本例中所示)或其他绑定框架/库,例如 ReactiveCocoa 与 ReactiveSwift,RxSwift 来绑定模型到视图。

查看 Example/RandomImage 示例,它使用了 ReactiveCocoa 进行绑定。

解耦视图和控制台

视图内部也没有任何对控制台的引用!View 协议还要求您绑定事件分配器。什么是事件分配器?它只是 (Event) -> Void 的包装器,您可以将其用于发送事件,Mvce 将事件分发给控制台,并通知其更新模型。

将模型、视图和控制台粘合在一起

使用 Mvce.glue(model:view:controller:) 将它们所有粘合在一起,在 UIViewController/NSViewControllerloadViewviewDidLoad 中注入。生命周期由 Mvce 管理的。

跨平台(iOS & macOS)?

当然,这是真正的 MVC 优势!模型和控制台可以共享,只需重写平台无关的视图。

// macOS/viewController.swift
class ViewController: NSViewController {
  @IBOutlet weak var label: NSTextField!
  @IBOutlet weak var incrButton: NSButton!
  @IBOutlet weak var decrButton: NSButton!

  override func viewDidLoad() {
    super.viewDidLoad()
    Mvce.glue(model: CounterModel(), view: self, controller: CounterController())
  }
}

extension ViewController: View {
  typealias Model = CounterModel
  typealias Event = CounterEvent

  func bind(model: Model, dispatcher: Dispatcher<Event>) -> View.BindingDisposer {
    let observation = model.bind(\.count, to: label, at: \.stringValue) { String(format: "%d", $0) }
    let action = ButtonAction(sendEvent: dispatcher.send(event:))
    incrButton.target = action
    incrButton.action = #selector(action.incr(_:))
    decrButton.target = action
    decrButton.action = #selector(action.decr(_:))
    let key: StaticString = #function
    objc_setAssociatedObject(self, key.utf8Start, action, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // Need to retain target
    return observation.invalidate
  }
}

macOS Sample App

就这样了!请记住检查 Example 目录以了解更复杂的一个。

如果您想运行 RandomImage 示例项目,请确保运行 git submodule update --init --recursive 来安装第三方依赖项。

Dispatchable 协议

如果您真的确实需要在视图或控制台中任何地方访问事件分配器,请采用 Dispatchable。这是最后的手段,我不推荐这种方式,这很容易搞砸代码,违反 MVC 规则。

许可

MIT

作者