Better apps. Less code. Get the most out of SwiftUI using MVVMCombine!
Build clean, pixel perfect, and declarative UIs, using the Model-View-ViewModel-Coordinator (MVVM-C) design pattern. MVVMCombine is a framework specifically developed for Apple’s newest framework Combine, alongside SwiftUI, that provides logical streams as the core of Functional Reactive Programming (FRP), with a declarative Swift syntax that’s easy to read and natural to write.
Now with MVVMCombine, the view model is responsible for exposing the data objects from the model in such a way that objects are easily managed and presented. In this respect, the view model is more model than view, and handles most if not all of the view's display logic, and navigation behaviour via coordinators.
- Dependency Injection using property wrappers
- Dynamically register, resolve, or inject services
- Dynamically register and inject view models for each view
- Manage view's lifecycle within its corresponding view model
- Coordinate view's navigation by view model coordinators
- Coordinators for root views, links, sheets, and tab items
- Dynamic callable view model output factory
- Dynamic member lookup view model inputs
- Bind list custom cell views to view items
- Complete Documentation
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Xcode 11.0+
- Swift 5+
CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate MVVMCombine into your Xcode project using CocoaPods, specify it in your Podfile
:
pod 'MVVMCombine'
The following diagram illustrates the basic archtictecture of Model-View-ViewModel-Coordinator (MVVM-C) design pattern:
- Model: refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content.
- View: a structure, layout, and appearance of what a user sees on the screen. It displays a representation of the model and receives the user's interaction with the view (clicks, keyboard, gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.
- View model: an abstraction of the view exposing public properties and commands. Instead of the controller of the MVC pattern, or the presenter of the MVP pattern, MVVM has a binder, which automates communication between the view and its bound properties in the view model. The view model has been described as a state of the data in the model.
- Coordinator: responsible for handling navigation flow, decides when and where to go based on events from ViewModel fired from its corresponding view.
For detailed information about MVVM-C iOS architecture. Please, refer to the following tutorial.
Here are some advantages of using MVVM-C archtictecture:
- A clean separation of different kinds of code should make it easier to go into one or several of those more granular and focused parts and make changes without worrying.
- With MVVM each piece of code is more granular and if it is implemented right your external and internal dependences are in separate pieces of code from the parts with the core logic that you would like to test. That makes it a lot easier to write unit tests against a core logic.
- You have a better chance of making any of those parts more reusable.
- Coordinator simplifies the navigation logic and data sharing between view models.
Register all services used in the project, by subclassing MwxServices
, and overriding registerServices()
.
Register each service as follows:
Mwx.register(BackendService.init).as(BackendProtocol.self).lifeCycle(.weakSingle)
Where lifeCycle
could be either single
, prototype
, weakSingle
, or objectGraph
.
In order to resolve a service:
let backendService = Mwx.resolve(BackendProtocol.self)
And to inject it as instance property, use @Service
as follows:
@Service var backendService: BackendProtocol
Let all views conform to MwxView
, and declare vm
property, to inject its corresponding view model, using @ViewModel
, as follows:
struct HomeView: MwxView
@ViewModel var vm: HomeViewModel
Let the view model conform to MwxObservableViewModel
, and determine its view as a generic type, as follows:
class HomeViewModel: MwxObservableViewModel<HomeView>
Let the body view always be wrapped by a MwxNavigationBody
if navigation view should be used, MwxTabBody
if tab view should be used, otherwise just MwxBody
, and let be bound to its vm
to synchronize the view and its view model lifecycle didAppear
, and didDisappear
, as follows:
var body: some View {
MwxNavigationBody {
Text("Hello World!")
}
.bind(to: vm)
}
Once this is done, you can now override view model lifecycle, as follows:
override func didLoad() {
}
override func didAppear() {
}
override func didDisappear() {
}
Let your list rows conform to MwxCell
, and declare item
property, to determine its corresponding view item, using @ViewItem
, as follows:
struct ListCell: MwxCell
@ViewItem var item: ListViewItem
Let the view item conform to MwxViewItem
, as follows:
class ListViewItem: MwxViewItem
In order to create a new coordinator, determine its view model, and presentation style. Currently 4 presentations are supported, root
, tab
, link
, or sheet
. Declare MwxCoordinator
, as follows:
let detail = DetailViewModel.link(self)
For tab coordinators, override func tabs() -> [MwxTab]
in the view model, to determine which tab are to be coordinated, and to generate corresponding tab items, as follows:
let home = HomeViewModel.tab(self)
let contact = ContactViewModel.tab(self)
override func tabs() -> [MwxTab] {
return [
home,
contact
]
}
For link and sheet coordinators, determine the view which is to be coordinated by this link or sheet, as follows:
Text("Link here!").coordinated(by: vm.detail)
In order to show link and sheet coordinators, use show()
in the view model, use pop()
to deactivate a link, and dismiss()
to deactivate a sheet(), as follows:
func showDetail() {
detail.show()
}
func save() {
pop()
}
In order to declare a callback, when coordinator is deactivated, use onDisappear: (() -> Void)
closure, as follows:
let detail = DetailViewModel
.sheet(self)
.onDisappear {
print("Sheet Dismissed!")
}
Build view model input using output property, MwxOutput
is a dynamic callable, so you can dynamically declare input keys, as follows:
let input = output(name: "Mike",
job: "Engineer")
Pass inputs to view models, within its coordinator declaration, as follows:
let detail = DetailViewModel
.sheet(self)
.with(input)
.onDisappear {
print("Sheet Dismissed!")
}
Alternatively, you could pass it while showing link or sheet coordinators, as follows:
func showDetail() {
detail.show(with: input)
}
Finally, resolve and get any input value for given key, using dynamic member lookup, inside the view model, as follows:
override func didAppear() {
if let name = input.name {
}
}
For tab coordinators, title and image keys are obligatory on their inputs, to render the title and image of its corresponding tab item, as follows:
var contactInput: MwxInput {
output(title: "Contact",
image: "contact")
}
ContactViewModel
.tab(self)
.with(contactInput)
Unicorn is a sample demo, summarizes MVVMCombine main features. Basically, you add, edit, and delete unicorns. Tabs, Link, sheet coordinators are used in the demo.
Before running this demo. Please, go to crudcrud, and copy your special API secret shown in your REST endpoint, then paste it in the secret
getter property in DataUrlService
.
This is very important, so that backend API's work!
The following diagram illustrates coordinators hierarchy managing view navigation within the sample demo:
- Automatic view model registry on runtime
- Automatic view to view model lookup on runtime
- Support custom coordinators
- Support view model publisher inputs
- Provide array to list/form adapters
- Provide dynamic styling engine
- Provide coordinators for iPad split views
I would like to thank:
- crudcrud, for using their wonderful service, to build dynamic CRUD operations with no back-end code!
- LiteCode, for their wonderful SwiftDI, for Dependency Injection using
@propertyWrapper
!
MVVMCombine is released under the MIT license. See LICENSE for details.
Mohamed Salem