Better MVVM setup with POP and Runtime

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.

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 vs let).
  • NonReusable case no longer needs to deal with potential nil 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 like UICollectionViewCell / 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 with lazy 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 and NonReusable 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 the didSetViewModel function
  • The only interface difference between NonReusableViewModelOwner and ReusableViewModelOwner is the fact that ReusableViewModelOwner uses optional for ViewModelProtocol?.

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 and getAssociatedObject are just wrappers for Objective-C associated objects that support pure Swift value types via a Box wrapper.
  • We use runtime to store the viewModel property, the underlying value will always either be nil or ViewModelProtocol 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 as ReusableViewModelOwner.

Using this approach in your app is simple

  • Conform to either NonReusableViewModelOwner or ViewModelOwner
  • 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.

You've successfully subscribed to Krzysztof Zabłocki
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.