So, you’ve implemented an MVC architecture in your iOS apps multiple times, explored MVP as an alternative, heard something about VIPER, and (probably) liked MVVM.
Now what? It’s time to take your MVVM knowledge to a whole new level — reactive MVVM with coordinators!
In this article, I will demonstrate how to implement an MVVM-Coordinator pattern with RxSwift, RxDataSources, RxAlamofire.
We will also use my own CocoaPod that provides the base implementation of ReactiveCoordinator, which is available in my GitHub repo.
The demo app uses a free Holiday API to get the list of countries and fetch last year’s holidays for a chosen one.
The source code of the project is available on GitHub.
To fetch holidays and countries successfully, we will need to obtain a free Holiday API key:
Now, simply replace “PASTE YOUR API KEY HERE” in APIManager.swift
with the key that you copied from the dashboard:
mport Foundation
class APIManager {
....
lazy var apiKey: String = {
return "PASTE YOUR API KEY HERE"
}()
}
APIManager.swift
To finish the setup, navigate to the project’s directory and run pod install
to install all the required dependencies:
First, let’s take a look at the Coordinator implementation in the ReactiveCoordinator framework.
CoordinationResult
to replace delegation between view controllers in the future.store<T>
method is used to store our coordinators in the childCoordinators
dictionary.release<T>
method is responsible for deallocating unused coordinations.coordinate<T>
method is used to provide navigation between screens.start()
method in which we will construct and connect view controllers with their view models.import RxSwift
open class ReactiveCoordinator<ResultType>: NSObject {
public typealias CoordinationResult = ResultType
public let disposeBag = DisposeBag()
private let identifier = UUID()
private var childCoordinators = [UUID: Any]()
private func store<T>(coordinator: ReactiveCoordinator<T>) {
childCoordinators[coordinator.identifier] = coordinator
}
private func release<T>(coordinator: ReactiveCoordinator<T>) {
childCoordinators[coordinator.identifier] = nil
}
@discardableResult
open func coordinate<T>(to coordinator: ReactiveCoordinator<T>) -> Observable<T> {
store(coordinator: coordinator)
return coordinator.start()
.do(onNext: { [weak self] _ in
self?.release(coordinator: coordinator) })
}
open func start() -> Observable<ResultType> {
fatalError("start() method must be implemented")
}
}
ReactiveCoordinator.swift
Here is an example of how we use ReactiveCoordinator in our app. Suppose we have two Coordinators: AppCoordinator
and HolidaysCoordinator
. Both inherit from ReactiveCoordinator. The workflow:
AppCoordinator
fires its start()
method.coordinate(to: )
method is called, which coordinates to HolidaysCoordinator
and fires its own start()
method, which constructs and connects the HolidaysViewController
and its ViewModel
.HolidaysViewController
presented on the screen.We use RxAlamofire to create a base generic get<T>
request.
APIClient.swift
class APIClient {
.....
func get<T: BaseMappable>(urlString: String, parameters: [String: Any] = [:], success: @escaping (Int, T) -> (), failure: @escaping (String) -> ()) {
var parameters = parameters
parameters["key"] = APIManager.shared.apiKey
guard let url = NSURL(string: urlString , relativeTo: self.baseURL as URL?) else {
return
}
let urlString = url.absoluteString!
_ = request(.get,
urlString,
parameters: parameters,
headers: nil)
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseJSON()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (response) in
let statusCode = response.response?.statusCode
let model = Mapper<T>().map(JSON: response.result.value as! [String : Any])
success(statusCode!, model!)
}, onError: { (error) in
failure(error.localizedDescription)
})
}
}
APIClient.swift
ObjectMapper
is used to construct objects from JSON that we receive from the Holiday API.
Holidays.swift
import ObjectMapper
struct Holidays: Mappable {
var holidays: [Holiday]?
init?(map: Map) {}
mutating func mapping(map: Map) {
holidays <- map["holidays"]
}
}
struct Holiday: Mappable {
var name: String?
var date: String?
var country: String?
var `public`: Bool?
init?(map: Map) {}
mutating func mapping(map: Map) {
name <- map["name"]
date <- map["date"]
country <- map["country"]
`public` <- map["public"]
}
}
Holidays.swift
Countries.swift
import ObjectMapper
struct Countries: Mappable {
var countries: [Country]?
init?(map: Map) {}
mutating func mapping(map: Map) {
countries <- map["countries"]
}
}
struct Country: Mappable {
var code: String?
var name: String?
var flag: String?
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- map["code"]
name <- map["name"]
flag <- map["flag"]
}
}
Countries.swift
With that being set up, we are ready to start building our Coordinators.
This class is responsible for setting up the rootViewController
for the window
.
In its start method, we specify that the rootViewController
of the window
is HolidaysViewController
, embedded in a UINavigationController
. Finally, we coordinate to HolidaysCoordinator
:
import RxSwift
import ReactiveCoordinator
class AppCoordinator: ReactiveCoordinator<Void> {
var window: UIWindow
init(window: UIWindow) {
self.window = window
}
override func start() -> Observable<Void> {
let navigationController = UINavigationController(rootViewController: HolidaysViewController())
let holidaysCoordinator = HolidaysCoordinator(rootViewController: navigationController.viewControllers[0])
window.rootViewController = navigationController
window.makeKeyAndVisible()
return coordinate(to: holidaysCoordinator)
}
}
AppCoordinator.swift
AppDelegate.swift
Here, we create and start our AppCoordinator
:
import UIKit
import RxSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var appCoordinator: AppCoordinator!
private let disposeBag = DisposeBag()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
appCoordinator = AppCoordinator(window: window!)
appCoordinator.start()
.subscribe()
.disposed(by: disposeBag)
return true
}
}
AppDelegate.swift
We have successfully set up the initial flow of our app.
This module displays a list of holidays if a country was chosen. Otherwise, it shows a placeholder message “Choose your country first”.
Here, similar to how we did it in the AppCoordinator
, we set up HolidaysViewController
and provide it with a HolidaysViewModel
.
import RxSwift
import ReactiveCoordinator
class HolidaysCoordinator: ReactiveCoordinator<Void> {
let rootViewController: UIViewController
init(rootViewController: UIViewController) {
self.rootViewController = rootViewController
}
override func start() -> Observable<Void> {
let viewController = rootViewController as! HolidaysViewController
let viewModel = HolidaysViewModel()
viewController.viewModel = viewModel
viewModel.selectedHoliday
.subscribe({ [weak self] holidayItem in
if let holiday = holidayItem.element {
self?.coordinateToHolidayDetail(with: holiday)
}
})
.disposed(by: disposeBag)
viewModel.chooseCountry
.flatMap { [weak self] _ -> Observable<String?> in
guard let `self` = self else { return .empty() }
return self.coordinateToChooseCountry()
}
.filter { $0 != nil }
.map { $0! }
.bind(to: viewModel.selectedCountry)
.disposed(by: disposeBag)
return Observable.never()
}
// MARK: - Coordination
private func coordinateToHolidayDetail(with holidayViewModel: HolidayViewModel) {
let holidayDetailCoordinator = HolidayDetailCoordinator(rootViewController: rootViewController)
holidayDetailCoordinator.viewModel = holidayViewModel
coordinate(to: holidayDetailCoordinator)
}
private func coordinateToChooseCountry() -> Observable<String?> {
let chooseCountryCoordinator = ChooseCountryCoordinator(rootViewController: rootViewController)
return coordinate(to: chooseCountryCoordinator)
.map { result in
switch result {
case .country(let country): return country
case .cancel: return nil
}
}
}
}
HolidaysCoordinator.swift
In the start()
method, we subscribe to the selectedHoliday
and chooseCountry
properties of the view model.
When an event is emitted to these properties, (i.e. the user taps on the Choose Country button or selects a cell), the coordinator knows that it’s time to navigate.
Its sole responsibility is to layout the view, and bind UI elements to the HolidaysViewModel
.
import PKHUD
import RxSwift
import RxCocoa
import RxDataSources
class HolidaysViewController: UIViewController {
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupNavItem()
bindTableView()
bindNavItem()
bindHUD()
bindVisibilityState()
viewModel.fetchHolidays{ (errorMessage) in
self.showMessage(errorMessage)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupNavBar()
}
// MARK: - Properties
let disposeBag = DisposeBag()
var viewModel: HolidaysViewModel!
....
}
// MARK: - Binding
extension HolidaysViewController {
func bindTableView() {
viewModel.holidays
.bind(to: tableView.rx.items(cellIdentifier: "HolidayTableViewCell", cellType: HolidayTableViewCell.self)) { index, viewModel, cell in
cell.viewModel = viewModel
}
.disposed(by: disposeBag)
tableView.rx.modelSelected(HolidayViewModel.self)
.bind(to: viewModel.selectedHoliday)
.disposed(by: disposeBag)
tableView.rx.itemSelected
.subscribe(onNext: { (indexPath) in
self.tableView.deselectRow(at: indexPath, animated: true)
})
.disposed(by: disposeBag)
}
func bindHUD() {
viewModel.isLoading
.subscribe(onNext: { [weak self] isLoading in
isLoading ? self?.showProgress() : self?.hideProgress()
})
.disposed(by: disposeBag)
}
func bindNavItem() {
chooseCountryItem.rx.tap
.bind(to: viewModel.chooseCountry)
.disposed(by: disposeBag)
viewModel.selectedCountry
.subscribe(onNext: { (country) in
self.chooseCountryItem.title = country
})
.disposed(by: disposeBag)
}
func bindVisibilityState() {
viewModel.selectedCountry
.subscribe(onNext: { _ in
self.tableView.isHidden = false
self.chooseCountryLabel.isHidden = true
})
.disposed(by: disposeBag)
}
}
// MARK: - UI Setup
extension HolidaysViewController {
func setupUI() {
overrideUserInterfaceStyle = .light
self.view.backgroundColor = .white
.....
}
func setupNavItem() {
self.navigationItem.title = "Holidays"
self.navigationItem.rightBarButtonItem = chooseCountryItem
}
func setupNavBar() {
self.navigationController?
.navigationBar
.tintColor = .white
}
}
HolidaysViewController.swift
Take a closer look at how we work with UITableView
:
extension HolidaysViewController {
func bindTableView() {
viewModel.holidays
.bind(to: tableView.rx.items(cellIdentifier: "HolidayTableViewCell", cellType: HolidayTableViewCell.self)) { index, viewModel, cell in
cell.viewModel = viewModel
}
.disposed(by: disposeBag)
tableView.rx.modelSelected(HolidayViewModel.self)
.bind(to: viewModel.selectedHoliday)
.disposed(by: disposeBag)
tableView.rx.itemSelected
.subscribe(onNext: { (indexPath) in
self.tableView.deselectRow(at: indexPath, animated: true)
})
.disposed(by: disposeBag)
}
......
HolidaysViewController.swift
First, we bind the holidays
property of the viewModel
to the tableView
using RxDataSources. It’s super short and simple, and we don’t even have to use these lengthy UITableViewDelegate
and UITableViewDataSource
implementations!
Next, we bind the selection of a particular tableView
cell to the viewModel
’s selectedHoliday
property. This way, we avoid the use of the didSelectRow(at: )
UITableViewDelegate
method.
And that being done in just three lines — neat!
Lastly, we subscribe to the selection event to bring the background of the cell to its former state, by receiving the selected indexPath
and calling the deselectRow(at: )
method.
Here, all the magic happens. This view model is responsible for accepting input from the view and fetching holidays.
import RxCocoa
class HolidaysViewModel {
private let disposeBag = DisposeBag()
// MARK: - Actions
let isLoading = BehaviorSubject<Bool>(value: false)
let selectedCountry = PublishSubject<String>()
let selectedHoliday = PublishSubject<HolidayViewModel>()
let chooseCountry = PublishSubject<Void>()
// MARK: - Table View Model and Data Source
var holidays = BehaviorSubject<[HolidayViewModel]>(
value: []
)
// MARK: - API Call
func fetchHolidays(onError: @escaping (String) -> ()) {
self.selectedCountry
.subscribe(onNext: { [weak self] (country) in
guard let `self` = self else { return }
self.isLoading.onNext(true)
HolidaysService.shared.getHolidays(country: country, success: { (code, holidays) in
self.isLoading.onNext(false)
let holidayItems = holidays.holidays!.compactMap { HolidayViewModel(holiday: $0)
}
self.holidays.onNext(holidayItems)
}) { (error) in
self.isLoading.onNext(false)
onError(error)
}
})
.disposed(by: disposeBag)
}
}
HolidaysViewModel.swift
The cell has its own HolidayViewModel
, on the assignment of which we call the configure()
method to update the cell’s UI with the Model.
class HolidayTableViewCell: UITableViewCell {
// MARK: - Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style,
reuseIdentifier: reuseIdentifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
setupUI()
}
// MARK: - Properties
var viewModel: HolidayViewModel! {
didSet {
self.configure()
}
}
lazy var holidayTitleLabel: UILabel = {
let label = UILabel()
return label
}()
}
// MARK: - Configuration
extension HolidayTableViewCell {
private func configure() {
holidayTitleLabel.text = viewModel.title
}
}
....
HolidayTableViewCell.swift
Used in HolidayTableViewCell
to represent its properties:
import Foundation
struct HolidayViewModel {
let title: String
let date: String
let country: String
let isPublic: Bool
init(holiday: Holiday) {
self.title = holiday.name!
self.date = holiday.date!
self.country = holiday.country!
self.isPublic = holiday.public!
}
}
HolidayViewModel.swift
This module displays a list of countries. On the selection of a particular country, the ChooseCountryViewController
is dismissed, forwarding the selected country back to HolidaysViewModel
.
Here is where we will use our CoordinationResult
. When we tap on the Close button, HolidaysCoordinator
receives nil
. When a cell is selected, it receives the selected country
:
import ReactiveCoordinator
enum ChooseCountryCoordinationResult {
case country(String)
case cancel
}
class ChooseCountryCoordinator: ReactiveCoordinator<ChooseCountryCoordinationResult> {
private let rootViewController: UIViewController
init(rootViewController: UIViewController) {
self.rootViewController = rootViewController
}
override func start() -> Observable<CoordinationResult> {
let viewController = ChooseCountryViewController()
let navigationController = UINavigationController(rootViewController: viewController)
let viewModel = ChooseCountryViewModel()
viewController.viewModel = viewModel
let country = viewModel.selectedCountry.map { CoordinationResult.country($0) }
let cancel = viewModel.didClose.map { _ in
CoordinationResult.cancel
}
rootViewController.present(navigationController, animated: true, completion: nil)
return Observable.merge(country, cancel)
.take(1)
.do(onNext: { _ in
viewController.dismiss(animated: true, completion: nil)
})
}
}
ChooseCountryCoordinator.swift
As before, the controller simply displays the UI and sends action events to the view model:
import RxSwift
import RxCocoa
class ChooseCountryViewController: UIViewController {
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupNavItem()
bindTableView()
bindSearchBar()
bindCloseItem()
bindHUD()
viewModel.fetchCountries { [weak self] (errorMessage) in
self?.showMessage(errorMessage)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupNavBar()
}
// MARK: - Properties
let disposeBag = DisposeBag()
var viewModel: ChooseCountryViewModel!
....
}
// MARK: - Binding
extension ChooseCountryViewController {
func bindTableView() {
viewModel.filteredCountries
.bind(to: tableView.rx.items(cellIdentifier: "CountryTableViewCell", cellType: CountryTableViewCell.self)) { (index, viewModel, cell) in
cell.viewModel = viewModel
}
.disposed(by: disposeBag)
tableView.rx.modelSelected(CountryViewModel.self)
.map { $0.code }
.bind(to: viewModel.selectedCountry)
.disposed(by: disposeBag)
}
func bindCloseItem() {
closeItem.rx.tap
.bind(to: viewModel.didClose)
.disposed(by: disposeBag)
}
func bindSearchBar() {
searchBar.rx.text
.orEmpty
.bind(to: viewModel.searchText)
.disposed(by: disposeBag)
}
func bindHUD() {
viewModel.isLoading
.subscribe(onNext: { [weak self] isLoading in
isLoading ? self?.showProgress() : self?.hideProgress()
})
.disposed(by: disposeBag)
}
}
.....
ChooseCountryViewController.swift
Has identical functionality as HolidayTableViewCell
:
import UIKit
class CountryTableViewCell: UITableViewCell {
// MARK: - Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Properties
var viewModel: CountryViewModel! {
didSet {
self.configure()
}
}
.....
}
// MARK: - Configuration
extension CountryTableViewCell {
func configure() {
countryNameLabel.text = viewModel.name
}
}
.....
CountryTableViewCell.swift
Here, we perform two important tasks: fetching countries and filtering based on the text from a searchBar
:
import RxCocoa
final class ChooseCountryViewModel {
private let disposeBag = DisposeBag()
// MARK: - Actions
let didClose = PublishSubject<Void>()
let selectedCountry = PublishSubject<String>()
let isLoading = BehaviorSubject<Bool>(value: false)
let searchText = PublishSubject<String>()
// MARK: - Table View Model & Data Source
let fetchedCountries = BehaviorSubject<[CountryViewModel]>(value: [])
let filteredCountries = BehaviorSubject<[CountryViewModel]>(value: [])
// MARK: - API Call
func fetchCountries(onError: @escaping (String) -> ()) {
self.isLoading.onNext(true)
CountriesService.shared.getCountries(success: { [weak self] (code, countries) in
guard let `self` = self else { return }
self.isLoading.onNext(false)
let countryItems = countries.countries!.compactMap {
CountryViewModel(country: $0)
}
self.fetchedCountries.onNext(countryItems)
self.filteredCountries.onNext(countryItems)
self.bindSearchToModel()
}) { [weak self] (error) in
guard let `self` = self else { return }
self.isLoading.onNext(false)
onError(error)
}
}
func bindSearchToModel() {
self.searchText.subscribe(onNext: { [weak self] (text) in
guard let `self` = self else { return }
switch text.isEmpty {
case false:
let countries = try! self.fetchedCountries.value().filter {
$0.name.range(of: text, options: .caseInsensitive) != nil
}
self.filteredCountries.onNext(countries)
case true:
self.filteredCountries.onNext(try! self.fetchedCountries.value())
}
})
.disposed(by: disposeBag)
}
}
ChooseCountryViewModel.swift
Represents a cell containing the name
of a country and its code
:
import RxSwift
import RxCocoa
struct CountryViewModel {
let code: String
let name: String
init(country: Country) {
self.code = country.code!
self.name = country.name!
}
}
CountryViewModel.swift
This module displays details of a particular holiday, showing its title
, date
, country
, and isPublic
status.
Builds up the HolidaysViewController
and provides it with a HolidayViewModel
:
import ReactiveCoordinator
class HolidayDetailCoordinator: ReactiveCoordinator<Void> {
private let rootViewController: UIViewController
public var viewModel: HolidayViewModel!
init(rootViewController: UIViewController) {
self.rootViewController = rootViewController
}
override func start() -> Observable<Void> {
let viewController = HolidayDetailViewController()
viewController.viewModel = viewModel
rootViewController.navigationController?
.pushViewController(viewController, animated: true)
return Observable.empty()
}
}
HolidayDetailCoordinator.swift
Does exactly the same job as other view controllers:
// MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
....
bindViewModel()
}
// MARK: - Properties
let disposeBag = DisposeBag()
var viewModel: HolidayViewModel!
.......
}
// MARK: - Binding
extension HolidayDetailViewController {
func bindViewModel() {
self.holidayNameLabel.text = viewModel.title
self.holidayDateLabel.text = viewModel.date
self.holidayCountryLabel.text = viewModel.country
self.isHolidayPublicLabel.text = viewModel.isPublic ? "True" : "False"
}
}
....
HolidayDetailViewController.swift
Now, with the Reactive MVVM-Coordinator design pattern being successfully implemented, we see the following benefits:
You could learn how others have implemented the reactive MVVM-Coordinator pattern from the following links:
If you are interested in how to implement a VIPER architecture in your app, check out my other article where I show how I did it in a simple client-server app.
☞ Vue JS 3 Reactivity Fundamentals - Composition API
☞ ReactJS & React Hooks Tutorial For Beginners
☞ Uploading Videos Using Multer - Let's Build A Youtube Clone with ReactJS, NodeJS, MySQL
☞ TypeWriter Effect In ReactJS Tutorial
☞ How to Learn ReactJS Really Quick As A Beginner [2020]
☞ Socket.io + ReactJS Tutorial | Learn Socket.io For Beginners