Using MVVM with tables and cells in iOS

When I ventured into native iOS development I immediately took a look at the possibility to use data binding on iOS which enables me to simply declare the relationships between the UI and the ViewModel. This article takes that approach further shows you how to use MVVM and data binding when using tables and cells, or in the world of iOS UITableView and UITableViewCell.

Sample scenario

Let’s start with simple example scenario. You want to show progress of some flow that contains of multiple steps, each of the steps can be either running or complete. When a step is running it can report its progress. You want to display this flow in a table that looks like this

The classic iOS way

Now imagine you want to implement this scenario in the classic iOS way. You will have a list of some model. Every time you add an item to the list you need to refresh the table. Every time you change something on a model in the list you need to refresh the table and of course you need a table delegate to manipulate the UI according to the properties of the model.

Of course there is a better, more declarative way.

The reactive way

ViewModel

First you need a ViewModel for all the steps of the flow. My flow is connected to application synchronization, so my ViewModel is called SyncStepViewModel

import Foundation
import RxSwift

class SyncStepViewModel {
    let isRunning = Variable(true)
    let percentComplete = Variable<Int>(0)
    let stepTitle: Observable<String>
    
    private let title: String
    
    init(title: String) {
        self.title = title
        
        stepTitle = Observable.combineLatest(isRunning.asObservable(), percentComplete.asObservable()) {
            (running: Bool, percent: Int) -> String in if (running && percent>0) {
                return "\(title) \(percent)%"
            } else {
                return title
            }
        }
    }
}

This ViewModel has a title, contains property determining if the flow step is currently running, property for the current progress percentage and a computed property for the step title. This computer property just adds the progress percentage at the end of the title when applicable.

The ViewModel for the screen just needs to hold the array of the flow steps in an observable way, so let’s make it easy

import Foundation
import RxSwift

class SyncViewModel {
    let syncSteps = Variable<[SyncStepViewModel]>([])
}    

This ViewModel will of course contains some logic to add the flow steps to the array.

Table and cells binding

Binding the SyncViewModel to the UITableView in the UIViewController is really easy

viewModel.syncSteps.asObservable()
            .bindTo(syncStepsTableView.rx.items(cellIdentifier: SyncStepCell.reuseIdentifier, cellType: UITableViewCell.self)) { (row, element, cell) in
                if let cell = cell as? SyncStepCell {
                    cell.viewModel = element
                }
            }
            .disposed(by: disposeBag)

It is just a few lines of declarative code and no delegates!

The tricky part is the UITableViewCell and making the UI work with the ViewModel. As you can see from the previous snippet, I do not access any of the UI elements of my SyncStepCell I just assign the ViewModel. The SyncStepCell takes care of the rest using data binding

import Foundation
import UIKit
import RxSwift

class SyncStepCell: UITableViewCell {
    static let reuseIdentifier = "SyncStepCell"
    
    @IBOutlet private weak var titleLabel: UILabel!
    @IBOutlet private weak var activityIndicator: UIActivityIndicatorView!
    @IBOutlet private weak var checkmarkImage: UIImageView!
    
    var disposeBag = DisposeBag()
    
    var viewModel: SyncStepViewModel? {
        didSet {
            if let vm = viewModel {
                vm.isRunning.asObservable().map({!$0}).bindTo(activityIndicator.rx.isHidden).disposed(by: disposeBag)
                vm.isRunning.asObservable().map({$0 ? UIColor.black : UIColor.gray}).bindTo(titleLabel.rx.textColor).disposed(by: disposeBag)
                vm.stepTitle.bindTo(titleLabel.rx.text).disposed(by: disposeBag)
                vm.isRunning.asObservable().bindTo(checkmarkImage.rx.isHidden).disposed(by: disposeBag)
            }
        }
    }
}

My UITableViewCell just “waits” for the ViewModel and then sets up all the necessary bindings. Again, no direct access to the UI elements, just making everything work in a simple declarative way.

If you want a more complex example of MVVM and binding, check out my iOS sample app on Github.


ios  swift  rxswift  mvvm 

See also