Unifying Table and Collection Views Presentation Logic Within MVVM. Part 1
Hello! Using table and/or collection views (I will name them lists throughout the article) is very common for any iOS app. If you care about following programming principles, you are probably wondering how can ViewModel be agnostic about View in its list representation. What is more, you want to be able to setup some list configuration and presentation logic once, and then reuse it throughout your project (or maybe in all your projects).
I hate huge cellForItemAtIndexPath/cellForRowAt methods implementation with several if clauses and frankly speaking it was the main reason I started finding some way to reduce those methods’ body.
In this article I am going to show you my way of building lists is iOS app within MVVM architecture. We will use RxSwift as binding framework (although Bond is also good framework and is my favorite, but is less known).
To start download and open starter project:
We will not pay attention on Coordinators here so we have highly simplified them in the project. In MVVMKit folder we have basic types and protocols that will help us building our app. Let me tell a few words about them.
ViewModel. This is the protocol every view model in the code must implement. There is one optional method to be implemented. This method is called from a view on start.
ViewRepresentable. This is the protocol every view in the code must implement. As you can see it requires a view to have view model property and bindWithModel method. This protocol is derivative from ViewModelTypeErasedViewRepresentable, that will help us to use dynamic polymorphism in datasource implementation.
ViewDependency. The struct defines connection between view class, its reuse identifier, corresponding nib name (if any) and kind (needed when we use UICollectionView headers etc.). We use this struct to register cells’ classes and their reuse identifiers in a view. But who registers them …
Whoever who implement CollectionItemsViewModelDependencyManager protocol. We inject this protocol’s implementation to a view, that further registers all the dependencies using the implementation. This protocol defines component that will give our datasource proper reuse identifier for particular model. If we mostly adopt SomeView-SomeViewModel naming convention then we only need an implementation for models with unusual names and we are all good.
Now let me show our RxDataSource. We have added extension to RxCollectionViewSectionedReloadDataSource to quickly build our datasource in any part of our app using our dependency manager.
As you can see we set two closures (you can add more if you need):
In configureCell closure we:
- We query reuse identifier for the next model from manager
- We then dequeue cell from collection view by the identifier and cast it to ViewModelTypeErasedViewRepresentable & UICollectionViewCell. As any cell will adopt ViewRepresentable protocol we can safely do the casting.
- We now can set the model to the view using typeErasedViewModel property and it will safely (as long as you use proper naming and carefully implement dependency manager) be casted to the view’s model type. Note that without this protocol we wouldn’t be able to do the setting because our ViewRepresentable protocol uses associated ViewModelType type which is constrained to be ViewModel derivative. Our model is of ViewModel type which is not nominal type. Using the type erasure we can encapsulate type casting when setting model to the view.
- Here we inject our top level manager to the lower level view if the view has any views dependencies. This is useful when you have nested collection views, so you define one manager for all the hierarchy.
- Before return we execute preReturnHandler that can be set by a view to do some additional logic. Then we return the cell.
In configureSupplementaryView we have almost the same logic actually, it only varies in model querying.
We also have similar datasource for table view.
In point 3 I pointed out that the casting is safe as long as you use proper naming and carefully implement dependency manager. I realize and admit that this is downside of the approach, that the safety relies on your implementation, it’s not inherited from the language. However, I’ve not found anything better and have never encountered any problems or crashes while using it for last 2 years. Of course you can define model property as optional and do as? casting so you will protect yourself from wrong force casting, but then you’ll need to do unwrapping in your views implementations all the time. That’s some kind of trade off. We will talk about some advanced usage in other parts.
So, that’s all you need to never again write datasource implementations. Never.
In order to define final dependencies for views with lists let’s define two additional protocols.
We also provide additional methods for view related protocol to only write one line in a view representing list for configuring datasource (check it in the starter project).
Next, I will show you how to use all of this to build an app.
Check out Part 2: