Even if we are writing pure Swift code in our app, we still deal with Objective-C Frameworks like UIKit.
Let's take a look at how we can improve our MVVM architecture by leveraging a little bit of Objective-C runtime and Protocol Oriented Programming.
Popular MVVM setup
Pretty standard approach for MVVM architecture is to have both UIView
and UIViewController
have a corresponding ViewModel
.
An improvement would be to introduce a ViewModelProtocol
that would contain a minimal interface that the View layer needs.
Advantages of using a protocol
:
- You can provide completely different implementations of said protocol without any View changes
- You can provide a simple stub ViewModel for tests
- If you have multiple sources for the ViewModel, this makes it easy to keep them completely separate instead of adding logic cases.
- You are required to think through what is needed for the View to function properly
- Simpler to define minimal public interface
- Allows to hide underlying complexity from the View
e.g.
protocol ListViewModelType {
var title: NSAttributedString { get }
var numberOfItems: Int { get }
func item(forIndexPath indexPath: NSIndexPath) -> ItemViewModel
}
final class ListViewController: UIViewController {
var viewModel: ListViewModelType? {
didSet {
label.attributedText = viewModel?.title
tableView.reloadData()
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel?.numberOfItems(section) ?? 0
}
// rest of the implementation...
}
Problems
Let's start with one distinction first.
A View might be Reusable
or NonReusable
.
It is important that the developer designing the class prepares it for reuse scenario, it requires properly managing state around the viewModel
that might be changing.
The distinction between those 2 cases is significant.
I noticed few issues with a standard approach like that:
- There is no indication whether class supports Reuse or not in the first setup.
- You can pass it
nil
as the ViewModel - Skipping
nil
and returning some stub value is not great, in fact, it might be hiding a potential serious programmer mistake. - Doing
nil
handling can get you only so far before you have to crash. The moment you hit an API that is not optional you are in trouble e.g.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let itemViewModel = viewModel.item(forIndexPath: indexPath) else {
fatalError("Asked viewModel for a cell out of range")
}
// rest of the implementation...
}
Alternatives
Not leveraging Interface Builder
final class ListViewController: UIViewController {
let viewModel: ListViewModelType
init(viewModel: ListViewModelType)
// ...
}
Pros:
- Clear indication whether ViewModel is reusable or not (
var
vslet
). NonReusable
case no longer needs to deal with potentialnil
values.- Can't pass nil to
NonReusable
view.
Cons:
- You have to write all layouts in code
- Can't leverage all the good things Storyboards brings to the table, because they do not support custom
init
injection. Arek Holko wrote great article on how you can use init injection with Xib's. - Won't help remove boilerplate from
Reusable
scenario and things likeUICollectionViewCell
/UITableViewCell
since that still can't use init injection.
Note: from now on this article now assumes you still want to leverage interface builder, and can't just use initializer injection
Implicitly Unwrapped Optional
var viewModel: ListViewModelType!
Pros:
- Gets rid of wishy washy handling of
nil
values, it improves on that by following "crash early crash often" approach.
Cons:
- There is no indication whether class supports Reuse or not.
- You can pass it
nil
as the ViewModel
Default value
Example from Artsy MVVM in Swift
lazy var viewModel: ListingsViewModelType = {
return ListingsViewModel(
selectedIndexSignal: self.switchView.selectedIndexSignal,
showDetails: self.showDetailsForSaleArtwork,
presentModal: self.presentModalForSaleArtwork
)
}()
Pros:
- Get's rid of
nil
values
Cons:
- Forces you to introduce dependencies and concrete implementation details into your View
- No
Reusable
variant withlazy var
approach - There is no indication whether class supports Reuse or not.
- Complicates dependency injection and Flow Coordination
A New Hope - MVVM+ with Protocol Oriented Programming and Runtime
Let's create an alternative approach.
One that removes unnecessary boilerplate and leverages:
Design goals
MVVM goes well in pair with some Observable
library like RxSwift.
Rx has a type called DisposeBag
, and it is a container for any subscriptions / cleanup logic.
- Minimal boilerplate
- Automatic cleanup
- It has to work with pure Swift value types and classes
- Clean distinction between
Reusable
andNonReusable
support NonReusableViewModelOwner
- Impossible to pass in nil, generating compile time error.
- assertionFailure when you set subsequent `viewModel'.
- fatalError if you try to access
viewModel
before it is configured ReusableViewModelOwner
- Allows setting nil as the
viewModel
. - Don't require users to react to
nil
values
Let's start with minimal interface goal and clean distinction between different Reuse
versions.
This is the only requirement you have to provide in your classes:
NonReusableViewModelOwner
:
class ListViewController: UIViewController, NonReusableViewModelOwner {
func viewModelDidChange(vm: ListViewModelType, disposeBag: DisposeBag) {}
}
ReusableViewModelOwner
:
class ListReusableView: UIView, ReusableViewModelOwner {
func viewModelDidChange(vm: ItemViewModelType, disposeBag: DisposeBag) {}
func prepareForReuse() {}
}
Note: If your base class implements prepareForReuse
then it already fills prepareForReuse requirement. This requirement exists only to force you to think about Reuse
when conforming to that protocol
This is what will happen:
let nonReusable = ListViewController()
let x = nonReusable.viewModel // not configured, fatalError(Programmer mistake)
nonReusable.viewModel = ListViewModel() //! initial configuration
nonReusable.viewModel = ListViewModel() //! assertFailure(This object isn't supporting reusable viewModel)
let reusable = ListReusableView()
reusable.viewModel = ItemViewModel() // didSetViewModel
reusable.viewModel = nil // cleanup()
reusable.viewModel = ItemViewModel() // didSetViewModel
reusable.viewModel = ItemViewModel() // cleanup() -> didSetViewModel
Implementation tidbits
Let's take a look at the protocol
for NonReusableViewModelOwner
:
public protocol NonReusableViewModelOwner: class {
associatedtype ViewModelProtocol
var viewModel: ViewModelProtocol { get set }
func didSetViewModel(viewModel: ViewModelProtocol, disposeBag: DisposeBag)
}
- The owner has to be a class. Otherwise, there is no identity we can use.
- The type of the
ViewModelProtocol
will be inferred from thedidSetViewModel
function - The only interface difference between
NonReusableViewModelOwner
andReusableViewModelOwner
is the fact thatReusableViewModelOwner
uses optional forViewModelProtocol?
.
Now let's implement default implementation for this:
private func viewModelDisposeBag(fromObject owner: NSObject, dispose: Bool = false) -> DisposeBag {
let bag: DisposeBag = {
let currentBag: DisposeBag? = owner.getAssociatedObject(forKey: &ViewModelOwnerKeys.reuseBag)
return currentBag ?? DisposeBag()
}()
if dispose {
bag.dispose()
}
owner.setAssociatedObject(bag, forKey: &ViewModelOwnerKeys.reuseBag, policy: .retain)
return bag
}
extension NonReusableViewModelOwner where Self: NSObject {
public var viewModel: ViewModelProtocol {
set {
let previousVM: ViewModelProtocol? = getAssociatedObject(forKey: &ViewModelOwnerKeys.viewModel)
assert(previousVM == nil, "\(self.dynamicType) doesn't support reusable viewModel. Use ReusableViewModelOwner instead.")
setAssociatedObject(newValue, forKey: &ViewModelOwnerKeys.viewModel, policy: .retain)
let bag = viewModelDisposeBag(fromObject: self, dispose: true)
didSetViewModel(newValue, disposeBag: bag)
}
get {
// swiftlint:disable:next force_unwrapping
return getAssociatedObject(forKey: &ViewModelOwnerKeys.viewModel)!
}
}
}
setAssociatedObject
andgetAssociatedObject
are just wrappers for Objective-C associated objects that support pure Swift value types via aBox
wrapper.- We use runtime to store the
viewModel
property, the underlying value will always either benil
orViewModelProtocol
type because there is no way of setting it outside of strongly Typed interface. - Safety net: If somehow we did not find our mistake in development or QA stage and passed in subsequent
viewModel
values after initial configuration: this implementation behaves correctly by removing previous registrations and acting asReusableViewModelOwner
.
Using this approach in your app is simple
- Conform to either
NonReusableViewModelOwner
orViewModelOwner
- Implement
didSetViewModel
function and implement your logic
e.g.
class UserSettingsTextSizeFooter: UITableViewHeaderFooterView, ReusableViewModelOwner {
func didSetViewModel(viewModel: ViewModel, disposeBag: DisposeBag) {
//! if using observables
viewModel.observableTitle.bindTo(instructionsLabel.rx_text)
//! if not using observables
instructionsLabel.text = viewModel.title
instructionsLabel.accessibilityLabel = viewModel.title
}
}
You do not even have to declare viewModel
property, you get that for free.
Conclusion
MVVM+ approach might not be ideal, but I believe it is a significant improvement over many popular approaches.
Give it a try! Let me know if you have ideas for improvement.