-
Notifications
You must be signed in to change notification settings - Fork 41
Creating a loop
Let’s build a simple “hello world” in Mobius. We’ll create a simple counter that counts up or down when we send events to the loop. We need to keep track of the current value of the counter, so we’ll be using an Int
as our model, and define an enum with events for increasing and decreasing the value:
enum MyEvent {
case up
case down
}
When we get the up event, the counter should increase, and when we get the down event, it should decrease. To make the example slightly more interesting, let’s say that you shouldn’t be able to make the counter go negative. Let’s write a simplified update function that describes this behaviour (“simplified” in the sense of not supporting Effects – we’ll get back to that later!):
func update(counter: Int, event: MyEvent) -> Int {
switch event {
case .up:
return counter + 1
case .down:
return counter > 0
? counter - 1
: counter
}
}
We are now ready to create the simplified loop:
import MobiusCore
import MobiusExtras
let loop = Mobius.beginnerLoop(update: update)
.start(from: 2)
This creates a loop that starts the counter at 2. Before sending events to the loop, we need to add an observer, so that we can see how the counter changes:
loop.addObserver { counter in print(counter) }
Observers always receive the most recent state when they are added, so this line of code causes the current value of the counter to be printed: “2”.
Now we are ready to send events! Let’s put in a bunch of .up
s and .down
s and see what happens:
loop.dispatchEvent(.down) // prints "1"
loop.dispatchEvent(.down) // prints "0"
loop.dispatchEvent(.down) // prints "0"
loop.dispatchEvent(.up) // prints "1"
loop.dispatchEvent(.up) // prints "2"
loop.dispatchEvent(.down) // prints "1"
Finally, you always want to clean up after yourself:
loop.dispose()
One of Mobius’s strengths is its declarative style of describing side-effects. However, in our first example we had a simplified update function that didn’t use any effects. Let’s expand it to show how you dispatch and handle an effect.
Let’s say that we want to keep disallowing negative numbers for the counter, but now if someone tries to decrease the number to less than zero, the counter is supposed to print an error message as a side-effect.
First we need to create a type for the effects. We only have one effect right now, but let’s use an enum anyway, like we did with the events:
enum MyEffect {
case reportErrorNegative
}
The update function is the only thing in Mobius that triggers effects, so we need to change the signature so that it can tell us that an effect is supposed to happen. In Mobius, the struct Next<Model, Effect>
is used to dispatch effects and apply changes to the model. Let’s start by changing the return type of the update function. The Int
we have used to keep track of the current value of the counter is usually referred to as the model in Mobius, so we change that name too.
func update(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
switch event {
case .up:
return .next(model + 1)
case .down:
return model > 0
? .next(model - 1)
: .next(model)
}
}
Think of Next
as a value that describes “what should happen next”. Therefore, the complete update function describes: ”given a certain model and an event, what should happen next?” This is what we mean when we say that the code in the update function is declarative: the update function only declares what is supposed to occur, but it doesn’t make it occur.
Let’s now change the less-than-zero case so that instead of returning the current model, it declares that an error should be reported:
func update(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
switch event {
case .up:
return Next.next(model + 1)
case .down:
return model > 0
? Next.next(model - 1)
: Next.next(model, effects: [.reportErrorNegative])
}
}
Since we now have an effect, we need an Effect Handler. When an Update function dispatches Effects, Mobius will automatically forward them to the Effect Handler. It executes the Effects, making the declared things happen.
There are several ways to define an Effect Handler, but for most use cases the preferred way is to use EffectRouter
. This lets you describe – again, declaratively – routes from effects to objects or functions that handle those effects.
Our .reportErrorNegative
is a “fire-and-forget” effect, which can be handled by a simple function from Void
to Void
:
func handleReportErrorNegative() {
print("error!")
}
We can declare our single route like so:
let effectHandler = EffectRouter<MyEffect, MyEvent>()
.routeCase(MyEffect.reportErrorNegative).to(handleReportErrorNegative)
.asConnectable
The
asConnectable
property converts the effect router to aConnectable<MyEffect, MyEvent>
, which is the fundamental form of an effect handler. Writing aConnectable
by hand is unnecessarily cumbersome for most effect handlers, which is why we preferEffectRouter
.
Now, armed with our new update function and effect handler, we’re ready to set up the loop again:
let loop: Mobius.loop(update: update, effectHandler: effectHandler)
.start(from: 2)
loop.addObserver { counter in print(counter) }
Like last time it sets up the loop to start from “2”, but this time with our new update function and an effect handler. Let’s enter the same .up
s and .down
s as last time and see what happens:
loop.dispatchEvent(.down) // prints "1"
loop.dispatchEvent(.down) // prints "0"
loop.dispatchEvent(.down) // prints "0", followed by "error!"
loop.dispatchEvent(.up) // prints "1"
loop.dispatchEvent(.up) // prints "2"
loop.dispatchEvent(.down) // prints "1"
It prints the new error message, and we see that it still prints a zero. However, we would like to get only the error message, and not the current value of the counter. Fortunately Next
has the following four static factory methods:
Model changed | Model unchanged | |
---|---|---|
Effects | Next.next(model, effects) | Next.dispatchEffects(effects) |
No Effects | Next.next(model) | Next.noChange |
This enables us to say either that nothing should happen (no new model, no effects) or that we only want to dispatch some effects (no new model, but some effects). To do this you use .noChange
or .dispatchEffects(...)
respectively. We don’t make any changes to the model in the less-than-zero case, so let’s change the update function to use dispatchEffects(...)
:
func update(model: Int, event: MyEvent) -> Next<Int, MyEffect> {
switch event {
case .up:
return .next(model + 1)
case .down:
return model > 0
? .next(model - 1)
: .dispatchEffects([.reportErrorNegative])
}
}
Now let’s send our events again:
loop.dispatchEvent(.down) // prints "1"
loop.dispatchEvent(.down) // prints "0"
loop.dispatchEvent(.down) // prints "error!"
loop.dispatchEvent(.up) // prints "1"
loop.dispatchEvent(.up) // prints "2"
loop.dispatchEvent(.down) // prints "1"
Success!
In this case we merely printed the error to the screen, but you can imagine the effect handler doing something more sophisticated, maybe flashing a light, playing a sound effect, or reporting the error to a server.
Getting Started
Reference Guide
Patterns