Protocols
The following protocols are available globally.
-
See moreMiddlewareProtocol
is a plugin, or a composition of several plugins, that are assigned to the app globalStoreType
pipeline in order to handle each action received (InputActionType
), to execute side-effects in response, and eventually dispatch more actions (OutputActionType
) in the process. It can also access the most up-to-dateStateType
while handling an incoming action.Declaration
Swift
public protocol MiddlewareProtocol
-
ActionHandler
defines a protocol for entities able to handle actions - defined by the associated typeActionType
.The only protocol requirement is a function that allows other entities to dispatch actions, so Views (or Presenters, ViewModels) in your UI layer, or even Middlewares can create actions of a certain type and send to your store, that is generalized by this protocol.
See moreDeclaration
Swift
public protocol ActionHandler
-
StateProvider
defines a protocol for entities able to offer state publishers (Combine Publisher, RxSwift Observable, ReactiveSwift SignalProducer) of certainStateType
, so everybody can observe the global state changes through this container. Usually aStore
will implement that, but it can also be aStoreProjection
with a state that is derived from the global source-of-truth.The only protocol requirement is to offer a property
See morestatePublisher
that will allow other entities to subscribe to state changes and react to those.Declaration
Swift
public protocol StateProvider
-
A protocol that defines the two expected roles of a “Store”: receive/distribute actions (
See moreActionHandler
); and publish changes of the the current app state (StateProvider
) to possible subscribers. It can be a real store (such asReduxStoreBase
) or just a “proxy” that acts on behalf of a real store, for example, in the case ofStoreProjection
.Declaration
Swift
public protocol StoreType : ActionHandler, StateProvider
-
A protocol to generalize MiddlewareReader. Unless you look for some very special behaviour, you should use MiddlewareReader directly which provides everything needed for your Middleware dependency injection.
See moreDeclaration
Swift
public protocol MiddlewareReaderProtocol
-
⛓
Middleware
is a plugin, or a composition of several plugins, that are assigned to theReduxStoreProtocol
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 theReducer
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 aStoreProtocol
). A Store likeReduxStoreProtocol
enqueues a new action that arrives and submits it to a pipeline of middlewares. So, in other words, aMiddleware
is class that handles actions, and has the power to dispatch more actions to theActionHandler
chain. TheMiddleware
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 someReducer
, 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 sameCoreLocation
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 withCoreLocation
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 managerCoreNFC
manager and delegateNotificationCenter
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 checklift(inputActionMap:outputActionMap:stateMap:)
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 asInputActionType
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 checklift(inputActionMap:outputActionMap:stateMap:)
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-dateStateType
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 theStateType
can safely be set toVoid
. 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 checklift(inputActionMap:outputActionMap:stateMap:)
for more details.When implementing your Middleware, all you have to do is to handle the incoming actions:
When implementing your Middleware, all you have to do is to handle the incoming actions:
See moreclass 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") } }) } }
Declaration
Swift
@available(*, deprecated, message: "Use `MiddlewareProtocol` instead of `Middleware`. This protocol will be removed on 1.0.") public protocol Middleware : MiddlewareProtocol
-
Protocol for a semigroup, any algebraic structure that allows two of its elements to be combined into one,
(A, A) -> A
, for any of its elements and keeping associativity property for all the cases, for example:(a1 <> a2) <> a3 = a1 <> (a2 <> a3)
for anya
s inA
.Axioms:
- Totality
- Associativity
For example, having a
See moref(x) -> x
and ag(x) -> x
, one would be able to composeh = f <> g
in a way that the new functionh(x)
will be similar tog(f(x))
Declaration
Swift
public protocol Semigroup
-
Protocol for a monoid algebra, allowing monoidal composition. It’s a
Semigroup
with identity element, element which, when combined to any other element, will keep the other elemenet unchanged, regardless if the composition happened from the left or from the right, for example:a <> identity = identity <> a = a
, for anya
inA
.Axioms:
- Totality
- Associativity
- Identity
For example, having a
See moref(x) -> x
and ag(x) -> x
, one would be able to composeh = f <> g
in a way that the new functionh(x)
will be similar tog(f(x))
, and there should be a functioni(x)
wherei
, when composed to any other function, will not change the result:f <> i = i <> f = f
, forf
,g
,h
and all other endo-functions.Declaration
Swift
public protocol Monoid : Semigroup
-
Abstraction over subscription types from reactive frameworks. This abstraction uses concept similar to type-erasure or protocol witness pattern, wrapping the behaviour of concrete implementations and delegating to them once the wrapper funcions are called.
See moreDeclaration
Swift
public protocol SubscriptionType
-
Abstraction over subscription collection (
See moreDisposeBag
orSet<AnyCancellable
depending on your chosen reactive framework), useful for keeping subscriptions alive while the parent class is alive, binding the lifecycle of subscriptions to the lifecycle of views, view controllers or presenters. Subscriptions added to a subscription collection will be cancelled/disposed automatically once the collection gets deallocated, stopping any pending operation and cleaning up the resources. This abstraction uses concept similar to type-erasure or protocol witness pattern, wrapping the behaviour of concrete implementations and delegating to them once the wrapper funcions are called.Declaration
Swift
public protocol SubscriptionCollection