Good Practice with RxSwift & MVVM - Exposing Variables

Just a quick tip for the use of RxSwift within an MVVM architected application. Most people likely know to do this, but it never hurts to throw it out there.

In a view model, my instinct was to set it up like so:

public protocol PeopleViewModel {
    /// Drives a collection of person summaries (UITableView or UICollectionView for example).
    var personViewModels: Variable<[PersonViewModel]> { get }
    /// Whether or not the perople are people loaded.
    var isLoading: Variable<Bool>
    /// Whether there are any people.
    var isEmpty: Variable<Bool>
    /// Fetches people after a given index (useful for paginated responses).
    func fetchPeople(after index: Int)
}

Within the view, I would do the usual stuff:

private func configure(with viewModel: PeopleViewModel) {
    viewModel.personViewModels.asObservable()
        .bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: PersonTableViewCell.self)) { (row: Int, cellModel: UserSummaryViewModeling, cell: UserTableViewCell) in
            cell.viewModel = cellModel
            viewModel.fetchPeople(after: row)
        }
        .disposed(by: disposeBag)

    /// etc…
}

However, this was poor enforcement of access control and from within the view, if I was evil, I could do something like this:

viewModel.personViewModels.value = []

All my people are gone!!!

What I really want is:

public protocol PeopleViewModel {
    var personViewModels: Driver<[PersonViewModel]> { get }
    var isLoading: Driver<Bool>
    var isEmpty: Driver<Bool>
    func fetchPeople(after index: Int)
}

The implementation of which would look like:

final class PeopleViewModelImp {
    let personViewModels: Driver<[PersonViewModel]> { get }
    let isLoading: Driver<Bool>
    let isEmpty: Driver<Bool>

    fileprivate let items = Variable([PersonViewModel]())
    fileprivate let isEmptyVariable = Variable(false)
    fileprivate let isLoadingVariable = Variable(false)
    fileptivate let apiClient: APIClient
    fileprivate let disposeBag: DisposeBag

    init(apiClient: APIClient) {
        personViewModels = items.asDriver()
        isEmpty = isEmptyVariable.asDriver()
        isLoading = isLoadingVariable.asDriver()
        self.apiClient = apiClient
        fetchPeople(after: 0)
    }

    func fetchPeople(after index: Int) {
        isLoadingVariable.value = true

        apiClient.fetchPeople().subscribe(onNext: { people in
            let viewModels = people.map(PersonViewModelImp.init(person:))
            items.value += viewModels
            isEmptyVariable.value = items.value.isEmpty
            isLoadingVariable.value = false
        })
        .disposed(by: disposeBag)
    }
}

Tada!

Now, in an ideal world, the fetching of the people would not be disposed of within the actual view model. The aim is to actually keep the view model free of a DisposeBag. For now, this is fine, and certainly an improvement over the original.

UIStackView Disappointments

When working with UIStackView within Interface Builder I would frequently encounter errors that would tell me the an impossible co-efficient could not be reconciled within auto-layout. I fixed this by not using UIStackView. This was a difficult decision to make and only came after losing a few hours to these issues. I had actually argued to target iOS 9 in large part because of my excitement about this new tool; hopefully Apple will fix it in iOS 10. 😞

The aforementioned error:

Cannot find an outgoing row head for incoming head during optimization of variable with near-zero coefficient, which should never happen.

Yet another, more frustrating problem I was hitting involved the reliable crashing of Xcode when working with 4 horizontal stack views within a larger vertical stack view. I lost half a day to this and once again accepted that I would have to pretend UIStackView does not exist.

Someone let me know if they ever fix it. 😒