Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
spotify/Mobius.swift
Mobius is a functional reactive framework for managing state evolution and side-effects. It emphasizes separation of concerns, testability, and isolating stateful parts of the code.
Mobius.swift is the Swift and Apple ecosystem focused implementation of the original Mobius Java framework. To learn more, see the wiki for a user guide. You can also watch a talk from an Android @Scale introducing Mobius.
This repository contains the core Mobius framework and add-ons for common development scenarios and testing.
Compatibility
Environment | details |
---|---|
đ± iOS | 10.0+ |
đ Xcode | 12.0+ |
đŠ Language | Swift 5.0 |
Installation
Mobius.swift supports most popular dependency managers. Choose your preferred method to see the instructions:
Swift Package Manager
Mobius can be built for all Apple platforms using the Swift Package Manager.
Add the following entry to your Package.swift
:
.package(url: "https://github.com/spotify/Mobius.swift.git", .upToNextMajor(from: "0.4.0"))
CocoaPods
Mobius can only be built for iOS using CocoaPods. For other platforms, please use Swift Package Manager.
Add the following entry in your Podfile
:
pod 'MobiusCore', '0.4.0'
Optionally, you can also choose to integrate MobiusExtras
, MobiusNimble
or MobiusTest
:
pod 'MobiusExtras', '0.4.0'
pod 'MobiusNimble', '0.4.0'
pod 'MobiusTest', '0.4.0'
Carthage
Mobius can only be built for iOS using Carthage. For other platforms, please use Swift Package Manager.
Add the following entry in your Cartfile
:
github "spotify/Mobius.swift" "0.4.0"
There are some additional steps to take as explained in the Carthage documentation.
NOTE: At this moment Carthage doesnât have a way to specify subspecs in a single repo. For this reason, Carthage will automatically pull our dependencies used to provide test helpers in
MobiusNimble
. You can simply choose not to link this library in your project if you donât plan to use it.
Mobius in Action - Building a Counter
The goal of Mobius is to give you better control over your application state. You can think of your state as a snapshot of all the current values of the variables in your application. In Mobius, we encapsulate all of the state in a data structure which we call the Model.
The Model can be represented by whatever type you like. In this example weâll be building a simple counter, so all of our state can be contained in an Int
:
typealias CounterModel = Int
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. We call these messages Events. In our case, weâll want to increment and decrement our counter. Letâs use an enum
to define these cases:
enum CounterEvent {
case increment
case decrement
}
Now that we have a Model and some Events, weâll need to give Mobius a set of rules which it can use to update the state on our behalf. We do this by giving the framework a function which will be sequentially called with every incoming Event and the most recent Model, in order to generate the next Model:
func update(model: CounterModel, event: CounterEvent) -> CounterModel {
switch event {
case .increment: return model + 1
case .decrement: return model - 1
}
}
With these building blocks, we can start to think about our applications as transitions between discrete states in response to events. But we believe there still one piece missing from the puzzle â namely the side effects which are associated with moving between states. For instance, pressing a ârefreshâ button might put our application into a âloadingâ state, with the side effect of also fetching the latest data from our backend.
In Mobius, we aptly call these side effects Effects. In the case of our counter, letâs say that when the user tries to decrement below 0, we play a sound effect instead. Letâs create an enum
that represents all the possible effects (which in this case is only one):
enum CounterEffect {
case playSound
}
Weâll now need to augment our update
function to also return a set of effects associated with certain state transitions. This looks like:
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)
}
}
}
Mobius sends each of the effects you return in any state transition to something called an Effect Handler. Letâs make one of those now:
import AVFoundation
private func beep() {
AudioServicesPlayAlertSound(SystemSoundID(1322))
}
let effectHandler = EffectRouter<CounterEffect, CounterEvent>()
.routeCase(CounterEffect.playSound).to { beep() }
.asConnectable
Now that we have all the pieces in place, letâs tie it all together:
let application = Mobius.loop(update: update, effectHandler: effectHandler)
.start(from: 0)
Letâs start using our counter:
application.dispatchEvent(.increment) // Model is now 1
application.dispatchEvent(.decrement) // Model is now 0
application.dispatchEvent(.decrement) // Sound effect plays! Model is still 0
This covers the fundamentals of Mobius. To learn more, head on over to our wiki.
Status
Mobius.swift is nearing a 1.0 release. We use the framework internally in deployed features, but have recently made a number of breaking changes. Release 0.3.0 breaks compatibility with the previous 0.2.0 release and contains deprecated backwards-compatbility wrappers for some of the smaller changes. These deprecated versions have been removed in the 0.4.0 release. Other additive changes will be made to form Mobius 1.0.
Development
- Clone
- Bootstrap the project
./Tools/bootstrap.sh
- Open Mobius.xcodeproj using Xcode.
- ????
- Create a PR
Code of Conduct
This project adheres to the Open Code of Conduct. By participating, you are expected to honor this code.
Github
link |
Stars: 360 |
Last commit: 2 weeks ago |
You may find interesting
Releases
0.3.0 - 2020-04-06T08:50:55
0.3.0 makes many changes from 0.2.0. Where possible, old names and types are available with deprecation attributes; these will soon be removed.
- Updated threading model:
MobiusLoop
is now single-threadedMobiusController
runs a loop on a single background queue.- Fixed several issues around hard-to-avoid assertions in loop teardown.
- New
EffectRouter
andEffectHandler
replaceEffectRouterBuilder
(which is deprecated along with several helpers). - Effects in
First
andNext
are now an array rather than a set. This doesnât imply an ordering guarantee, but does mean that effects donât have to beHashable
. - Various things renamed or changed from methods to properties to better conform to Swift API Guidelines and for internal consistency:
Initiator
is nowInitiate
, and is only used withMobiusController
.MobiusLoop.getMostRecentModel()
becomeslatestModel
.MobiusController.getModel()
becomesmodel
.Connectable.InputType
andOutputType
becomeInput
andOutput
;Connection.ValueType
becomesValue
.
Update
is now a struct. This isnât leveraged by Mobius itself at the moment, but makes it easier to write transformations on updates in a fluent style.- For consistency,
MobiusController
is created through amakeController()
method onMobius.Builder
instead of being initialized with a builder argument. - All methods on
MobiusLogger
now have default do-nothing implementations. ConsoleLogger
has been replaced withSimpleLogger
, which can take a consumer function to use instead ofprint
.NoEffect
andBrokenConnection
are deprecated.MobiusHooks.ErrorHandler
now returnsNever
rather thanVoid
.- Mobius no longer adds a public extension to
NSRecursiveLock
. - There are more documentation comments than there used to be.
- Tooling updated to Swift 5.0 and Xcode 11.0.
- Swift Package Manager is explicitly supported for all Apple platforms; Carthage and CocoaPods are supported for iOS only.