Swiftpack.co - Package - SwiftRex/SwiftRex

Build Status codecov Jazzy Documentation CocoaPods compatible Swift Package Manager compatible Swift Platform support License Apache 2.0

Introduction

SwiftRex is a framework that combines Unidirectional Dataflow architecture and reactive programming (Combine, RxSwift or ReactiveSwift), providing a central state Store for the whole state of your app, of which your SwiftUI Views or UIViewControllers can observe and react to, as well as dispatching events coming from the user interactions.

This pattern, also known as "Redux", allows us to rethink our app as a single pure function that receives user events as input and returns UI changes in response. The benefits of this workflow will hopefully become clear soon.

API documentation can be found here.

Quick Guide

In a hurry? Already familiar with other redux implementations?

No problem, we have a TL;DR Quick Guide that shows the minimum you need to know about SwiftRex in a very practical approach.

We still recommend reading the full README for a deeper understanding behind SwiftRex concepts.

Goals

Several architectures and design patterns for mobile development nowadays propose to solve specific issues related to Single Responsibility Principle (such as Massive ViewControllers), or improve testability and dependency management. Other common challenges for mobile developers such as state handling, race conditions, modularization/componentization, thread-safety or dealing properly with UI life-cycle and ownership are less explored but can be equally harmful for an app.

Managing all of these problems may sound like an impossible task that would require lots of patterns and really complex test scenarios. After all, how to to reproduce a rare but critical error that happens only with some of your users but never in developers' equipment? This can be frustrating and most of us has probably faced such problems from time to time.

That's the scenario where SwiftRex shines, because it:

I'm not gonna lie, it's a completely different way of writing apps, as most reactive approaches are; but once you get used to, it makes more sense and enables you to reuse much more code between your projects, gives you better tooling for writing software, testing, debugging, logging and finally thinking about events, state and mutation as you've never done before. And I promise you, it's gonna be a way with no return, an Unidirectional journey.

Reactive Framework Libraries

SwiftRex currently supports the 3 major reactive frameworks:

More can be easily added later by implementing some abstraction bridges that can be found in the ReactiveWrappers.swift file. To avoid adding unnecessary files to your app, SwiftRex is split in 4 packages:

  • SwiftRex: the core library
  • CombineRex: the implementation for Combine framework
  • RxSwiftRex: the implementation for RxSwift framework
  • ReactiveSwiftRex: the implementation for ReactiveSwift framework

SwiftRex itself won't be enough, so you have to pick one of the three implementations.

Parts

Let's understand the components of SwiftRex by splitting them into 3 sections:


Conceptual Parts


Action

There's no "Action" protocol or type in SwiftRex. However, Action will be found as a generic parameter for most core data structures, meaning that it's up to you to define what is the root Action type.

Conceptually, we can say that an Action represents something that happens from external actors of your app, that means user interactions, timer callbacks, responses from web services, callbacks from CoreLocation and other frameworks. Some internal actors also can start actions, however. For example, when UIKit finishes loading your view we could say that viewDidLoad is an action, in case we're interested in this event. Same for SwiftUI View (.onAppear, .onDisappear, .onTap) or Gesture (.onEnded, .onChanged, .updating) modifiers, they all can be considered actions. When URLSession replies with a Data that we were able to parse into a struct, this can be a successful action, but when the response is a 404, or JSONDecoder can't understand the payload, this should also become a failure Action. NotificationCenter does nothing else but notifying actions from all over the system, such as keyboard dismissal or device rotation. CoreData and other realtime databases have mechanism to notify when something changed, and this should become an action as well.

Actions are about INPUT events that are relevant for an app.

For representing an action in your app you can use structs, classes or enums, and organize the list of possible actions the way you think it's better. But we have a recommended way that will enable you to fully use type-safety and avoid problems, and this way is by using a tree structure created with enums and associated values.

enum AppAction {
    case started
    case movie(MovieAction)
    case cast(CastAction)
}

enum MovieAction {
    case requestMovieList
    case gotMovieList(movies: [Movie])
    case movieListResponseError(MovieResponseError)
    case selectMovie(id: UUID)
}

enum CastAction {
    case requestCastList(movieId: UUID)
    case gotCastList(movieId: UUID, cast: [Person])
    case castListResponseError(CastResponseError)
    case selectPerson(id: UUID)
}

All possible events in your app should be listed in these enums and grouped the way you consider more relevant. When grouping these enums one thing to consider is modularity: you can split some or all these enums in different frameworks if you want strict boundaries between your modules and/or reuse the same group of actions among different apps.

For example, all apps will have common actions that represent life-cycle of any iOS app, such as willResignActive, didBecomeActive, didEnterBackground, willEnterForeground. If multiples apps need to know this life-cycle, maybe it's convenient to create an enum for this specific domain. The same for network reachability, we should consider creating an enum to represent all possible events we get from the system when our connection state changes, and this can be used in a wide variety of apps.

IMPORTANT: Because enums in Swift don't have KeyPath as structs do, we strongly recommend reading Action Enum Properties document and implementing properties for each case, either manually or using code generators, so later you avoid writing lots and lots of error-prone switch/case. We also offer some templates to help you on that.


State

There's no "State" protocol or type in SwiftRex. However, State will be found as a generic parameter for most core data structures, meaning that it's up to you to define what is the root State type.

Conceptually, we can say that state represents the whole knowledge that an app holds while is open, usually in memory and mutable; it's like a paper on where you write down some values, and for every action you receive you erase one value and replace it by a different value. Another way of thinking about state is in a functional programming way: the state is not persisted, but it's the result of a function that takes the initial condition of your app plus all the actions it received since it was launched, and calculates the current values by applying chronologically all the action changes on top of the initial state. This is known as Event Sourcing Design Pattern and it's becoming popular recently in some web backend services, such as Kafka Event Sourcing.

In a device with limited battery and memory we can't afford having a true event-sourcing pattern because it would be too expensive recreating the whole history of an app every time someone requests a simple boolean. So we "cache" the new state every time an action is received, and this in-memory cache is precisely what we call "State" in SwiftRex. So maybe we mix both ways of thinking about State and come up with a better generalisation for what a state is.

STATE is the result of a function that takes two arguments: the previous (or initial) state and some action that occurred, to determine the new state. This happens incrementally as more and more actions arrive. State is useful for output data to the user.

However, be careful, some things may look like state but they are not. Let's assume you have an app that shows an item price to the user. This price will be shown as "$3.00" in US, or "$3,00" in Germany, or maybe this product can be listed in british pounds, so in US we should show "£3.00" while in Germany it would be "£3,00". In this example we have:

  • Currency type (£ or $)
  • Numeric value (3)
  • Locale (en_US or de_DE)
  • Formatted string ("$3.00", "$3,00", "£3.00" or "£3,00")

The formatted string itself is NOT state, because it can be calculated from the other properties. This can be called "derived state" and holding that is asking for inconsistency. We would have to remember to update this value every time one of the others change. So it's better off to represent this String either as a calculated property or a function of the other 3 values. The best place for this sort of derived state is in presenters or controllers, unless you have a high cost to recalculate it and in this case you could store in the state and be very careful about it. Luckily SwiftRex helps to keep the state consistent as we're about to see in the Reducer section, still, it's better off not duplicating information that can be easily and cheaply calculated.

For representing the state of an app we recommend value types: structs or enums. Tuples would be acceptable as well, but unfortunately Swift currently doesn't allow us to conform tuples to protocols, and we usually want our whole state to be Equatable and sometimes Codable.

struct AppState: Equatable {
    var appLifecycle: AppLifecycle
    var movies: Loadable<[Movie]> = .neverLoaded
    var currentFilter: MovieFilter
    var selectedMovie: UUID?
}

struct MovieFilter: Equatable {
    var stringFilter: String?
    var yearMin: Int?
    var yearMax: Int?
    var ratingMin: Int?
    var ratingMax: Int?
}

enum AppLifecycle: Equatable {
    case backgroundActive
    case backgroundInactive
    case foregroundActive
    case foregroundInactive
}

enum Loadable<T> {
    case neverLoaded
    case loading
    case loaded(T)
}
extension Loadable: Equatable where T: Equatable {}

Some properties represent a state-machine, for example the Loadable enum will eventually change from .neverLoaded to .loading and then to .loaded([Movie]) in our movies property. Learning when and how to represent properties in this shape is a matter of experimenting more and more with SwiftRex and getting used to this architecture. Eventually this will become natural and you can start writing your own data structures to represent such state-machines, that will be very useful in countless situations.

Annotating the whole state as Equatable helps us to reduce the UI updates in case view models are not used, but this is not a strong requirement and there are other ways to also avoid that, although we still recommend it. Annotating the state as Codable can be useful for logging, debugging, crash reports, monitors, etc and this is also recommended if possible.


Core Parts


Store

Store is a class that you want to create and keep alive during the whole execution of an app, because its only responsibility is to act as a coordinator for the Unidirectional Dataflow lifecycle. That's also why we want one and only one instance of a Store, so either you create a static instance singleton, or keep it in your AppDelegate. Be careful with SceneDelegate if your app supports multiple windows and you want to share the state between these multiple instances of your app, which you usually want. That's why AppDelegate, singleton or global variable is usually recommended for the Store, not SceneDelegate.

SwiftRex will provide a protocol and a base type for helping you to create your own Store. Let's learn about them.

StoreType

StoreType is the protocol that defines the minimum implementation requirement of a Store, and it's actually composed only by two other protocols, one for the store input and one for the store output.

1. ActionHandler

ActionHandler: that's the store input, so it makes it able to receive and distribute events of generic type ActionType. Being an action handler means that an UIViewController or SwiftUI View can dispatch events to it, such as .userTappedButtonX, .didScrollToPosition(_:), .viewDidLoad or queryTextFieldChangedTo(_:). There's only one requirement:

func dispatch(_ action: ActionType, from dispatcher: ActionSource)

It gives for free another dispatch function:

func dispatch(_ action: ActionType, file: String = #file, function: String = #function, line: UInt = #line, info: String? = nil)

Most of the times, users will only provide the first parameter, action: ActionType, and the rest will be collected automatically from where the action was dispatched from. This is useful for log and analytics purposes, we can track the source of each action.

// Usage:
store.dispatch(.appStarted) // this collects automatically the dispatcher

Because ActionType is generic, when you dispatch you don't have to provide the full enum name, only the case. This also avoids mistakes as it's compiled-checked.

2. StateProvider

StateProvider: that's the store output, so the system can subscribe a store for updates on State. Being a state provider basically means that store is an Observable (RxSwift) or a Publisher (Combine) of state elements, and an UIViewController or SwiftUI View can subscribe to the store and react to state changes. There's only one requirement:

var statePublisher: UnfailablePublisherType<StateType> { get }

The UnfailablePublisherType<StateType> is an abstraction that will be implemented as Observable, Publisher or SignalProducer according to the selected Reactive Framework, and emits the element StateType (your root app state) with Never type for failure, when the framework supports it.

// Combine Usage:
let cancellable = store.statePublisher.sink { value in
    print("Got new state: \(value)") 
}
// RxSwift Usage:
let disposable = store.statePublisher.subscribe(onNext: { value in
    print("Got new state: \(value)") 
})

There are other ways to observe a Store, such as SwiftUI ObservableObject protocol, but these tools are built on top of this very simple StateProvider protocol.

StoreType Flow

Either using UIKit, AppKit, WatchKit, SwiftUI or any other presentation layer, the communication between a Store and the UI will happen exclusively through these two members, dispatch for input actions and statePublisher for output state.

ViewController and Store

As seen in the animation above, the Store only exposes an input (action) and an output (state provider), and that's all the Views need to know about the Store.

IMPORTANT: However you should only have one store, you can have multiple projections of this store and give to your views, for example, that way they will only have a limited amount of operations and state available and can't mess with things that are out of their responsibilities. This is also the perfect way to modularize an app, as multiples projections can live in different frameworks and later only assembled together in the main target. We will talk more about that on chapter Store Projection, but for now it's only important to understand that Store Projections won't hold source-of-truth, but will pretend to be a real store, therefore, implement the StoreType protocol, and the flow above will still be valid regardless of being the real singleton Store or a simple projection of it.

ReduxStoreBase

ReduxStoreBase is an open class that conforms to StoreType and provides all we need to start using SwiftRex. You can choose to inherit from this class or use it directly. We recommend inheritance because this will allow you to better mock the Store if necessary, however there's nothing you really have to write once ReduxStoreBase is complete: it glues all the parts together and acts as a proxy to the non-Redux world.

A suggested Store can be written with no more than 10 lines of code:

class Store: ReduxStoreBase<AppAction, AppState> {
    init(world: World) {
        super.init(
            subject: .combine(initialValue: AppState()),
            reducer: appReducer,
            middleware: appMiddleware().inject(world),
            emitsValue: .whenDifferent
        )
    }
}

The ReduxStoreBase initialiser expects a middleware and a reducer as input, and that's enough for the store to coordinate the entire process. It creates a queue of incoming actions that will be handled by the middleware pipeline and by the reducer pipeline. By the end of this process the state may or may not change, as a result of reducer pipeline acting on action + current state. Finally, the store notifies all subscribers about the state change and only then starts evaluating the next action on the queue.

Store internals

We will see more in depth this dataflow when reading about middlewares and reducers, but please come back to this picture above every time you read about the store internals, it can be very useful.

At this point all you have to notice is the action handler (dispatch action function) and the state provider (subscribe state) boxes that are shown to the outside world. When writing UIViewControllers or SwiftUI Views those are the only 2 functions you'll ever have to use.

There will be only one honest Store in your entire app, so either you create it as a singleton or a property in a long-living class such as AppDelegate or AppCoordinator. That's crucial for making the store completely detached from the UIKit/SwiftUI world.

                 ┌────────────────────────────────────────┐
                 │                                        │
                 │    SwiftUI View / UIViewController     │
                 │                                        │
                 └────┬───────────────────────────────────┘
                      │                            ▲
                      │                            │
                      │ action        notification
          ┌─────────┐ │                            │
          │         ▼ │                       ─ ─ ─ ─ ─ ─
          │      ┏━━━━│━━━━━━━━━━━━━━━━━━━━━━┫   State   ┣┓
  new actions    ┃    │            Store       Publisher  ┃░
from middleware  ┃    ▼                      └ ─ ─ ┬ ─ ─ ┘┃░
          │      ┃ ┌───────────────────┐                  ┃░
          │      ┃ │    Middlewares    │           │      ┃░
          └────────┤┌───┐  ┌───┐  ┌───┐│                  ┃░
                 ┃ ││ 1 │─▶│ 2 │─▶│ 3 ││◀─         │      ┃░
                 ┃ │└───┘  └───┘  └───┘│  │               ┃░
                 ┃ └────────────────┬──┘      ┌────┴────┐ ┃░
                 ┃                  │     │   │         │ ┃░
                 ┃    ┌─────────────┘      ─ ─│  State  │ ┃░
                 ┃    │ ┌─────────────────────│         │ ┃░
                 ┃    ▼ ▼                     └────▲────┘ ┃░
                 ┃ ┌───────────────────┐           ║      ┃░
                 ┃ │     Reducers      │           ║      ┃░
                 ┃ │┌───┐  ┌───┐  ┌───┐│           ║      ┃░
                 ┃ ││ 1 │─▶│ 2 │─▶│ 3 │╠═══════════╝      ┃░
                 ┃ │└───┘  └───┘  └───┘│    state         ┃░
                 ┃ └───────────────────┘   mutation       ┃░
                 ┃                                        ┃░
                 ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛░
                  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

Middleware

Middleware is a plugin, or a composition of several plugins, that are assigned to the ReduxStoreProtocol pipeline in order to handle each action received (InputActionType), to execute side-effects in response, and eventually dispatch more actions (OutputActionType) in the process. This happens before the Reducer to do its job.

We can think of a Middleware as an object that transforms actions into sync or async tasks and create more actions as these side-effects complete, also being able to check the current state at any point.

An action is a lightweight structure, typically an enum, that is dispatched into the ActionHandler (usually a StoreProtocol). A Store like ReduxStoreProtocol enqueues a new action that arrives and submits it to a pipeline of middlewares. So, in other words, a Middleware is class that handles actions, and has the power to dispatch more actions to the ActionHandler chain. The Middleware can also simply ignore the action, or it can execute side-effects in response, such as logging into file or over the network, or execute http requests, for example. In case of those async tasks, when they complete the middleware can dispatch new actions containing a payload with the response (a JSON file, an array of movies, credentials, etc). Other middlewares will handle that, or maybe even the same middleware in a future RunLoop, or perhaps some Reducer, as reducers pipeline is at the end of every middleware pipeline.

Middlewares can schedule a callback to be executed after the reducer pipeline is done mutating the global state. At that point, the middleware will have access to the new state, and in case it cached the old state it can compare them, log, audit, perform analytics tracking, telemetry or state sync with external devices, such as Apple Watches. Remote Debugging over the network is also a great use of a Middleware.

Every action dispatched also comes with its action source, which is the primary dispatcher of that action. Middlewares can access the file, line, function and additional information about the entity responsible for creating and dispatching that action, which is a very powerful debugging information that can help developers to trace how the information flows through the app.

Because the Middleware receive all actions and accesses the state of the app at any point, anything can be done from these small and reusable boxes. For example, the same CoreLocation middleware could be used from an iOS app, its extensions, the Apple Watch extension or even different apps, as long as they share some sub-state struct.

Some suggestions of middlewares:

  • Run Timers, pooling some external resource or updating some local state at a constant time
  • Subscribe for CoreData, Realm, Firebase Realtime Database or equivalent database changes
  • Be a CoreLocation delegate, checking for significant location changes or beacon ranges and triggering actions to update the state
  • Be a HealthKit delegate to track activities, or even combining that with CoreLocation observation in order to track the activity route
  • Logger, Telemetry, Auditing, Analytics tracker, Crash report breadcrumbs
  • Monitoring or debugging tools, like external apps to monitor the state and actions remotely from a different device
  • WatchConnectivity sync, keep iOS and watchOS state in sync
  • API calls and other "cold observables"
  • Network Reachability
  • Navigation through the app (Redux Coordinator pattern)
  • CoreBluetooth central or peripheral manager
  • CoreNFC manager and delegate
  • NotificationCenter and other delegates
  • WebSocket, TCP Socket, Multipeer and many other connectivity protocols
  • RxSwift observables, ReactiveSwift signal producers, Combine publishers
  • Observation of traits changes, device rotation, language/locale, dark mode, dynamic fonts, background/foreground state
  • Any side-effect, I/O, networking, sensors, third-party libraries that you want to abstract
                   ┌─────┐                                                                                        ┌─────┐
                   │     │     handle   ┌──────────┐ request      ┌ ─ ─ ─ ─  response     ┌──────────┐ dispatch   │     │
                   │     │   ┌─────────▶│Middleware├─────────────▶ External│─────────────▶│Middleware│───────────▶│Store│─ ─ ▶ ...
                   │     │   │ Action   │ Pipeline │ side-effects │ World    side-effects │ callback │ New Action │     │
                   │     │   │          └──────────┘               ─ ─ ─ ─ ┘              └──────────┘            └─────┘
 ┌──────┐ dispatch │     │   │                ▲
 │Button│─────────▶│Store│──▶│                └───afterReducer─────┐                   ┌────────┐
 └──────┘ Action   │     │   │                                     │                ┌─▶│ View 1 │
                   │     │   │                                  ┌─────┐             │  └────────┘
                   │     │   │ reduce   ┌──────────┐            │     │ onNext      │  ┌────────┐
                   │     │   └─────────▶│ Reducer  ├───────────▶│Store│────────────▶├─▶│ View 2 │
                   │     │     Action   │ Pipeline │ New state  │     │ New state   │  └────────┘
                   └─────┘     +        └──────────┘            └─────┘             │  ┌────────┐
                               State                                                └─▶│ View 3 │
                                                                                       └────────┘

Middleware protocol is generic over 3 associated types:

InputActionType:

The Action type that this Middleware knows how to handle, so the store will forward actions of this type to this middleware. Thanks to optics, this action can be a sub-action lifted to a global action type in order to compose with other middlewares acting on the global action of an app. Please check lift(inputAction:outputAction:state:) for more details.

OutputActionType:

The Action type that this Middleware will eventually trigger back to the store in response of side-effects. This can be the same as InputActionType or different, in case you want to separate your enum in requests and responses. Thanks to optics, this action can be a sub-action lifted to a global action type in order to compose with other middlewares acting on the global action of an app. Please check lift(inputAction:outputAction:state:) for more details.

StateType:

The State part that this Middleware needs to read in order to make decisions. This middleware will be able to read the most up-to-date StateType from the store at any point in time, but it can never write or make changes to it. In some cases, middleware don't need reading the whole global state, so we can decide to allow only a sub-state, or maybe this middleware doesn't need to read any state, so the StateTypecan safely be set to Void. Thanks to lenses, this state can be a sub-state lifted to a global state in order to compose with other middlewares acting on the global state of an app. Please check lift(inputAction:outputAction:state:) for more details.

When implementing your Middleware, all you have to do is to handle the incoming actions:

class LoggerMiddleware: Middleware {
    typealias InputActionType = AppGlobalAction // It wants to receive all possible app actions
    typealias OutputActionType = Never          // No action is generated from this Middleware
    typealias StateType = AppGlobalState        // It wants to read the whole app state

    var getState: GetState<AppGlobalState>!

    func receiveContext(getState: @escaping GetState<AppGlobalState>, output: AnyActionHandler<Never>) {
        self.getState = getState
    }

    func handle(action: AppGlobalAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
        let stateBefore: AppGlobalState = getState()
        let dateBefore = Date()

        afterReducer = .do {
            let stateAfter = self.getState()
            let dateAfter = Date()
            let source = "\(dispatcher.file):\(dispatcher.line) - \(dispatcher.function) | \(dispatcher.info ?? "")"

            Logger.log(action: action, from: source, before: stateBefore, after: stateAfter, dateBefore: dateBefore, dateAfter: dateAfter)
        }
    }
}

class FavoritesAPIMiddleware: Middleware {
    typealias InputActionType = FavoritesAction  // It wants to receive only actions related to Favorites
    typealias OutputActionType = FavoritesAction // It wants to also dispatch actions related to Favorites
    typealias StateType = FavoritesModel         // It wants to read the app state that manages favorites

    var getState: GetState<FavoritesModel>!
    var output: AnyActionHandler<FavoritesAction>!

    func receiveContext(getState: @escaping GetState<FavoritesModel>, output: AnyActionHandler<FavoritesAction>) {
        self.getState = getState
        self.output = output
    }

    func handle(action: FavoritesAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
        guard let .toggleFavorite(movieId) = action else { return }

        let favoritesList = getState()
        let makeFavorite = !favoritesList.contains(where: { $0.id == movieId })

        API.changeFavorite(id: movieId, makeFavorite: makeFavorite) (completion: { result in
            switch result {
            case let .success(value):
                self.output.dispatch(.changedFavorite(movieId, isFavorite: true), info: "API.changeFavorite callback")
            case let .failure(error):
                self.output.dispatch(.changedFavoriteHasFailed(movieId, isFavorite: false, error: error), info: "API.changeFavorite callback")
            }
        })
    }
}

SwiftUI Side-Effects

Reducer

Reducer is a pure function wrapped in a monoid container, that takes current state and an action to calculate the new state.

The Middleware pipeline can trigger ActionProtocol, and handles both EventProtocol and ActionProtocol. But what they can NOT do is changing the app state. Middlewares have read-only access to the up-to-date state of our apps, but when mutations are required we use the Reducer function. Actually, it's a protocol that requires only one method:

func reduce(_ currentState: StateType, action: Action) -> StateType

Given the current state and an action, returns the calculated state. This function will be executed in the last stage of an action handling, when all middlewares had the chance to modify or improve the action. Because a reduce function is composable monoid and also can be lifted through lenses, it's possible to write fine-grained "sub-reducer" that will handle only a "sub-state", creating a pipeline of reducers.

It's important to understand that reducer is a synchronous operations that calculates a new state without any kind of side-effect, so never add properties to the Reducer structs or call any external function. If you are tempted to do that, please create a middleware. Reducers are also responsible for keeping the consistency of a state, so it's always good to do a final sanity check before changing the state.

Once the reducer function executes, the store will update its single source of truth with the new calculated state, and propagate it to all its observers.

                  ┌─────┐                                                                                        ┌─────┐
                  │     │     handle   ┌──────────┐ request      ┌ ─ ─ ─ ─  response     ┌──────────┐ dispatch   │     │
                  │     │   ┌─────────▶│Middleware├─────────────▶ External│─────────────▶│Middleware│───────────▶│Store│─ ─ ▶ ...
                  │     │   │ Action   │ Pipeline │ side-effects │ World    side-effects │ callback │ New Action │     │
                  │     │   │          └──────────┘               ─ ─ ─ ─ ┘              └──────────┘            └─────┘
┌──────┐ dispatch │     │   │
│Button│─────────▶│Store│──▶│                                                         ┌────────┐
└──────┘ Action   │     │   │                                                      ┌─▶│ View 1 │
                  │     │   │                                  ┌─────┐             │  └────────┘
                  │     │   │ reduce   ┌──────────┐            │     │ onNext      │  ┌────────┐
                  │     │   └─────────▶│ Reducer  ├───────────▶│Store│────────────▶├─▶│ View 2 │
                  │     │     Action   │ Pipeline │ New state  │     │ New state   │  └────────┘
                  └─────┘     +        └──────────┘            └─────┘             │  ┌────────┐
                              State                                                └─▶│ View 3 │
                                                                                      └────────┘

Projection and Lifting

Store Projection

TBD

Store Projection

Lifting Middleware

TBD

Lifting Reducer

TBD

Architecture

This dataflow is, somehow, an implementation of MVC, one that differs significantly from the Apple's MVC for offering a very strict and opinative description of layers' responsibilities and by enforcing the growth of the Model layer, through a better definition of how it should be implemented: in this scenario, the Model is the Store. All your Controller has to do is to forward view actions to the Store and subscribe to state changes, updating the views whenever needed. If this flow doesn't sound like MVC, let's check a picture taken from Apple's website:

iOS MVC

One important distinction is about the user action: on SwiftRex it's forwarded by the controller and reaches the Store, so the responsibility of updating the state becomes the Store's responsibility now. The rest is pretty much the same, but with a better definition of how the Model operates.

     ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾
    ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲
  ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲
 ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃
╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃
│┃░┃             ┌──────────┐             ┃░┃
╰┃░┃             │ UIButton │────────┐    ┃░┃
 ┃░┃             └──────────┘        │    ┃░┃
╭┃░┃         ┌───────────────────┐   │    ┃░┃╮ dispatch<Action>(_ action: Action)
│┃░┃         │UIGestureRecognizer│───┼──────────────────────────────────────────────┐
│┃░┃         └───────────────────┘   │    ┃░┃│                                      │
╰┃░┃             ┌───────────┐       │    ┃░┃│                                      ▼
╭┃░┃             │viewDidLoad│───────┘    ┃░┃╯                           ┏━━━━━━━━━━━━━━━━━━━━┓
│┃░┃             └───────────┘            ┃░┃                            ┃                    ┃░
│┃░┃                                      ┃░┃                            ┃                    ┃░
╰┃░┃                                      ┃░┃                            ┃                    ┃░
 ┃░┃               ┌───────┐              ┃░┃                            ┃                    ┃░
 ┃░┃               │UILabel│◀─ ─ ─ ─ ┐    ┃░┃                            ┃                    ┃░
 ┃░┃               └───────┘              ┃░┃  Combine, RxSwift    ┌ ─ ─ ┻ ─ ┐                ┃░
 ┃░┃                                 │    ┃░┃  or ReactiveSwift       State      Store        ┃░
 ┃░┃        ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│Publisher│                ┃░
 ┃░┃        ▼               │             ┃░┃  subscribe(onNext:)                             ┃░
 ┃░┃ ┌─────────────┐        ▼             ┃░┃  sink(receiveValue:) └ ─ ─ ┳ ─ ┘                ┃░
 ┃░┃ │  Diffable   │ ┌─────────────┐      ┃░┃  assign(to:on:)            ┃                    ┃░
 ┃░┃ │ DataSource  │ │RxDataSources│      ┃░┃                            ┃                    ┃░
 ┃░┃ └─────────────┘ └─────────────┘      ┃░┃                            ┃                    ┃░
 ┃░┃        │               │             ┃░┃                            ┃                    ┃░
 ┃░┃ ┌──────▼───────────────▼───────────┐ ┃░┃                            ┗━━━━━━━━━━━━━━━━━━━━┛░
 ┃░┃ │                                  │ ┃░┃                             ░░░░░░░░░░░░░░░░░░░░░░
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │         UICollectionView         │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ │                                  │ ┃░┃
 ┃░┃ └──────────────────────────────────┘ ┃░┃
 ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃
 ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃
  ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱
    ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱
     ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾

You can think of Store as a very heavy "Model" layer, completely detached from the View and Controller, and where all the business logic stands. At a first sight it may look like transferring the "Massive" problem from a layer to another, so that's why the Store is nothing but a collection of composable boxes with very well defined roles and, most importantly, restrictions.

     ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾
    ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲
  ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲
 ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃
╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃
│┃░┃               ┌────────┐             ┃░┃
╰┃░┃               │ Button │────────┐    ┃░┃
 ┃░┃               └────────┘        │    ┃░┃              ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐             ┏━━━━━━━━━━━━━━━━━━━━━━━┓
╭┃░┃          ┌──────────────────┐   │    ┃░┃╮ dispatch                                            ┃                       ┃░
│┃░┃          │      Toggle      │───┼────────────────────▶│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶  │────────────▶┃                       ┃░
│┃░┃          └──────────────────┘   │    ┃░┃│ view event      f: (Event) → Action     app action  ┃                       ┃░
╰┃░┃              ┌──────────┐       │    ┃░┃│             │                         │             ┃                       ┃░
╭┃░┃              │ onAppear │───────┘    ┃░┃╯                                                     ┃                       ┃░
│┃░┃              └──────────┘            ┃░┃              │   ObservableViewModel   │             ┃                       ┃░
│┃░┃                                      ┃░┃                                                      ┃                       ┃░
╰┃░┃                                      ┃░┃              │     a projection of     │  projection ┃         Store         ┃░
 ┃░┃                                      ┃░┃                   the actual store                   ┃                       ┃░
 ┃░┃                                      ┃░┃              │                         │             ┃                       ┃░
 ┃░┃      ┌────────────────────────┐      ┃░┃                                                      ┃                       ┃░
 ┃░┃      │                        │      ┃░┃              │                         │            ┌┃─ ─ ─ ─ ─ ┐            ┃░
 ┃░┃      │    @ObservedObject     │◀ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─    ◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ◀─ ─ ─ ─ ─ ─    State                ┃░
 ┃░┃      │                        │      ┃░┃  view state  │   f: (State) → View     │  app state │ Publisher │            ┃░
 ┃░┃      └────────────────────────┘      ┃░┃                               State                  ┳ ─ ─ ─ ─ ─             ┃░
 ┃░┃        │          │          │       ┃░┃              └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘             ┗━━━━━━━━━━━━━━━━━━━━━━━┛░
 ┃░┃        ▼          ▼          ▼       ┃░┃                                                       ░░░░░░░░░░░░░░░░░░░░░░░░░
 ┃░┃   ┌────────┐ ┌────────┐ ┌────────┐   ┃░┃
 ┃░┃   │  Text  │ │  List  │ │ForEach │   ┃░┃
 ┃░┃   └────────┘ └────────┘ └────────┘   ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░┃                                      ┃░┃
 ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃
 ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃
 ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃
  ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱
    ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱
     ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾

And what about SwiftUI? Is this architecture a good fit for the new UI framework? In fact, this architecture works even better in SwiftUI, because SwiftUI was inspired by several functional patterns and it's reactive and stateless by conception. It was said multiple times during WWDC 2019 that, in SwiftUI, the View is a function of the state, and that we should always aim for single source of truth and the data should always flow in a single direction.

SwiftUI Unidirectional Flow

Installation

CocoaPods

Create or modify the Podfile at the root folder of your project. Your settings will depend on the ReactiveFramework of your choice.

For Combine:

# Podfile
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!

target 'MyAppTarget' do
  pod 'CombineRex'
end

For RxSwift:

# Podfile
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!

target 'MyAppTarget' do
  pod 'RxSwiftRex'
end

For ReactiveSwift:

# Podfile
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!

target 'MyAppTarget' do
  pod 'ReactiveSwiftRex'
end

As seen above, some lines are optional because the final Podspecs already include the correct dependencies.

Then, all you must do is install your pods and open the .xcworkspace instead of the .xcodeproj file:

$ pod install
$ xed .

Swift Package Manager

Create or modify the Package.swift at the root folder of your project. You can use the automatic linking mode (static/dynamic), or use the project with suffix Dynamic to force dynamic linking and overcome current Xcode limitations to resolve diamond dependency issues.

If you use it from only one target, automatic mode should be fine.

Combine, automatic linking mode:

// swift-tools-version:5.3

import PackageDescription

let package = Package(
  name: "MyApp",
  platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
  products: [
    .executable(name: "MyApp", targets: ["MyApp"])
  ],
  dependencies: [
    .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.7.4")
  ],
  targets: [
    .target(name: "MyApp", dependencies: [.product(name: "CombineRex", package: "SwiftRex")])
  ]
)

RxSwift, automatic linking mode:

// swift-tools-version:5.3

import PackageDescription

let package = Package(
  name: "MyApp",
  platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)],
  products: [
    .executable(name: "MyApp", targets: ["MyApp"])
  ],
  dependencies: [
    .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.7.4")
  ],
  targets: [
    .target(name: "MyApp", dependencies: [.product(name: "RxSwiftRex", package: "SwiftRex")])
  ]
)

ReactiveSwift, automatic linking mode:

// swift-tools-version:5.3

import PackageDescription

let package = Package(
  name: "MyApp",
  platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)],
  products: [
    .executable(name: "MyApp", targets: ["MyApp"])
  ],
  dependencies: [
    .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.7.4")
  ],
  targets: [
    .target(name: "MyApp", dependencies: [.product(name: "ReactiveSwiftRex", package: "SwiftRex")])
  ]
)

Combine, dynamic linking mode (use similar approach of appending "Dynamic" also for RxSwift or ReactiveSwift products):

// swift-tools-version:5.3

import PackageDescription

let package = Package(
  name: "MyApp",
  platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
  products: [
    .executable(name: "MyApp", targets: ["MyApp"])
  ],
  dependencies: [
    .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.7.4")
  ],
  targets: [
    .target(name: "MyApp", dependencies: [.product(name: "CombineRexDynamic", package: "SwiftRex")])
  ]
)

Then you can either building on the terminal or use Xcode 11 or higher that now supports SPM natively.

$ swift build
$ xed .

Carthage

Carthage is no longer supported due to lack of interest and high maintenance effort.

In case this is REALLY critical for you, please open a Github issue and let us know, we will evaluate the possibility to bring it back. In meantime you can check last Carthage compatible version, which was 0.7.1, and eventually target that version until we come up with a better solution.

Github

link
Stars: 219

Used By

Total: 0

Releases

v0.7.8: Fix SPM manifest setting min target to iOS 9 (by @npvisual) -

Fix SPM warning about min target version

v0.7.7: Podspec target to iOS 9.0 (fix warning reported by @npvisual) -

v0.7.6: Fix critical retain cycle on ObservableViewModel found by @scornflake -

This release fixes a critical retain cycle that could make the ObservableViewModels to not be freed and it's recommended by anyone using CombineRex in a SwiftUI app.

v0.7.5: @haifengkao fix for Middleware leak -

Middleware is usually alive while the app is alive, so in the case of this memory leak it was not necessarily a huge problem for most cases, but this can be a huge problem for unit tests, so this fix is important and this hotfix should be applied in case you use ReduxStoreBase with a middleware in your unit tests.

v0.7.4 -

ObservableViewModel to not be final class anymore, so custom view model code can be added to it, such as calculated properties, publishers or temporary view state for gestures such as DragGesture, that might wanted to be throttled before sent to the Store in some cases.

Making it open to allow that.

v0.7.3 -

Swift 5.2 was already set in the project, now it's also in CocoaPods manifest to fix inconsistency.

v0.7.2 -

ObservableViewModel to not be final class anymore, so custom view model code can be added to it, such as calculated properties, publishers or temporary view state for gestures such as DragGesture, that might wanted to be throttled before sent to the Store in some cases.

v0.7.1 -

  • SPM Product CombineRexDynamic, forcing dynamic linking. This is meant to solve Xcode attempt to always use static linking and creating diamond dependency problem.

v0.7.0 -

SwiftRex 0.7.0

Lots of middleware improvements!

  • Context injection now happens through the function func receiveContext(getState: @escaping GetState<StateType>, output: AnyActionHandler<OutputActionType>), so you can ignore context in case your middleware doesn't need it.
  • Action source: from the middleware you can now check who's the responsible for dispatching that action (file, line, function and additional info), which can be very useful for logging and analytics. The dispatcher information is filled automatically thanks to #file, #function and #line macros, but you can customize if you want
  • Instead of calling next() from the middleware action handling, you're given an "afterReducer" in/out parameter that you can OPTIONALLY set to a closure that will be executed once the reducer is done with mutating the state. This makes it much easier to implement the function handle and avoids mistakes.
  • CombineRex improvements for the obsevable view model
  • StoreProjection is now only a regular AnyStoreType type-erased Store
  • Better docs

v0.6.0 -

SwiftRex 0.6.0

Lifting stores, middlewares and reducers to make your architecture more modular Improved docs (but still not finished)

Beta version. Please don't use it in production code yet.

v0.5.0 -

SwiftRex 0.5.0

  • Choose between RxSwift 5 or ReactiveSwift 6
  • Swift 5 only

Beta version. Please don't use it in production code yet.

v0.4.0 -

  • Choose between RxSwift or ReactiveSwift for state observation and side-effects middlewares
  • Swift 5!

v0.3.1 -

Alpha version. Please don't use it in production code yet.

v0.3.0 -

Alpha version. Please don't use it in production code yet.

v0.2.3 -

Alpha version. Please don't use it in production code yet.

v0.2.2 -

Alpha version. Please don't use it in production code yet.

v0.2.1 -

v0.2.0 -

Second alpha version. Please don't use it in production code yet.

v0.1.0 -

First alpha version. Please don't use it in production code yet.

v0.0.0 -