MVVM with Coordinators and RxSwift

Every application needs good architecture. In his talk from Mobilization 2016, Łukasz will show you the architecture he uses in his iOS projects: MVVM with coordinators and RxSwift. Not only will he talk about basics, but he’ll include a live code demo, describing what belongs where, controlling the flow using coordinators, testing everything using Quick/Nimble, and making network requests using Moya.


Intro (0:00)

My name is Łukasz, and I am an iOS developer at Droids on Roids. I like to think a lot, so today we will be talking about architecture that requires a lot of thinking: MVVM + coordinators + RxSwift.

This is based on an MVVM pattern that, while new in Swift, is very popular right now. It was introduced to me by Soroush Khanlou about a year ago at NSSpain. I highly recommend watching that talk.

MVC (1:03)

Image 1

MVC is a basic pattern; we have a view, a model, and a controller that connects the view to the controller. This connection gets data from the model and passes it to the view, which just displays the data. The view also passes notifications about user actions.

We also have the network layer. Where should it go? Perhaps it should go to the controller. There is Core Data, which could go to the controller as well. We may also have delegates and many more navigations, and we may just end up with a new architecture, which is just a plain controller.

MVVM (2:15)

Image 2

MVVM is very similar to MVC, but there is no view controller. Instead, there is another new class: ViewModel. In iOS, we have to use controllers, so you may think of a view as a view controller plus view, as one entity split between two files.

Above, we can see that view displays the data like in MVC. There is also a model, and a view model that processes the data and passes it to the view. You may notice there is no place for networking and other code.

Coordinators (3:11)

Image 3

Coordinators are another cool pattern that was introduced to me by Soroush Khanlou. A coordinator is basically an object that controls the flow of you application. It controls every push view controller, every pop controller, etc. This is one of the two responsibilities a coordinator should have, in addition to injection.

What does a controller look like in an application? We should have at least one app coordinator, which starts in an app delegate, and coordinates the flow of the first screens. As an example, let’s say we have the first screen as buttons. If a user clicks a “register” button, for example, we would have another controller. I tend to think of coordinators in a way such that every story we have, there should be another coordinator.

In this “register” example, the story involved is that the button may need some space for itself, since there might be a registration for using Facebook or Gmail, etc. In my opinion, it should be another app coordinate.

To continue our example, there could also be a “log in” button and a “forgot password” button, so we would have three coordinators, as pictured above, just in the beginning of our app. The logic can get very hard to explain because there are many view models, many coordinators and how they are connected can get complex.

RxSwift (5:06)

While you can use any functional library, I personally prefer RxSwift because I have the most experience with it. However, there are many reactive libraries out there such as RxCocoa, RxSwift, Bond, Interstellar, and more. Here, we will be using RxSwift for bindings.

Get more development news like this

If you don’t really follow the functional, reactive, or observer patterns at all, you can think of Rx in this way: imagine you have something that you pass to the model or to the view controller. Now imagine that you have a sequence of those things that you send to the object. However, a sequence could have one item, many items, or no items at all. I will use BinHex to observe the value that the view model contains and bind it to the view. We don’t really use “k key” values observing delegates, and a lot of code that we don’t need in our application.

Demo (7:03)

For the demo portion of this talk we will be looking at an app called Kittygram. Check out the GitHub respository.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

	var window: UIWindow?

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    	window = UIWindow()

    	var rootViewController: UIViewController!
    	if arc4random_uniform(5) % 4 == 0 { // super secret algorithm, dont change
        	rootViewController = PayMoneyPleaseViewController()
    	} else {
        	rootViewController = DashboardViewController()
    	}
    	window?.rootViewController = UINavigationController(rootViewController: rootViewController)
    	window?.makeKeyAndVisible()

    	return true
	}
}

Here we have repositories for rush ROI. Basically, when an item is clicked, there is a cut to another screen. I’m not going to get into too much detail here. What you see above is MVC with a small app delegate. In this case we have a “super secret algorithm” to push controllers in the beginning. There is also some sort of business plan. In the case that the statement passes, there will be money; otherwise it will run the dashboard view controller that probably has some repositories.

Dashboard View Controller (8:50)

Now looking at the DashboardViewController, which is the biggest controller in the app. It includes repositories, registration, or names for data sources and delegates. We also have downloadRepositories and registration of name. There’s also a function for showing alerts and other UI stuff.

class DashboardViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

	@IBOutlet weak var tableView: UITableView!

	var repos = [Repository]()
	var provider = MoyaProvider<GitHub>()

	override func viewDidLoad() {
    	super.viewDidLoad()

    	let nib = UINib(nibName: "KittyTableViewCell", bundle: nil)
    	tableView.register(nib, forCellReuseIdentifier: "kittyCell")
    	tableView.dataSource = self
    	tableView.delegate = self

    	downloadRepositories("ashfurrow")
	}

	fileprivate func showAlert(_ title: String, message: String) {
    	let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
    	let ok = UIAlertAction(title: "OK", style: .default, handler: nil)
    	alertController.addAction(ok)
    	present(alertController, animated: true, completion: nil)
	}

	// MARK: - API Stuff
	
	func downloadRepositories(_ username: String) {
        provider.request(.userRepositories(username)) { result in
            switch result {
            case let .success(response):
                do {
                    let repos = try response.mapArray() as [Repository]
                    self.repos = repos
                } catch {

                }
                self.tableView.reloadData()
            case let .failure(error):
                guard let error = error as? CustomStringConvertible else {
                    break
                }
                self.showAlert("GitHub Fetch", message: error.description)
        }
    }
}

It gets the repositories from GitHub in case it errors out, you can just push an alert. It also includes the standard tableView.dataSource.

Pay Money Please Controller (9:27)

Here is another controller for “pay money please.”

import UIKit

class PayMoneyPleaseViewController: UIViewController {

	@IBOutlet private weak var descriptionLabel: UILabel!

	override func viewDidLoad() {
	    super.viewDidLoad()

	    title = "Hey mate"
	    descriptionLabel.text = "💰💸🤑Please pay me money if you want to use this app, thanks! 💰💸🤑"
	}
}

It is pretty standard; just a description and a label.

Kitty Controller (9:41)

import UIKit

class KittyDetailsViewController: UIViewController {

	@IBOutlet private weak var descriptionLabel: UILabel!

	var kitty: Repository!

	convenience init(kitty: Repository) {
    	self.init()

    	self.kitty = kitty
	}

	override func viewDidLoad() {
    	super.viewDidLoad()

    	descriptionLabel.text = "🐱" + kitty.name + "🐱"
	}
}

This is the KittyDetailsViewController. We passed the repository, which is a model, downloaded it from GitHub, and then showed the kitty.name along with two images.

Models

User Model

import Mapper

struct User: Mappable {

	let login: String

	init(map: Mapper) throws {
   		try login = map.from("login")
	}
}

We have some models for the GitHub API. This is the User model, which is really simple.

Repository Model

import Mapper

struct Repository: Mappable {

	let identifier: Int?
	let language: String?
	let name: String
	let url: String?

	init(map: Mapper) throws {
    	identifier = map.optionalFrom("id")
    	name = map.optionalFrom("name") ?? "no name 😿"
    	language = map.optionalFrom("language")
    	url = map.optionalFrom("url")
	}
}

This is the Repository model. I use a mapper from Lyft, which I think is pretty cool. The model maps the identifier, but everything is optional. In case there is no name, I need to display something in the details.

Endpoint

Check out the endpoint code here.

The endpoint is based on Moya. I really like Moya because it makes you flexible enough to not use external abstractions over the network requests. I can do an infinite number of things in Moya using plugins, closures etc.

In case we want to stop our requests, we have to pass the sample data, whether it’s sample data adjacent or data just to use the network without networking.

For bigger apps, there might more than one provider, in which case they should also be provided as an dependency injection.

Coordinator

import Foundation
import UIKit

class Coordinator {

	var childCoordinators: [Coordinator] = []
	weak var navigationController: UINavigationController?

	init(navigationController: UINavigationController?) {
    	self.navigationController = navigationController
	}
}

This is the Coordinator class, which is the most important. It contains childCoordinators, so we can spawn another one and yet another. The interesting thing here is the navigation controller. You may have seen that the system is weak. A navigation controller must be weak in our case. If you think about the application with only one navigation controller, and a child and parent are put on the same controller, you create a cycle where each one gets called. This is really important, because we have a weak navigation controller in case you don’t use one navigation controller.

Who is in charge of the reference counter for the navigation controller?

We will start up our MVC in MVVM style. We have a flow started in our delegate, which shouldn’t be here, so we will take care of it using coordinators.

We will create a new app coordinator to be the basic one that we use in the beginning of our application.

import UIKit

final class AppCoordinator: Coordinator {

	func start() {
		var viewController: UIViewController!
    	if arc4random_uniform(5) % 4 == 0 { // super secret algorithm, dont change
        	viewController = PayMoneyPleaseViewController()
    	} else {
        	viewController = DashboardViewController()
    	}

		navigationController?.pushViewController(viewController, animated: true)
	}
}

So this is the logic of our flow in our app coordinator; it doesn’t really belong in AppDelegate, so we can try to fix the app delegate which we broke. We can’t pass UI navigation with root controller because we don’t really have one, so we will create another navigation controller without any root controllers. We will add a pure navigation controller here just to satisfy Xcode. We can use the navigation control that we just created.

Our app delegate should now look like this:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

	var window: UIWindow?

	func application(_ application: UIApplication, didFinishLaunchingWithOptions 	launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    	window = UIWindow()

    	let navigationController = UINavigationController()
    	window?.rootViewController = navigationController
		let coordinator = AppCoordinator(navigationController: navigationController)
		coordinator.start()
    	window?.makeKeyAndVisible()

    	return true
	}
}

With this, we have two stories: where the user doesn’t pay money, and another that controls the dashboard. When the user doesn’t pay money, we just show the controller that does nothing. The story that controls the dashboard is split into two new coordinators. We will create a dashboard coordinator.

So we have two more stories for our application. Let’s just start with the easiest one, which is “pay money please.”

final class PayMoneyPleaseCoordinator: Coordinator {

	func start() {
		let viewController = PayMoneyPleaseViewController()
		let viewController = DashboardViewController(viewModel: viewModel)
		navigationController?.pushViewController(viewController, animated = true)
	}
}

This creates a new controller and pushes to the navigation stack. With this done, we move to the dashboard.

final class DashboardCoordinator: Coordinator {

	func start() {
		let viewController = DashboardViewController()
		navigationController?.pushViewController(viewController, animated = true)
	}
}

This also pushes to the navigation stack. With these both completed, we have both stories now. Now we place the stories in the app coordinator, so it should be responsible for knowing which story to perform.

Here is what the app coordinator should look like:

import UIKit

final class AppCoordinator: Coordinator {

	func start() {
		if arc4random_uniform(5) % 4 == 0 { // super secret algorithm, don't change
			let coordinator = PayMOneyPleaseCoordinator(navigationController: navigationController)
			coordinator.start()
			childCoordinators.append(coordinator)
		} else {
			let coordinator = DashboardCoordinator(navigationController: navigationController)
			coordinator.start()
			childCoordinators.append(coordinator)
		}
	}
}

This is the case with only one coordinator in the app so it would have a cycle. The most important part is to add the child coordinators to keep the reference to coordinators so it can work.

Dashboard Controller Refactor

Now we will shift to the dashboard controller that we want to refactor, and create a dashboard view model for this class. We will create a new group and new model.

There is most likely a guideline to create on view model per coordinator, but in some cases you may want more. If you have more views in your view controller that you want to bind with different logic, you may want more.

One thing that I noticed about view models is that you almost always want to create a protocol. Whenever you bind a view model to a view controller, you may want to change the logic, but if you don’t use protocols that will be implemented by the view model, you may end up creating a lot of inits. This complicates your architecture. As a rule of thumb, use a protocol for every view model you create.

Now to create a protocol dashboard view model type. For now it will be empty, but as we go with our refactor, it will get filled.

In the view controller, we have networking that we don’t really need there, so we move the API stuff from the view controller, create an init in the dashboard view model, and move the request. For testing purposes, we will comment out the UI logic that we had in the view controller. We will also move the download repository to the view model since it was slow in the beginning.

The network request gets your repositories from the internet and passes it to the table view. We will do that with a sequence of objects. The easiest way to do this if you want to try RxSwift is by creating a variable, which is a sequence of at least one object. It will be a repositories variable for now, starting with an empty one. This is an easy bridge between imperative and declarative programming.

This may not be the best Rx code, because the best Rx code won’t be understandable by many people.

We create a new item in the sequence using the value property. Another rule of thumb is to not really expose variables: so we will make it private, using Xcode hacks to use lazy variable types. These will be reposeObservable. You never want to expose variables in case someone from view controller makes another sequence; you won’t be prepared for it. We will add this reposVariable to our protocol.

Here is the Dashboard view model:

import RxSwift

protocol DashboardViewModelType {
	var reposeObservable: Observable<[Repository]> {get}
}

final class DashboardViewModel {

	private let reposVariable = Variable<[Repository]>([])

	lazy var reposObservable: Observable<[Repository]> = self.reposVariable.asObservable()

	intit() {
		downloadRepositories("ashfurrow")
	}

	func downloadRepositories(_ username: String) {
		provider.request(.userRepositories(username)) { result in
		switch result {
		case let .success(response):
			do {
				let repos = try response.mapArray() as [Repository]
				self.reposVariable.value = repos
			} catch {

			}
		case let .faliure(error):
			gaurd let error = error as? CustomStringConvertible else {
				break
			}
//				self.showAlert("GitHub Fetch", message: error.description)
		}
	}
}

I’ve commented out and eliminated some lines. In RxSwift, there is a binding function to view controller that we can use only closure, and it fills out all the data service delegates.

We don’t have our view model, so we will pass it in independence injection to our coordinator. In Dashboard Coordinator, we have to pass the view model.

In Dashboard view controller:

private var viewModel: DashboardViewModelType!
convenience init(viewModel: DashboardViewModelType) {
	self.init()

	self.viewModel = viewModel
}

Just use the type here, because it’s very helpful in testing controllers when rendered properly. The private var doesn’t need to be optional: I would just force unwrap it because whenever we create this controller without a view model, I want it to crash to show something is wrong, and the code won’t ship to the public.

Now we can use the magic of RxSwift. We need to import RxSwift and RxCocoa. So what do we bind?

In the bind function, the first parameter is the tableView, and the second is index, and the third is item. Now, we have to create a cell to return it in the closure; we can remove the delegates, because we won’t need them for now. We also don’t get repository here, because we have it from an item from the observable repositories.

First however, we need to make an index path, because the method is really for simple usage. The row will be index and the section will be zero for now, and we don’t have an our repository, we have an item instead. We also need a dispose bag, and for the sake of time, you’re just going to have to trust me that it is needed here. While we don’t have time to do this properly, my suggestion is to not return observable of items, but observable of view models since view controller shouldn’t really have an access to model, this is another step in creating our view model, but this is just for purposes of creating view model’s type.

Here is the refactored Dashboard view Controller:

import Foundation
import Moya
import Moya_modelMapper
import UIKit
import RxSwift
import RxCocoa

class DashboardViewController: UIViewController {

	@IBOutlet weak var tableView: UITableView!

	var repos = [Repository]()
	private var viewModel: DashboardViewModelType!
	private let disposeBag = DisposeBag()

	convenience init(viewModel: DashboardViewModelType) {
		self.init()

		self.viewModel = viewModel
	}

	override func viewDidLoad() {
		super.viewDidLoad()

		let nib = UINib(nibName: "KittyTableViewCell", bundle: nil)
		tableView.register(nib, forCellReuseIdentifier: "kittyCell")
//		tableView.dataSource = self
//		tableView.delegate = self

		viewModel.reposObservable.bindTo(tableView.rx.items) { tableView, index, item in
		let indexPath = IndexPath(row: index section: 0)
		let cell = tableView.dequeueReusableCell(withIdentifier: "kittyCell", for: indexPath) as UITableViewCell
		cell.textLabel?.text = item.name

		return cell
	}
	.addDisposableTo(disposeBag)
}

You have to remember about not passing the model. If you wanted to make the navigation from the controller that holds the kitties to details, I would suggest to make another variable, and now its a view model. Bind the moment it clicks from the view controller to the view model, and then the view model has to do something with it.

In a Dashboard view controller, the coordinator has to push the next controller up, so we create a new protocol, which is delegate for the controller that has to inherit from a class. (Maybe that will be fixed in Swift 3?) However, the only action we would have would be to click on the cell with the cat, so we would make a function that says “did tap cell with index path, and now create a custom init in our view model here.” Without parameters, make a view delegate which is a view controller delegate, and call the delegate in the signal from view model. You of course pass it into coordinator, and now coordinator knows how to react to it.

We also need to think about how to remove coordinators. There should be a method that checks if coordinator finished its action and the app coordinator just removes from the stack.

Q&A (37:43)

Q: In your demo, you used xibs. Have you ever tried to make it work with storyboards and segues?

Łukasz: Yes, it does work with storyboards. I found that GitHub repositories work with controllers. I don’t have a link right now, but if you search for “coordinators with storyboards,” you can find it.

Q: What is your take on subjects in Rx?

Łukasz: You shouldn’t really need to use subjects in your projects. If you are in the early stages of using Rx, it’s okay. In general, don’t use it, but if you have to, do it the way I did: creating a private one and exposing only to observable, so that the view controller doesn’t have access to the data from the view model. It should only do binding there.

Next Up: Building a Unidirectional Data Flow App in Swift with Realm

General link arrow white

Łukasz Mróz

Łukasz started as a back-end web developer and quickly found a new home in iOS. He’s in love with Swift, learning, and everything reactive. Endorsed on LinkedIn for coffee skills.

Edited by Billy Leet