A lightweight Elm-like Store for SwiftUI

Last update: May 14, 2022

ObservableStore

A simple Elm-like Store for SwiftUI, based on ObservableObject.

ObservableStore helps you craft more reliable apps by centralizing all of your application state into one place, and making all changes to state deterministic. If you’ve ever used Elm or Redux, you get the gist. All state updates happen through actions passed to an update function. This guarantees your application will produce exactly the same state, given the same actions in the same order.

Because Store is an ObservableObject, it can be used anywhere in SwiftUI that ObservableObject would be used.

Store is meant to be used as part of a single app-wide, or major-view-wide component. It deliberately does not solve for nested components or nested stores. Following Elm, deeply nested components are avoided. Instead, it is designed for apps that use a single store, or perhaps one store per major view. Instead of decomposing an app into many stateful components, ObservableStore favors decomposing an app into many stateless views that share the same store and actions. Sub-views can be passed data through bare properties of store.state, or bindings, which can be created with store.binding, or share the store globally, through EnvironmentObject. See https://guide.elm-lang.org/architecture/ and https://guide.elm-lang.org/webapps/structure.html for more about this philosophy.

Example

A minimal example of Store used to increment a count with a button.

import SwiftUI
import Combine
import ObservableStore

/// Actions
enum AppAction {
    case increment
}

/// Services like API methods go here
struct AppEnvironment {
}

/// App state
struct AppState: Equatable {
    var count = 0

    /// State update function
    static func update(
        state: AppState,
        action: AppAction,
        environment: AppEnvironment
    ) -> Update<AppState, AppAction> {
        switch action {
        case .increment:
            var model = state
            model.count = model.count + 1
            return Update(state: model)
        }
    }
}

struct AppView: View {
    @StateObject var store = Store(
        update: AppState.update,
        state: AppState(),
        environment: AppEnvironment()
    )

    var body: some View {
        VStack {
            Text("The count is: \(store.state.count)")
            Button(
                action: {
                    // Send `.increment` action to store,
                    // updating state.
                    store.send(.increment)
                },
                label: {
                    Text("Increment")
                }
            )
        }
    }
}

State, updates, and actions

A Store is a source of truth for application state. It's an ObservableObject, so you can use it anywhere in SwiftUI that you would use an ObservableObject—as an @ObservedObject, a @StateObject, or @EnvironmentObject.

Store exposes a single @Published property, state, which represents your application state. state is read-only, and cannot be updated directly. Instead, like Elm or Redux, all state changes happen through a single update function, with the signature:

(State, Action, Environment) -> Update<State, Action>

The Update returned is a small struct that contains a new state, plus any optional effects and animations associated with the state transition (more about that in a bit).

state can be any Equatable type, typically a struct. Before setting a new state, Store checks that it is not equal to the previous state. New states that are equal to old states are not set, making them a no-op. This means views only recalculate when the state actually changes. Additionally, because state is Equatable, you can make any view that relies on Store, or part of Store, an EquatableView, so the view’s body will only be recalculated if the values it cares about change.

Getting and setting state in views

There are a few different ways to work with Store in views.

Store.state lets you reference the current state directly within views. It’s read-only, so this is the approach to take if your view just needs to read, and doesn’t need to change state.

Text(store.state.text)

Store.send(_) lets you send actions to the store to change state. You might call send within a button action, or event callback, for example.

Button("Set color to red") {
    store.send(AppAction.setColor(.red))
}

Store.binding(get:tag:) lets you create a binding that represents some part of the state. A get function reads the state into a value, a tag function turns a value set on the binding into an action. The result is a binding that can be passed to any vanilla SwiftUI view, yet changes state only through deterministic updates.

TextField(
    "Username"
    text: store.binding(
        get: { state in state.username },
        tag: { username in .setUsername(username) }
    )
)

Or, shorthand:

TextField(
    "Username"
    text: store.binding(
        get: \.username,
        tag: .setUsername
    )
)

You can also create bindings for sub-properties, just like with any other SwiftUI binding. Here's an example of creating a binding to a deep property of the state:

TextField(
    "Bio"
    text: store
        .binding(
            get: { state in state.settings },
            tag: { settings in .setSettings(settings) }
        )
        .profile
        .bio
)

Bottom line, because Store is just an ordinary ObservableObject, and can produce bindings, you can write views exactly the same way you write vanilla SwiftUI views. No special magic! Properties, @Binding, @ObservedObject, @StateObject and @EnvironmentObject all work as you would expect.

Effects

Updates are also able to produce asyncronous effects via Combine publishers. This lets you schedule asyncronous things like HTTP requests or database calls in response to actions. Using effects, you can model everything via a deterministic sequence of actions, even asyncronous side-effects.

Effects are modeled as Combine Publishers which publish actions and never fail.

For convenience, ObservableStore defines a typealias for effect publishers:

public typealias Fx<Action> = AnyPublisher<Action, Never>

The most common way to produce effects is by exposing methods on Environment that produce effects publishers. For example, an asyncronous call to an authentication API service might be implemented in Environment, where an effects publisher is used to signal whether authentication was successful.

struct Environment {
    // ...
    func authenticate(credentials: Credentials) -> AnyPublisher<Action, Never> {
      // ...
    }
}

You can subscribe to an effects publisher by returning it as part of an Update:

func update(
    state: State,
    action: Action,
    environment: Environment
) -> Update<State, Action> {
    switch action {
    // ...
    case .authenticate(let credentials):
        return Update(
            state: state,
            fx: environment.authenticate(credentials: credentials)
        )
    }
}

Store will manage the lifecycle of any publishers returned by an Update, piping the actions they produce back into the store, producing new states, and cleaning them up when they complete.

Animations

You can also drive explicit animations as part of an Update.

Use Update.animation to set an explicit Animation for this state update.

func update(
    state: State,
    action: Action,
    environment: Environment
) -> Update<State, Action> {
    switch action {
    // ...
    case .authenticate(let credentials):
        return Update(state: state).animation(.default)
    }
}

When you specify a transition or animation as part of an Update thisway, Store will use it when setting the state for the update.

GitHub

https://github.com/subconsciousnetwork/ObservableStore
Comments
  • 1. Reorder update fn signature to State, Action, Environment

    This PR changes the order of update function arguments:

    • Changes update function signature from update(State, Environment, Action) to update(State, Action, Environment)
    • Changes Store from Store<State, Environment, Action> to Store<State, Action, Environment>

    Why?

    • This order of arguments is used by two other Elm-like libraries: Swift Composable Architecture and Redux-like state container.
    • Context arguments like environment often seem to be passed last in Swift. E.g. UIViewRepresentable.Context is passed last to methods in SwiftUI.
    • State and Action are always used, but Environment might be nil, or an empty struct. You don't always need environment. It is optional.
    • The order reads better in the signature, since state and action are always paired together in update function and Update struct.
    Reviewed by gordonbrander at 2022-03-26 16:36
  • 2. Create binding form that does not set animation

    Previously if no animation was passed to binding, we would send withAnimation(nil). However, we don't want to set withAnimation(nil) unless explicitly asked, as this may override withAnimation called elsewhere.

    Instead, we now introduce two forms of Store.binding:

    Store.binding(get:tag:)
    Store.binding(get:tag:animation:)
    

    The first form does not call withAnimation, leaving the transaction state alone.

    Reviewed by gordonbrander at 2022-03-25 21:26
  • 3. Update arg labels for send, subscribe

    Fixes #7

    Per https://www.swift.org/documentation/api-design-guidelines/#argument-labels,

    If the first argument forms part of a grammatical phrase, omit its label.

    This is the case for store.send(_), so we remove the label.

    When the first argument forms part of a prepositional phrase, give it an argument label.

    We subscribe to a publisher, therefore store.subscribe(to:)

    Reviewed by gordonbrander at 2022-03-25 21:04
  • 4. Introduce `mergeFx`

    Fixes #8.

    This PR also removes Update.transaction(_) since name conflicts with Update.transaction property. Note I decided against adding fx for the same reason. I will avoid adding plain old setters for now, since chaining sets is not really the most common need.

    If we find we really need more than property setters for simple sets in the future, we may want to add a map function instead.

    Reviewed by gordonbrander at 2022-03-25 20:41
  • 5. Deprecate send(action:) in favor of send(_)

    The action label is redundant, and unnecessary vis Swift's own style guide https://www.swift.org/documentation/api-design-guidelines/#argument-labels.

    Consider removing.

    Reviewed by gordonbrander at 2022-03-21 23:28
  • 6. Receive Fx on main thread

    Receive Fx on main thread. This does two important things:

    First, SwiftUI requires that any state mutations that would change views happen on the main thread. Receiving on main ensures that all fx-driven state transitions happen on main, even if the publisher is off-main-thread.

    Second, if we don’t schedule to receive on main, it is possible for publishers to complete immediately, causing receiveCompletion to attempt to remove the publisher from cancellables before it is added. This was happening with Empty() publishers returned by default when you do not specify Update(fx:).

    By scheduling to receive publisher on main, we force publisher to complete on next tick, ensuring that it is always first added, then removed from cancellables.

    This is an alternative to the flag-based approach in https://github.com/gordonbrander/ObservableStore/blob/2022-03-12-fx-tests-2/Sources/ObservableStore/ObservableStore.swift#L175.

    More background

    Cancellables will cancel themselves they are released (makes sense). This means you must keep cancellables alive for the duration of the lifetime of the publisher by storing them somewhere. In our case, we store them in a map of [UUID: Cancellable].

    So anyway, we remove them when publisher completes in the receiveCompletion. HOWEVER, it is possible for a publisher to IMMEDIATELY complete during the same tick. This causes the receiveCompletion code which removes the cancellable from the map to run BEFORE the cancellable is added to the map. The result is that these immediately-completing cancellables leak, in the sense that they build up and are never removed. Cancellables are very tiny, so we didn't notice the impact on memory, but it is bad hygiene.

    Reviewed by gordonbrander at 2022-03-13 04:11
  • 7. Allow specifying transaction for Update

    Allow updates to specify transaction under which state update should take place.

    This allows update functions to drive explicit animations. Animations, as well as state changes, can be specified.

    Reviewed by gordonbrander at 2022-03-05 00:31
  • 8. receiveValue with weak self closure

    This allows store to be released before effect is complete, without creating a retain cycle. Ordinarily store's lifetime is the lifetime of the app, so this should not be an issue for single-store apps, but a weak reference to self should used here regardless. Additionally, this will now avoid a possible retain cycle if using multiple short-lived stores, such as one per component.

    Reviewed by gordonbrander at 2022-03-03 22:59
🤖 RxSwift + State Machine, inspired by Redux and Elm.
🤖 RxSwift + State Machine, inspired by Redux and Elm.

RxAutomaton RxSwift port of ReactiveAutomaton (State Machine). Terminology Whenever the word "signal" or "(signal) producer" appears (derived from Rea

Jun 29, 2022
🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.
🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.

NOTE: This repository has been discontinued in favor of Actomaton. ?? Harvest Apple's Combine.framework (from iOS 13) + State Machine, inspired by Elm

Apr 26, 2022
Store-App - Store app made for IOS using Swift programming language
Store-App - Store app made for IOS using Swift programming language

Store-App Store app views products, cart, and using login from https://fakestore

Jan 1, 2022
An experimental time traveling state store for SwiftUI
An experimental time traveling state store for SwiftUI

SwiftUI Time Travel A SwiftUI state store and view that allow you to scrub through an application's state. This is a super rough prototype: it's only

Jul 7, 2022
💎 Redux like architecture for SwiftUI
💎 Redux like architecture for SwiftUI

Simple Architecture like Redux Installation SPM dependencies: [ .package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0

Jun 21, 2022
Simple and lightweight Functional Reactive Coding in Swift for the rest of us
Simple and lightweight Functional Reactive Coding in Swift for the rest of us

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find. This library does not use the term FRP (Functional R

Jun 29, 2022
Lightweight observations and bindings in Swift
Lightweight observations and bindings in Swift

What is Hanson? Hanson is a simple, lightweight library to observe and bind values in Swift. It's been developed to support the MVVM architecture in o

Dec 27, 2021
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

Jan 29, 2022
📬 A lightweight implementation of an observable sequence that you can subscribe to.
📬 A lightweight implementation of an observable sequence that you can subscribe to.

Features Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minima

Jun 10, 2022
Simple, lightweight swift bindings

Bindy Just a simple bindings. Installation Add pod 'Bindy' to your podfile, and run pod install SPM is supported too. Usage For now, Bindy has a coupl

Mar 3, 2021
A lightweight, event-driven architectural framework

Trellis Trellis features a declarative DSL that simplifies service bootstrapping: let cluster = try await Bootstrap { Group { Store(model:

Jun 9, 2022
A barebone, thread-safe Redux-like container for Swift.

SwiftTinyRedux SwiftTinyRedux is a barebone, thread-safe Redux-like container for Swift. It features a minimal API and supports composable reducers. I

Jun 24, 2022
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.

Verge.swift ?? An effective state management architecture for iOS - UIKit and also SwiftUI ?? _ An easier way to get unidirectional data flow _ _ Supp

Jul 31, 2022
A powerful, minimal and composable architecture for building reactive iOS apps with SwiftUI or UIKit

SourceArchitecture A simple yet powerful framework for reactive programming with only a minimal optimized set of types. Sources are self-contained, hi

Dec 17, 2021
Sample iOS application in SwiftUI presenting Redux architecture
Sample iOS application in SwiftUI presenting Redux architecture

SwiftUI-Redux-Demo Sample iOS application in SwiftUI presenting Redux architecture. My full article about Redux in detail you will find here: Redux ar

Jun 22, 2022
Netflix Onboarding made with SwiftUI
Netflix Onboarding made with SwiftUI

OnBoardSwiftUI-Netflix Netflix Onboarding made with SwiftUI.

Aug 5, 2022
Flux for SwiftUI, inspired by Vuex
Flux for SwiftUI, inspired by Vuex

⚠️ Fluxus is no longer maintained, and may not be using latest SwiftUI best practices. ?? I encourage you to look at the source of Fluxus. If you do,

Jul 31, 2022
Eazy is the missing piece in your SwiftUI and UIKit application.

Eazy is the missing piece in your SwiftUI and UIKit application. It aims at harmonizing how your views communicate with the model and vice versa in a clear and consistent way. Eazy can be used on any Apple platform.

Jun 7, 2022
Elm-parcel-capacitor - A sample setup to build an app with Elm, Capacitor, Parcel and Tailwind CSS

Elm, Capacitor, Parcel and Tailwindcss This project is a sample setup to build a

May 9, 2022
A simple Elm-like Store for SwiftUI, based on ObservableObject

ObservableStore A simple Elm-like Store for SwiftUI, based on ObservableObject.

May 14, 2022