Skip to content
Go back

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:

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:

    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:

Cons:

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:

Cons:

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:

Cons:

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.

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)
}

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)!
      }
    }
}

Using this approach in your app is simple

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.


Share this post on:

Previous Post
Meta-programming in Swift
Next Post
Investing time into developer tools