Skip to content

Using MobiusController

Jens Ayton edited this page Apr 22, 2020 · 2 revisions

In earlier parts of the documentation, we’ve been using “raw” Mobius loops. A raw loop is single-threaded, and keeps its effect handler and event source connections open until it’s disposed of. MobiusController adds a layer of sophistication on top of this, running a loop on a background queue and adding start-stop semantics.

Running a loop on a background queue isn’t usually a great performance win, but it actually simplifies matters when dealing with asynchronous effect handlers. In real-world code, it is common for effect handlers to interact with APIs that use completion handlers, which generally won’t all fire on the same queue. If you use a raw MobiusLoop, you are responsible for avoiding concurrent access to the loop. When you use a MobiusController, events can be dispatched from arbitrary threads and will automatically be handled on one queue managed by the controller.

A MobiusController is created by calling makeController on a Mobius.Builder, instead of start:

let controller = Mobius.loop(update: myUpdate, effectHandler: myEffectHandler)
    .withEventSource(myEventSource)
    .withLogger(SimpleLogger(tag: "my loop"))
    .makeController(from: model, initiate: myInitiate)

// MobiusController starts out in a stopped state
controller.start()

The MobiusController lifecycle

Unlike a raw loop, a Mobius controller can be stopped and restarted. For example, when a Mobius controller is associated with a UIViewController, it makes sense to start it in viewWillAppear and stop it in viewDidDisappear to avoid unnecessary state management when the view is hidden.

When the controller is stopped, it disconnects from the effect handler and event source, and any events that have already been queued up will be ignored.

When the controller is restarted, there is often a need to resynchronize state with collaborator objects, or to back the model out of a temporary state. This is done using an initiator function which is passed to makeController.

Observing the model

With raw loops, the model can be observed through simple functions registered with addObserver, which normally stay registered for the lifetime of the loop.

In order to handle the start-stop behaviour of MobiusController, a more complex solution is used, the view connectable. A view connectable implements a connect function, which takes an event consumer function and returns a connection. The connection’s acceptClosure will be called with every new model, and its disposeClosure will be called when the loop is stopped.

The view connectable can only be set (or removed) when the controller is stopped.

let controller = Mobius.loop(update: myUpdate, effectHandler: myEffectHandler)
    .makeController(from: model, initiate: myInitiate)

controller.connectView(myViewConnectable)

controller.start()

When not to use MobiusController

We recommend defaulting to MobiusController rather than raw loops. However, raw loops have one potential advantage: they operate synchronously. Calling dispatchEvent on a raw loop will immediately:

  • Call update to handle the provided event
  • Update the model
  • Call the effect handler to handle any effects issued by the update call
  • Handle any events issued by the effect handler before it returns. (The effect handler may asynchronously dispatch effects later; we don’t wait for those.)

This means that you can implement synchronous interfaces using a MobiusLoop by calling dispatchEvent immediately followed by reading latestModel. Since MobiusController does everything asynchronously, there is no simple equivalent.