Skip to content

Mobius and Mobile Apps

Jesper Sandström edited this page Jun 24, 2019 · 3 revisions

Connecting a MobiusLoop to Mobile Apps

As discussed when talking about configuration, a Mobius.Builder is useful if you want to be able to start the same loop many times from different starting points. One example of this is when connecting a MobiusLoop to your mobile app.

Whether you’re using Activities, Fragments, UIViewControllers or some other abstractions, you typically have some concept of a lifecycle or restoring state. There may or may not be a saved state available when your component starts, but if there is some saved state, you should start from it instead from starting from a default state. On top of that there are usually lifecycle callbacks where you need to pause and subsequently resume the execution.

These cases are examples of starting from different Model objects, and the reason why we use Mobius.Builder when connecting Mobius to our app. It allows Mobius to keep track of state for you, and create new loops as required.

Connecting a MobiusLoop to iOS

We will start by creating a Mobius.Builder:

let loopBuilder: Mobius.Builder<MyLoopTypes> = Mobius
    .loop(update: myUpdate, effectHandler: myEffectHandler)
    .withInitiator(myInit)
    .withEventSource(myEventSource)
    .logger(ConsoleLogger<MyLoopTypes>(tag: "my_app"))

To manage its lifecycle we will be using a MobiusLoop.Controller, which allows the loop to be stopped and resumed from the last state.

let controller = MobiusController(builder: loopBuilder, defaultModel: MyModel.makeDefault());

Connecting the MobiusLoop.Controller to a UIViewController

Above we have created a MobiusLoop.Controller and now we are going to to hook it up to the lifecycle events of our UIViewController:

class MyViewController: UIViewController {

    private let loopController: MobiusController<MyLoopTypes>
    // ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loopController.connectView(AnyConnectable<MyModel, MyEvent>(self.connectViews))
        loopController.start()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        loopController.stop()
        loopController.disconnectView()
    }
    //...
}

Now let's implement the one part we’ve left out: connectViews(). The argument to MobiusController.connectView(...) is a Connectable with the form Connectable<Model,Event>; it receives Models instead of Effects. We hold on to the eventConsumer and derive events from UIButton interactions:

    private var eventConsumer: Consumer<MyEvent>?
    // ...

    @objc func emitEvent() {
        eventConsumer?(MyEvent.buttonPressed)
    }

    func connectViews(consumer: @escaping Consumer<MyEvent>) -> Connection<MyModel> {
        self.eventConsumer = consumer
        button.addTarget(self, action: #selector(emitEvent), for: .touchUpInside)

        let accept: (MyModel) -> Void = { model in
            // Keep in mind that this closure can be called from a background thread
            DispatchQueue.main.async {
                self.myLabel.text = model.getText()
            }
        }
        let dispose = {
            self.eventConsumer = nil
            self.button.removeTarget(nil, action: nil, for: .allEvents)
        }
        return Connection(acceptClosure: accept, disposeClosure: dispose)
    }

This becomes the one place where we hook up event listeners to the UI and update the UI based on the Model. Now our MobiusLoop gets created whenever the UIViewController starts, and it’ll stop and resume from where it left off whenever the UIViewController disappears and reappears.