MapKit Tutorial: Clustering in iOS 11

With the iOS 11 announcement one of the things that most excited me was the MapKit updates, the highlight of course being clustering. Let's look at an implementation now.

Read More

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. 😒