A Different Take on MVVM with Swift

Datetime:2016-08-23 01:24:21          Topic: MVVM Model  Swift           Share

31 May 2016

Model-View-ViewModel or MVVM is everywhere these days. There isn’t a day I don’t see a blog post or a tweet about it. It’s being praised into havens. But what exactly is MVVM? What is a ViewModel? What are its concerns? Many seem to have different opinions.

I’ve been using MVVM for two years now. I started with it in Objective-C, continued with Swift and now I’m combining it with FRP because they’re nice fit, as if they’re made for each other.

Some of you might have read my previous articles about MVVM. I don’t do it like that any more. Not that there is anything wrong with it, but I’ve found better ways. More than a year has passed. A lot has changed. I’ve even stopped calling it MVVM. Doh.

In next few paragraphs I’ll describe what I found to be the most convenient app architecture for me in 2016. Note that I haven’t invented anything new, this is just my way of doing MVVM, i.e. a variation of it for which I have an analogy that makes it easier to reason about it. I call it Scene-Stage-Director .

Stage

The uppermost component of MVVM is the View. It’s what user sees, where the appearance is defined, layout calculated, animations performed, etc. On iOS however we don’t work just with views, we work with view hierarchies managed by the view controllers. So to avoid any confusion, instead of View, I’ll call the uppermost layer Stage. Because it’s where the play unfolds.

Stage is comprised of a view controller and its views. Its concern is displaying the content and UI to the user. It knows nothing about business or application logic. It knows nothing about networking, services or persistence. On the other hand, it knows about layout, appearance and animations.

People tend to say that views should be dumb. That might be the case on some platforms, but I don’t agree it’s true on iOS. On iOS views are dynamic and alive and to provide liveliness they do stuff. They calculate, they transform and they react. They are logical and they are mathematical. They are also declarative and often imperative.

Director

What content does the stage present? How does it react to user feedback? Those aspects are directed by the Director. Hence the name. It’s the component that’s called ViewModel in MVVM. But I don’t like that name. I don’t believe it expresses its concerns well. Model usually refers to some data, scheme or the persistence, often static. It says nothing about application logic.

Director represents application logic of a use case. It interacts with lower-level components from which it gets the content, transforms it and provides it to the stage. It’s the intermediary between application services and the stage. Additionally, it ‘observes’ user actions and inputs through stage and implements logic that reacts to those events. Director is ideal fit for functional reactive paradigm.

Scene

Questions I had most trouble with are: Who creates the stage (the view controller)? Who creates the director? Who presents the view controller? That’s where the Director-Stage analogy helped me clarify my though process. The missing part I needed was Scene. I like to think of it as a public part of Scene-Stage-Director triad. Scene creates the stage and the director, sets the director to the stage and knows how to present the stage onto a given context.

Scene also presents other scenes. It observes the director outputs for presentations events and presents destination scenes upon them. Scene couples other components, it should have no logic in itself. Scene should be instantiated with the data it needs to show the content (e.g. user ID).

Context

Among many other concerns, view controllers also handle navigation (presentation of other view controllers). That makes it hard to reason about navigation logic in the app. If you add custom transitions to that, things get out of control pretty fast. Easy way to solve this is to abstract presentation interface. For example, navigation controller could be abstracted into

protocol NavigationContext {
  func push(stage: UIViewController)
  func pop()
}

The scene will be given an object conforming to this protocol and it’ll just push its stage to it. How the context pushes the stage is not important. Is it animated, is it custom transition or something completely different is the concern of the concrete implementation of the context.

Adhering to this Architecture

Here are some rules and examples that should be followed if applying this architecture.

Stage is usually just a UIViewController subclass

It could also be a UIView or an OpenGL view but you’ll probably never go down that path.

class Stage: UIViewController {
}

Stage owns its Director

Upper layers should always own lower layers, as they depend on them. Same is in the Stage-Director relationship.

class Stage: UIViewController {
  let director: Director
}

Director is injected into the Stage

If stage would create the director, it would have to know about all the dependencies that the director needs. We don’t want that, so we’ll just inject it. There is one problem however. UIViewController does not load the outlets at the instantiation time, but we need the outlets in order to pass their actions to the director.

The solution is take director factory instead, that given the stage itself, returns the director. Then all that’s needed is to wait for viewDidLoad and load the director there.

class Stage: UIViewController {
  init(directorFactory: Stage -> Director)
}

It’s best to create a base class that handles this logic once for all time. Here is one for your convenience.

public class BaseStage<D>: UIViewController {

  /// Director. Do not access before the view loads!
  public var director: D {
    if let director = _director {
      return director
    } else {
      fatalError("Director must not be accessed before view loads.")
    }
  }

  private var _director: D?
  private var directorFactory: (DirectedViewController -> D)!

  public override func viewDidLoad() {
    super.viewDidLoad()
    _director = directorFactory(self)
    directorFactory = nil
    bindDirector(director)
  }
  
  /// Wrapper over the constructor that loads view controller from nib file.
  public init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: NSBundle? = nil, directorFactory: DirectedViewController -> D) {
    self.directorFactory = directorFactory
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }

  /// Convenience class method that loads view controller from a storyboard.
  public static func create(storyboard: UIStoryboard, directorFactory: DirectedViewController -> D) -> DirectedViewController {
    let viewController = storyboard.instantiateInitialViewController() as! DirectedViewController
    viewController.directorFactory = directorFactory
    return viewController
  }

  /// To be overridden by subclasses.
  public func bindDirector(director: D) {}
}

Director is purely reactive

Or at least as pure as it can be. We live in a real word unfortunately. By purely reactive I mean that it takes streams on input and exposes streams on output. Outputs should be derived from inputs using reactive operators.

Director should take input streams in the constructor. It should provide output streams as properties. Output streams should be, preferably compiler-guaranteed, non-failable ( Stream in ReactiveKit, Driver in RxSwift or Signal<T, NoError> in ReactiveCocoa).

Purely reactive director probably won’t have any non-private methods. Director will often also take dependencies like services or APIs on input (in the constructor).

Here is an example of user profile stage director. It takes current session as a dependency and a userWebsiteButtonTap stream from the stage. It provides two outputs to the stage and one to the scene. It transforms inputs to outputs by utilising reactive operators.

class Director {

  /// Stage Outputs
  
  let userName: Stream<String>
  let userWebsiteName: Stream<String>
  
  /// Scene Outputs
  
  let urlToOpen: Stream<NSURL>

  init(session: Session, websiteButtonTap: Stream<Void>) {
    let user /*: Stream<User> */ = session.currentUser().shareReply(1)
    userName = user.map { $0.name } 
    userWebsiteName = user.map { $0.websiteName } 
    urlToOpen = websiteButtonTap.withLatestFrom(user) { _, user in user.websiteURL }
  }
}

Director outputs are observed by the stage

The stage should observe director outputs (that are streams) and update respective views accordingly. Binding should be the preferred way of observing.

If using base stage from above, you’d do binding in bindDirector method override.

  ...
  
  override func bindDirector(director: Director) {
    bind(director.userName, to: nameLabel.rText)
    bind(director.userWebsiteName, to: websiteButton.rTitle)
  }
  
  ...

Director outputs are observed also by the scene

If the scene presents another scene, there should be a stream on the director output that triggers that presentation. The scene should observe that stream and present another scene when the event is received.

Here is an example of the scene method that creates the stage, provides it with the director factory and also observes urlToOpen and opens Safari when the event is received.

public class Scene {

  ...

  func createStage() -> Stage {
  
    // We provide a director factory to the stage 
    return Stage { stage in
    
      let director = Director(
        session: session, 
        websiteButtonTap: stage.websiteButton.rTap
      )
      
      director.urlToOpen.observeNext { url in
        UIApplication.sharedApplication().openURL(url) 
      }.disposeIn(stage.rBag)
      
      return director
    }
  }
}

Scene knows to present itself

Scene should expose an interface that can be used to present the scene. Scene should take a context onto which it should present itself.

public class Scene {

  let context: Context

  public init(context: Context) {
    self.context = context
  }

  public func presentInContext() {
    context.present(createStage())
  }
  
  ...
}

Be flexible

Bad architecture is worse than no architecture. There are no scene or director type protocols on purpose. Once you put them into place you have to live by their rules. Leaving it open makes it flexible. If you don’t need a director, if your stage is static and dumb, don’t create one. Being a purist is noble, but the software is not mathematics.

Where to go from here

I’ve put up an example app on GitHub that applies this architecture. If this looks promising to you, go and check it out. I’ll welcome your feedback as I’m sure there is a room for improvement.

The example is based on ReactiveKit framework I’ve been developing as a successor to Swift Bond, but the architecture presented here is not limited to any specific FRP library. If you’re using RxSwift or ReactiveCocoa you are good to go as those are great libraries I’d recommend at any time.

ReactiveKit is my exercise in FRP and a way to provide something different. As opposed to Observable of RxSwift or Signal/SignalProducer of ReactiveCocoa, ReactiveKit provides Stream type that cannot error out. It’s similar to Driver from RxCocoa, but not the same. Director output streams should use that type because you never want to bind something that can error out to a view. For streams that can error out, ReactiveKit provides Operation type.





About List