Framework
spotify/Mobius.swift avatar
spotify/Mobius.swift

Mobius.swift: a state machine with effects, from the people who wrote Mobius

A functional reactive framework for managing state evolution and side-effects [Swift implementation]

583 stars43 forksSwiftApache-2.0

At a glance

What is it?
Mobius.swift is Spotify's Swift and Apple ecosystem implementation of the Mobius pattern for managing state evolution and side effects, built around a Model, an Event, an update function and an Effect Handler. Version 0.8.0 arrived in August 2026, and the library targets iOS 10 and later with Swift 5, using Swift Package Manager to build for all Apple platforms.
Who is it for?
Adopt Mobius.swift if you are building an Apple application whose state is complex enough to deserve isolation from its side effects, and if you want a design where the state transition function is a pure function you can test without a view. Do not adopt it for a simple state model, because the counter example in the README takes seven pieces of ceremony and a framework that adds that overhead to a simple app is a net cost.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 79 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What Mobius is, and where this repository sits

Mobius is a functional reactive framework for managing state evolution and side effects. The README states the emphasis directly: separation of concerns, testability, and isolating stateful parts of the code.

The important framing detail is that Mobius.swift is the Swift and Apple ecosystem focused implementation of the original Mobius Java framework. The Java project lives at a different repository under the same organisation, and this one is the port to the language and platform the README's audience works in. The README also points to a wiki with a user guide and to a talk from an Android at Scale event introducing Mobius, which gives you the background on the pattern itself from people who were not writing Swift when they first used it.

The repository contains the core Mobius framework and add-ons for common development scenarios and testing, and the directory listing shows the shape of that. There is MobiusCore/, MobiusExtras/, MobiusTest/, MobiusNimble/ and MobiusThrowableAssertion/, plus Templates/ and Tools/ directories, a Package.resolved, a catalog-info.yaml which is a Backstage catalogue descriptor, and a codecov.yml.

The compatibility table is short and worth quoting in substance: iOS 10.0 or later, Xcode 12.0 or later, and Swift 5.0 as the language. The primary language is Swift, the licence is Apache-2.0, the default branch is master, and the recent releases are 0.8.0 from 2026-08-05, 0.7.0 from 2025-08-21 and 0.6.0 from 2025-02-25. The last push was on 2026-08-05, and the project is not archived.

A sub-1.0 version number on a framework from a large engineering organisation is a signal worth pausing on. It is not unusual for an internal framework to be published at 0.x for years, and the release cadence here, roughly every eight months, is consistent with a library maintained on demand rather than one with a dedicated team. That affects how you should read the API stability guarantees.

Installing it through Swift Package Manager

The installation section is short. Mobius can be built for all Apple platforms using the Swift Package Manager, and you add an entry to your Package.swift:

swift
.package(url: "https://github.com/spotify/Mobius.swift", from: "0.5.0")

The version constraint there starts at 0.5.0, which tells you something about the compatibility range the maintainers consider safe across the 0.x line. The important point for an adopter is that this is a source dependency, not a binary, so the version you resolve is the version you compile, and there is no separate distribution channel to track.

The repository itself is a Swift package with a Package.swift and a Package.resolved, and the presence of a Templates/ directory suggests the project also ships Xcode project templates, which would let a new module in an existing app start with the Mobius wiring already in place rather than leaving each team to write the loop setup by hand.

The add-ons are worth understanding as a set, because they show what the maintainers considered the common needs beyond the core loop. MobiusExtras/ is the add-ons for common development scenarios, MobiusTest/ is the testing support, MobiusNimble/ is integration with the Nimble matcher library, and MobiusThrowableAssertion/ is support for asserting on thrown errors. A framework that ships its own test helpers is telling you that testability is part of the design, not a consequence of it.

The four building blocks, using a counter

The README walks through a counter example, and the reason it works as documentation is that the example is complete enough to be a real program and small enough to fit on a page.

The first building block is the Model. The goal of Mobius is to give you better control over your application state, and the way it puts it is that you can think of your state as a snapshot of all the current values of the variables in your application, encapsulated in a data structure called the Model. The Model can be represented by whatever type you like, and in this example all of the state fits in an Int:

swift
typealias CounterModel = Int

The second is the Event. Mobius does not let you manipulate the state directly. In order to change the state, you have to send the framework messages saying what you want to do, and the framework calls these messages Events. For a counter that means an enum with two cases:

swift
enum CounterEvent {
    case increment
    case decrement
}

The third is the update function, which the framework calls sequentially with every incoming Event and the most recent Model, in order to generate the next Model:

swift
func update(model: CounterModel, event: CounterEvent) -> CounterModel {
    switch event {
    case .increment: return model + 1
    case .decrement: return model - 1
    }
}

These three pieces are the whole core, and they are pure functions. No framework state, no I/O, no closures over mutable boxes. That is what makes the pattern testable, and it is the reason the update function is where the interesting logic lives.

Effects, Next, and the return type that carries both

The fourth building block is the Effect, and the way it is introduced is a good description of why the pattern exists at all.

With the first three pieces in place you can think of the application as transitions between discrete states in response to events. But there is a piece missing, namely the side effects associated with moving between states. The example is a refresh button that puts the application into a loading state, with the side effect of also fetching the latest data from the backend.

In Mobius these side effects are called Effects. The counter example makes them concrete: when the user tries to decrement below 0, play a sound effect instead. The Effects are an enum as well:

swift
enum CounterEffect {
    case playSound
}

The update function is then augmented to also return the set of effects associated with certain state transitions, and the return type changes to Next, which carries either a new model or a set of effects to dispatch:

swift
func update(model: CounterModel, event: CounterEvent) -> Next<CounterModel, CounterEffect> {
    switch event {
    case .increment: 
        return .next(model + 1)
    case .decrement:
        if model == 0 {
            return .dispatchEffects([.playSound])
        } else {
            return .next(model - 1)
        }
    }
}

The .next case returns the next model, and the .dispatchEffects case returns the effects without changing the model, which is why decrementing at zero plays a sound and leaves the model at zero.

The effects you return are sent to something called an Effect Handler, and the README builds one with AVFoundation:

swift
import AVFoundation

private func beep() {
    AudioServicesPlayAlertSound(SystemSoundID(1322))
}

let effectHandler = EffectRouter<CounterEffect, CounterEvent>()
    .routeCase(CounterEffect.playSound).to { beep() }
    .asConnectable

The EffectRouter is a builder that routes a case to a handler, and the .asConnectable at the end is what turns it into something the loop can use.

Everything is tied together with Mobius.loop, which takes the update function and the effect handler and starts from an initial model:

swift
let application = Mobius.loop(update: update, effectHandler: effectHandler)
    .start(from: 0)

And then the application is used by dispatching events:

swift
application.dispatchEvent(.increment) // Model is now 1
application.dispatchEvent(.decrement) // Model is now 0
application.dispatchEvent(.decrement) // Sound effect plays! Model is still 0

Those three lines are the whole user-facing API for the common case. The comments record what happens: the model is 1 after the increment, back to 0 after the first decrement, and after the second decrement the sound plays and the model is still 0.

How to evaluate it, and what the pattern costs

There is a real argument for this pattern and a real cost, and both are visible in the example.

The argument is that state and side effects are separated in a way that is not true of most UI frameworks. The update function is a pure function of model and event, so it can be tested exhaustively without a view, a run loop or a mock. The effect handler is the only place with I/O, so it is the only place you need to stub. The README's emphasis on testability, and the presence of MobiusTest/ and MobiusNimble/ in the repository, are the same point made twice.

The cost is that the shape is unfamiliar, and the example shows exactly where the friction is. A counter needs a typealias, an enum, a function, a second enum, a revised function with a different return type, an effect router and a loop. That is seven pieces of ceremony for a feature that, in a view-controller codebase, is one property and one method. The framework does not remove work; it moves it, and the move is only worth it when the state logic is complicated enough to deserve the isolation.

Three practical checks. First, whether your application's state is genuinely a single value that can be snapshotted. Mobius assumes your state is one Model type, and a codebase whose state is spread across a dozen loosely related properties will either need a large Model or a redesign. Second, whether you are prepared to have effects dispatched rather than performed inline. The example's sound plays after the transition rather than inside it, and code that assumes the effect has already run by the time update returns will be wrong.

Third, the version story. The package is at 0.8.0, the constraint in the README starts at 0.5.0, and the compatibility table lists Swift 5.0 and Xcode 12.0 as the floor, which is a reasonable baseline but a check against your own toolchain. The 0.x number also means you should read CHANGELOG.md before upgrading, and pin the version in your Package.resolved rather than tracking the branch.

If you are starting a new Apple project with genuinely complex state, this is a well-designed reference implementation from a company with the engineering resources to have thought about the failure modes. If your state is a handful of values, the ceremony will cost more than it returns.

Editorial conclusion

Adopt Mobius.swift if you are building an Apple application whose state is complex enough to deserve isolation from its side effects, and if you want a design where the state transition function is a pure function you can test without a view. Do not adopt it for a simple state model, because the counter example in the README takes seven pieces of ceremony and a framework that adds that overhead to a simple app is a net cost. Before you commit, check that your state really can be expressed as one Model value, confirm the Swift and Xcode versions in the compatibility table match your toolchain, and read the CHANGELOG before moving past 0.5.0, since the package is still on a 0.x version and the API is not guaranteed stable.

Frequently asked questions

What is Mobius.swift?

It is a functional reactive framework for managing state evolution and side effects, and it is the Swift and Apple ecosystem focused implementation of the original Mobius Java framework. The emphasis is on separation of concerns, testability and isolating stateful parts of the code.

How do I add Mobius.swift to a Swift project?

Use the Swift Package Manager and add the package entry to your Package.swift, for example .package(url: "https://github.com/spotify/Mobius.swift", from: "0.5.0"). It can be built for all Apple platforms.

What are the Model, Event and Effect concepts in Mobius?

The Model is a data type holding a snapshot of all current application state, the Event is a message describing something you want to do, and the Effect is a side effect such as playing a sound or fetching data, returned from the update function alongside the next model.

What type does the Mobius update function return?

It returns a Next value. In the counter example it returns .next(model + 1) for an increment and .dispatchEffects([.playSound]) when decrementing at zero, so the return type carries either the new state or a set of effects to dispatch.

What platforms and tools does Mobius.swift support?

The compatibility table lists iOS 10.0 or later, Xcode 12.0 or later and Swift 5.0 as the language. The recent releases are 0.8.0 from 2026-08-05, 0.7.0 from 2025-08-21 and 0.6.0 from 2025-02-25, and the repository is Apache-2.0 licensed.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. spotify/Mobius.swift on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spotify-mobius-swift.svg)](https://hysenlabs.com/projects/spotify-mobius-swift)

Was this useful?