Mvce — 事件驱动 MVC
Mvce 可以发音为 /myo͞oz/。
一个用于将割离的模型、视图和控制器的 MVC 库,用于 UIKit/AppKit。简洁、简单且不影响。
本文档同时提供简体中文版。
原因
UIKit/AppKit 主要关于视图。不要被 Controller
在 UIViewController
/NSViewController
及其子类中的使用误导,它们都是视图,应避免归真正控制器所有的事情,比如网络、模型更新。
如何连接视图、模型和控制器取决于您,UIKit/AppKit 对此没有强制选项。通常,正如(糟糕的)官方示例所展示的,我们定义一个模型,在 UIViewController
/NSViewController
中引用它,然后直接操作模型。它像...魔法一样工作吗?
不,这是没有 C 的 MVC,它是紧密耦合的,不是可重用的(对于跨 UIKit 和 AppKit),如果您在乎,它也是不可测试的。
如何
MVC 的关键思想是将模型、视图和控制器分离。为了连接它们,Mvce 提供了一种替代方法。
让我们先尝尝 Mvce,以下是一个简单的计数器应用
以下所有代码显示在这里(整个项目 在这里)
// 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
内的标签。然后将事件分配器绑定到增加和减少按钮。
您可以使用 KVO(本例中所示)或其他绑定框架/库,例如 ReactiveCocoa 与 ReactiveSwift,RxSwift 来绑定模型到视图。
查看 Example/RandomImage 示例,它使用了 ReactiveCocoa 进行绑定。
解耦视图和控制台
视图内部也没有任何对控制台的引用!View
协议还要求您绑定事件分配器。什么是事件分配器?它只是 (Event) -> Void
的包装器,您可以将其用于发送事件,Mvce 将事件分发给控制台,并通知其更新模型。
将模型、视图和控制台粘合在一起
使用 Mvce.glue(model:view:controller:)
将它们所有粘合在一起,在 UIViewController
/NSViewController
的 loadView
或 viewDidLoad
中注入。生命周期由 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
}
}
就这样了!请记住检查 Example 目录以了解更复杂的一个。
如果您想运行 RandomImage
示例项目,请确保运行 git submodule update --init --recursive
来安装第三方依赖项。
Dispatchable
协议
如果您真的确实需要在视图或控制台中任何地方访问事件分配器,请采用 Dispatchable
。这是最后的手段,我不推荐这种方式,这很容易搞砸代码,违反 MVC 规则。
许可
MIT
作者
- 博客: realazy.com (中文)
- Github: @cxa
- Twitter: @_cxa (主要中文)