In part 1 we learned about Clean Architecture. In this part 2, we will continue with the MVVM model in the Clean Architecture series and MVVM on iOS (Swift).
MVVM
Model-View-ViewModel pattern (MVVM) clearly separates UI and domain. When using them (MVVM) with Clean Architecture can clearly separate between UI and Presentation layers.
Implementations of different Views can use the same ViewModel . For example, you can implement CarsAroundListView and CarsAroundMapView and use CarsAroundViewModel for both. You can also implement another UIkit View and View with SwiftUI . It’s important to remember not to import UIkit, WatchKit, or SwiftUI inside your ViewModel , so you can reuse it in other platforms if needed.
For example, data binding between View and ViewModel can be done with closures , Delegate or Observerble (e.g. RxSwift). Combine and SwiftUI can also be used but only if the iOS system is >= 13. The View has a direct relationship with the ViewModel and notifies it whenever an event inside the View occurs. From the ViewModel , there is no direct reference to the View (only Data Binding).
In this example, we’ll use a simple combination of Closure and didSet to avoid third-party dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public final class Observable<Value> { private var closure: ((Value) -> ())? public var value: Value { didSet { closure?(value) } } public init(_ value: Value) { self.value = value } public func observe(_ closure: @escaping (Value) -> Void) { self.closure = closure closure(value) } } |
Note: Here is a very simple version of Observable, to see the whole implementation with multiple observerble and observer removal : Observerble .
An example of Data Binding of ViewController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | final class ExampleViewController: UIViewController { private var viewModel: MoviesListViewModel! private func bind(to viewModel: ViewModel) { self.viewModel = viewModel viewModel.items.observe(on: self) { [weak self] items in self?.tableViewController?.items = items // Important: Bạn không thể sử dụng viewmodel bên trong closure này, nó có thể là nguyên nhân rò rỉ bộ nhớ (viewModel.items.value not allowed) // self?.tableViewController?.items = viewModel.items.value // nó có thể retain cycle. bạn có thể access viewModel chỉ với self?.viewModel } // hoặc là chỉ trên 1 line viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 } } override func viewDidLoad() { super.viewDidLoad() bind(to: viewModel) viewModel.viewDidLoad() } } protocol ViewModelInput { func viewDidLoad() } protocol ViewModelOutput { var items: Observable<[ItemViewModel]> { get } } protocol ViewModel: ViewModelInput, ViewModelOutput {} |
Note : Accessing the viewModel from the observing closure is not allowed. It is the cause of the retain cycle (memory leak). You can access the viewModel just by self: self?.viewModel.
An example data binding on TableViewCell (Reusable Cell):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | final class MoviesListItemCell: UITableViewCell { private var viewModel: MoviesListItemViewModel! { didSet { unbind(from: oldValue) } } func fill(with viewModel: MoviesListItemViewModel) { self.viewModel = viewModel bind(to: viewModel) } private func bind(to viewModel: MoviesListItemViewModel) { viewModel.posterImage.observe(on: self) { [weak self] in self?.imageView.image = $0.flatMap(UIImage.init) } } private func unbind(from item: MoviesListItemViewModel?) { item?.posterImage.remove(observer: self) } } |
Note : We must unbind if the View is reusable (e.g. UITableViewCell)
MVVM Templates can be viewed here
MVVMs Communication
Delegation
The ViewModel of one MVVM (screen) communicates with another ViewModel of another MVVM (screen) using the delegation pattern:
For example, we have ItemsListViewModel and ItemEditViewModel . Then create a protocol ItemEditViewModelDelegate with the ItemEditViewModelDidEditItem(item) method. And for the viewmodel to conform to this protocol: extension ListItemsViewModel: ItemEditViewModelDelegate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Step 1: Define delegate and add it to first ViewModel as weak property protocol MoviesQueryListViewModelDelegate: class { func moviesQueriesListDidSelect(movieQuery: MovieQuery) } ... final class DefaultMoviesQueryListViewModel: MoviesListViewModel { private weak var delegate: MoviesQueryListViewModelDelegate? func didSelect(item: MoviesQueryListViewItemModel) { // Note: We have to map here from View Item Model to Domain Enity delegate?.moviesQueriesListDidSelect(movieQuery: MovieQuery(query: item.query)) } } // Step 2: Make second ViewModel to conform to this delegate extension MoviesListViewModel: MoviesQueryListViewModelDelegate { func moviesQueriesListDidSelect(movieQuery: MovieQuery) { update(movieQuery: movieQuery) } } |
Note : We can also name the Delegates in this case as a Responders: ItemEditViewModelResponder
Closures
Another way to communicate is to use closures specified or included by the FlowCoordinator . In the example project we can see how the MoviesListViewModel uses the showMovieQueriesSuggestions action closure to show MoviesQueriesSuggestionsView . It also passes the parameter (_ didSelect: MovieQuery) -> Void so they can also be called from that View. how this communication is connected inside MoviesSearchFlowCoordinator , see example below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // MoviesQueryList.swift // Step 1: Define action closure to communicate to another ViewModel, e.g. here we notify MovieList when query is selected typealias MoviesQueryListViewModelDidSelectAction = (MovieQuery) -> Void // Step 2: Call action closure when needed class MoviesQueryListViewModel { init(didSelect: MoviesQueryListViewModelDidSelectAction? = nil) { self.didSelect = didSelect } func didSelect(item: MoviesQueryListItemViewModel) { didSelect?(MovieQuery(query: item.query)) } } // MoviesQueryList.swift // Step 3: When presenting MoviesQueryListView we need to pass this action closure as paramter (_ didSelect: MovieQuery) -> Void struct MoviesListViewModelActions { let showMovieQueriesSuggestions: (@escaping (_ didSelect: MovieQuery) -> Void) -> Void } class MoviesListViewModel { var actions: MoviesListViewModelActions? func showQueriesSuggestions() { actions?.showMovieQueriesSuggestions { self.update(movieQuery: $0) } //or simpler actions?.showMovieQueriesSuggestions(update) } } // FlowCoordinator.swift // Step 4: Inside FlowCoordinator we connect communication of two viewModels, by injecting actions closures as self function class MoviesSearchFlowCoordinator { func start() { let actions = MoviesListViewModelActions(showMovieQueriesSuggestions: self.showMovieQueriesSuggestions) let vc = dependencies.makeMoviesListViewController(actions: actions) present(vc) } private func showMovieQueriesSuggestions(didSelect: @escaping (MovieQuery) -> Void) { let vc = dependencies.makeMoviesQueriesSuggestionsListViewController(didSelect: didSelect) present(vc) } } |
Split layers into frameworks (Modules)
Now, each layer (Domain, Presentation, UI, Data, Infrastructure Network) of the example app can be easily split into separate frameworks.
1 2 | New Project -> Create Project… -> Cocoa Touch Framework |
You can then incorporate those frameworks into your project using CocoaPods. You can see an example project here :
Note: You need to delete ExampleMVVM.xcworkspace and run pod install to create a new one, because it’s a licensing issue.
Dependency Injection Container
Dependency injection is a technique whereby one object provides the dependencies of another object. The DIContainer in your application is the central unit of all injections.
Using dependencies factory protocols
One of the options for declaring a dependency protocol is delegates, the initialization of the dependency for the DIContainer . To do this, we need to define the MoviesSearchFlowCoordinatorDependencies protocol and have the MoviesSceneDIContainer confirm the protocol itself, and then, inside the MoviesSearchFlowCoordinator as a param to initialize and present the MoviesListViewController . Here are the steps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Define Dependencies protocol for class or struct that needs it protocol MoviesSearchFlowCoordinatorDependencies { func makeMoviesListViewController() -> MoviesListViewController } class MoviesSearchFlowCoordinator { private let dependencies: MoviesSearchFlowCoordinatorDependencies init(dependencies: MoviesSearchFlowCoordinatorDependencies) { self.dependencies = dependencies } ... } // Make the DIContainer to conform to this protocol extension MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {} // And inject MoviesSceneDIContainer `self` into class that needs it final class MoviesSceneDIContainer { ... // MARK: - Flow Coordinators func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator { return MoviesSearchFlowCoordinator(navigationController: navigationController, dependencies: self) } } |
Using closures
Another option is closures . To do this we need to declare the closure inside the class to be injected and then we inject this closure. For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Define makeMoviesListViewController closure that returns MoviesListViewController class MoviesSearchFlowCoordinator { private var makeMoviesListViewController: () -> MoviesListViewController init(navigationController: UINavigationController, makeMoviesListViewController: @escaping () -> MoviesListViewController) { ... self.makeMoviesListViewController = makeMoviesListViewController } ... } // And inject MoviesSceneDIContainer's `self`.makeMoviesListViewController function into class that needs it final class MoviesSceneDIContainer { ... // MARK: - Flow Coordinators func makeMoviesSearchFlowCoordinator(navigationController: UINavigationController) -> MoviesSearchFlowCoordinator { return MoviesSearchFlowCoordinator(navigationController: navigationController, makeMoviesListViewController: self.makeMoviesListViewController) } // MARK: - Movies List func makeMoviesListViewController() -> MoviesListViewController { ... } } |
Source code
Resources
Conclude
Architectural patterns used mostly in mobile are Clean Architecture(Layered), MVVM, and Redux.
MVVM and Clean Architecture can of course be used separately, but MVVM only separates the inside of the Presentation Layer, while Clean Architecture separates your code into modular layers that can make your project easily testable, reproducible. use.
It is important not to skip creating a Use Case, even though the Use Case does nothing but call the Repository. This way, your architecture will be easy to understand when a new developer sees your use cases.
While this will be helpful as a starting point, there is no product, method or trick that can guarantee the best results. Depending on the needs of the project, you can choose a suitable architecture.
Clean Architecture works really well with (Test Driven Development) TDD. This architecture makes your project testable and easily replaceable layers (UI and Data).
Original article link: https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3