A Protocol Oriented MVVM implementation
MVVMKit is a protocol oriented library that defines a clear way to adopt the MVVM software architecture in your iOS applications.
Aside the classical MVVM implementation, MVVMKit also provides the tools for putting the navigation logics inside instances conforming to the Coordinator
protocol.
When you use MVVMKit most of your software types will belong to one of the following categories:
The actual data manipulated by your applications.
Model types can be NSManagedObject
fetched from a Core Data database or Codable
instances coming from a web API.
Model’s responsibilities:
- Maintain the state of the application
The actual user interface.
On iOS views are typically subclasses of UIView
.
UIView’s responsibilities:
- Show the application content
- Deliver the user interaction to a
UIViewController
A subclass of UIViewController
.
UIViewController’s responsibilities
- Deliver the user interaction to the View Model
- Bind the View Model properties to the view
Note: on iOS we divide the View entity of the MVVM pattern in two entities: UIView
and UIViewController
.
The actual "brain" of a scene of your application, containing most of your application’s logic.
View Model’s responsibilities:
- Manage the model
- Present the model in a way that is immediately suitable for the view
- Notify the view controller when it should update the view
The entity responsible for the application navigation.
Coordinator’s Responsibilities
- Decide which is, and how to show the next view controller
- Instantiate view controllers and associated view models doing the appropriate dependency injections
- ⚙️ Protocol Oriented
- 🖼 Reusable View support (cells, supplementary views)
- 🔨 Diffable Data Source ready
- 📌 Combine bindings
- 🧩 Embedding
- 👆🏼 Custom cell interaction
- 🧭 Coordinator support
The view controller forwards the user interaction to the view model.
class RootViewController: UIViewController, ViewModelOwner {
typealias ViewModelType = RootViewModel
var viewModel: RootViewModel!
func bind(viewModel: RootViewModel) { … }
@IBAction func didTapButton(_ sender: UIButton) {
viewModel.didTapButton()
}
}
The view model starts the navigation using the coordinator.
class RootViewModel: ViewModel {
private let coordinator: any RootCoordinatorProtocol
init(coordinator: any RootCoordinatorProtocol) { … }
func didTapButton() {
coordinator.showDestinationViewController()
}
}
The coordinator instantiates and makes the approriate dependency injection in the destination scene.
class RootCoordinator: Coordinator {
let weakViewController: WeakReference<UIViewController>
init(sourceViewController viewController: UIViewController) { … }
func showDestinationViewController() {
let viewController = SomeViewController()
viewController.viewModel = SomeViewModel()
self.viewController?.show(viewController, sender: nil)
}
}
Define the cell and the cell’s view model.
struct MyCellViewModel: ReusableViewViewModel {
let identifier: String = MyCell.identifier
let text: String?
}
class MyCell: UICollectionViewCell, CustomBinder {
typealias ViewModelType = MyCellViewModel
@IBOutlet private var label: UILabel!
func bind(viewModel: ViewModelType) {
label.text = viewModel.text
}
}
Conform the scene’s view model to DiffableCollectionViewViewModel
.
Additionally add the logic for publishing the data source snapshots.
class MyViewModel: DiffableCollectionViewViewModel {
typealias SectionType = Section
private let state: CurrentValueSubject<[MySectionModel], Never> = .init([])
enum Section {
case main
case second
}
var snapshot: AnyPublisher<SnapshotAdapter, Never> {
state
.map(createSnapshot(from:))
.eraseToAnyPublisher()
}
func createSnapshot(from: [MySectionModel]) -> SnapshotAdapter {
// create your snapshot here
.init()
}
}
Subclass MVVMDiffableCollectionViewController
.
class MyViewController: MVVMDiffableCollectionViewController<MyViewModel> { }
Conform the view controller to ContainerViewProvider
.
class ContainerViewController: UIViewController, ViewModelOwner, ContainerViewProvider {
typealias ViewModelType = ContainerViewModel
@IBOutlet weak private var containerView: UIView!
var viewModel: ContainerViewModel!
func view(for kind: ContainerViewKind) -> UIView? {
switch kind {
case .main:
return containerView
}
}
}
Conform the coordinator to EmbedderCoordinator
.
final class ContainerCoordinator: EmbedderCoordinator {
typealias ViewController = ContainerViewController
typealias ContainerViewKind = ContainerViewController.ContainerViewKind
let weakViewController: WeakReference<ContainerViewController>
init(viewController: ContainerViewController) {
weakViewController = .init(viewController)
}
func embedChildViewController(in view: ContainerViewKind) {
let childViewController = ChildViewController()
guard let containerView = viewController?.view(for: view) else {
return
}
// embed here ‘childViewController’ into the ‘containerView’
}
}
Start the embedding process in the view model specifying the desired view identifier.
final class ContainerViewModel: ViewModel {
private let coordinator: any ContainerCoordinatorProtocol
init(coordinator: any ContainerCoordinatorProtocol) { … }
func didTapSomething() {
coordinator.embedChildViewController(in: .main)
}
Conform the cell’s view model to ReusableViewViewModel
.
struct SampleCellViewModel: ReusableViewViewModel {
let identifier: String = SampleCell.identifier
let text: String?
}
Create a delegate for your cell’s custom interaction and conform the cell to CustomDelegator
protocol.
Addtionally you need to conform the cell to CustomBinder
where you make the usual view model’s properties binding.
protocol SampleCellDelegate: class {
func didTapButton(in sampleCell: SampleCell)
}
class SampleCell: UICollectionViewCell, CustomBinder, CustomDelegator {
typealias ViewModelType = SampleCellViewModel
typealias Delegate = SampleCellDelegate
@IBOutlet private weak var titleLabel: UILabel!
func bind(viewModel: SampleCellViewModel) {
titleLabel.text = viewModel.text
}
@IBAction func didTapButton(_ sender: UIButton) {
delegate?.didTapButton(in: self)
}
}
Conform the scene’s view controller to the cell’s delegate protocol to get the events delivered.
extension SampleViewController: SampleCellDelegate {
func didTapButton(in sampleCell: SampleCell) {
guard let indexPath = collectionView.indexPath(for: sampleCell) else { return }
viewModel.didTapButton(at: indexPath)
}
}
Copy the Templates/MVVMKit
folder into ~/Library/Developer/Xcode/Templates
.
The result should be the following:
MVVMKit is available as a Swift Package.
Open the project file inside the folder Example-UIKit
MVVMKit is available under the MIT license. See the LICENSE file for more info.