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.