{"id":20010101,"url":"https://github.com/cxa/mvce","last_synced_at":"2025-05-04T19:36:01.077Z","repository":{"id":56935311,"uuid":"138390090","full_name":"cxa/Mvce","owner":"cxa","description":"An event driven MVC library to glue decoupled Model, View, and Controller for UIKit/AppKit. Minimal, simple, and unobtrusive.","archived":false,"fork":false,"pushed_at":"2019-03-12T05:26:20.000Z","size":254,"stargazers_count":23,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-17T17:24:56.707Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cxa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-06-23T09:53:16.000Z","updated_at":"2022-08-01T15:44:22.000Z","dependencies_parsed_at":"2022-08-21T01:10:14.769Z","dependency_job_id":null,"html_url":"https://github.com/cxa/Mvce","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cxa%2FMvce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cxa%2FMvce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cxa%2FMvce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cxa%2FMvce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cxa","download_url":"https://codeload.github.com/cxa/Mvce/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252390801,"owners_count":21740388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T07:18:28.449Z","updated_at":"2025-05-04T19:36:00.396Z","avatar_url":"https://github.com/cxa.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mvce — Event driven MVC\n\nMvce can be pronounced as **/myo͞oz/**.\n\nAn event driven MVC library to glue decoupled Model, View, and Controller for UIKit/AppKit. Minimal, simple, and unobtrusive.\n\n本文档同时提供[简体中文版](README.zh_CN.md)。\n\n## Why\n\nUIKit/AppKit is mainly about view. Don't be misled by the `Controller` in `UIViewController`/`NSViewController` and descendants, they are all views, should be avoided things that belong to a real controller, such as networking, model updating.\n\nHow to glue view, model, and controller is upon to you, UIKit/AppKit has no strong options on that. Typically, as the (bad) official examples show to us, we define a model, refer it inside `UIViewController`/`NSViewController`s, and manipulate the model directly. It works like a...charm?\n\nNo, it's M-VC without C, it's strong coupling, it's not reusable(for crossing UIKit and AppKit), if you care, it's also untestable.\n\n## How\n\nThe key idea of MVC is the separation of Model, View, and Controller. To glue 'em, Mvce provides an alternative way.\n\nLet's take a taste of Mvce first, here is a simple counter app:\n\n![iOS Sample App](Assets/iOSCounterApp.png)\n\nAll code shows below (whole project [here](Example/Counter)):\n\n```swift\n// CounterModel.swift:\n// Model to represent count\nfinal class CounterModel: NSObject {\n  @objc dynamic var count = 0\n}\n\n// CounterController.swift:\n// Event to represent behavior for button ++ and --\nenum CounterEvent {\n  case increment\n  case decrement\n}\n\n// Controller to represent how to update model\nstruct CounterController: Mvce.Controller {\n  typealias Model = CounterModel\n  typealias Event = CounterEvent\n\n  func update(model: Model, for event: Event, dispatcher: Dispatcher\u003cEvent\u003e) {\n    switch event {\n    case .increment:\n      model.count += 1\n    case .decrement:\n      model.count -= 1\n    }\n  }\n}\n\n// ViewContorller.swift:\n// View to represent model state, and emit event to notify controller to update model\nfinal class ViewController: UIViewController {\n  @IBOutlet weak var label: UILabel!\n  @IBOutlet weak var incrButton: UIButton!\n  @IBOutlet weak var decrButton: UIButton!\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n    Mvce.glue(model: CounterModel(), view: self, controller: CounterController())\n  }\n}\n\nextension ViewController: View {\n  typealias Model = CounterModel\n  typealias Event = CounterEvent\n\n  func bind(model: Model, dispatcher: Dispatcher\u003cEvent\u003e) -\u003e View.BindingDisposer {\n    let observation = model.bind(\\CounterModel.count, to: label, at: \\UILabel.text) { String(format: \"%d\", $0) }\n    let action = ButtonAction(sendEvent: dispatcher.send(event:))\n    incrButton.addTarget(action, action: #selector(action.incr(_:)), for: .touchUpInside)\n    decrButton.addTarget(action, action: #selector(action.decr(_:)), for: .touchUpInside)\n    let key: StaticString = #function\n    objc_setAssociatedObject(self, key.utf8Start, action, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // Need to retain target\n    return observation.invalidate\n  }\n}\n\n// ButtonAction.swift:\n// Helper for button actions\nfinal class ButtonAction: NSObject {\n  let sendEvent: (CounterEvent) -\u003e Void\n\n  init(sendEvent: @escaping (CounterEvent) -\u003e Void) {\n    self.sendEvent = sendEvent\n  }\n\n  @objc func incr(_ sender: Any?) {\n    sendEvent(.increment)\n  }\n\n  @objc func decr(_ sender: Any?) {\n    sendEvent(.decrement)\n  }\n}\n```\n\n### Decouple View and Model\n\nTake a careful look at our `ViewController`, there's no any reference to model! Just adopt `View` protocol and bind model's count to label inside `func bind(model: Model, dispatcher: Dispatcher\u003cEvent\u003e) -\u003e View.BindingDisposer`. And bind event dispatcher to the increment and decrement buttons.\n\nYou can use KVO (this example) or other binding framework/library e.g. ReactiveCocoa w/ ReactiveSwift, RxSwift to bind model to view.\n\nCheck [Example/RandomImage](Example/RandomImage), which uses ReactiveCocoa for binding.\n\n### Decouple View and Controller\n\nThere is no any reference to controller inside view too! `View` protocol also requires you bind event dispatcher. What's an event dispatcher? Just a wrapper for `(Event) -\u003e Void`, you can use it to send event, Mvce will dispatch event to controller and inform it to update model.\n\n### Glue Model, View, and Controller together\n\nGlue 'em all with `Mvce.glue(model:view:controller:)`, inject to `loadView` or `viewDidLoad` in `UIViewController`/`NSViewController`. And lifetime is managed by Mvce.\n\n### Cross-Platform (iOS \u0026 macOS)?\n\nSure, that's _REAL_ MVC's advantage! Model and Controller can be shared, only platform-independent view is required to rewrite.\n\n```swift\n// macOS/viewController.swift\nclass ViewController: NSViewController {\n  @IBOutlet weak var label: NSTextField!\n  @IBOutlet weak var incrButton: NSButton!\n  @IBOutlet weak var decrButton: NSButton!\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n    Mvce.glue(model: CounterModel(), view: self, controller: CounterController())\n  }\n}\n\nextension ViewController: View {\n  typealias Model = CounterModel\n  typealias Event = CounterEvent\n\n  func bind(model: Model, dispatcher: Dispatcher\u003cEvent\u003e) -\u003e View.BindingDisposer {\n    let observation = model.bind(\\.count, to: label, at: \\.stringValue) { String(format: \"%d\", $0) }\n    let action = ButtonAction(sendEvent: dispatcher.send(event:))\n    incrButton.target = action\n    incrButton.action = #selector(action.incr(_:))\n    decrButton.target = action\n    decrButton.action = #selector(action.decr(_:))\n    let key: StaticString = #function\n    objc_setAssociatedObject(self, key.utf8Start, action, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) // Need to retain target\n    return observation.invalidate\n  }\n}\n```\n\n![macOS Sample App](Assets/macOSCounterApp.png)\n\nThat's it! Remember to check out [Example](Example) directory for a more complex one.\n\nDon't forget to run `git submodule update --init --recursive` in order to install 3rd dependencies if you want to run the `RandomImage` sample project.\n\n### `Dispatchable` protocol\n\nIf you really, really need to access event dispatcher anywhere in View or Controller, just adopt `Dispatchable`. This's last resort, I don't recommend this way, it's easily to mess up code, violate MVC rules.\n\n## License\n\nMIT\n\n## Author\n\n- Blog: [realazy.com](https://realazy.com) (Chinese)\n- Github: [@cxa](https://github.com/cxa)\n- Twitter: [@\\_cxa](https://twitter.com/_cxa) (Chinese mainly)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcxa%2Fmvce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcxa%2Fmvce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcxa%2Fmvce/lists"}