Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

SwiftRex Reducer component

License

NotificationsYou must be signed in to change notification settings

SwiftRex/Reducer

Repository files navigation

SwiftRex

Unidirectional Dataflow for your favourite reactive framework

Build StatuscodecovJazzy DocumentationCocoaPods compatibleSwift Package Manager compatiblePlatform supportLicense Apache 2.0

If you've got questions, about SwiftRex or redux and Functional Programming in general, pleaseJoin our Slack Channel.

SwiftRex

This is part of"SwiftRex library". Please read the library documentation to have full context about what Reducer is used for.

SwiftRex is a framework that combines Unidirectional Dataflow architecture and reactive programming (Combine,RxSwift orReactiveSwift), 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 singlepure 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.

Reducer

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

TheMiddlewareProtocol pipeline can do two things: dispatch outgoing actions and handling incoming actions. 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 theMutableReduceFunction function:

(ActionType, inout StateType)-> Void

Which has the same semantics (but better performance) than oldReduceFunction:

(ActionType, StateType)-> StateType

Given an action and the current state (as a mutable inout), it calculates the new state and changes it:

initial state is 42action: incrementreducer: increment 42 => new state 43current state is 43action: decrementreducer: decrement 43 => new state 42current state is 42action: halfreducer: half 42 => new state 21

The function is reducing all the actions in a cached state, and that happens incrementally for each new incoming action.

It's important to understand that reducer is a synchronous operations that calculates a new state without any kind of side-effect (including non-obvious ones as creatingDate(), using DispatchQueue orLocale.current), so never add properties to theReducer structs or call any external function. If you are tempted to do that, please create a middleware and dispatch actions with Dates or Locales from it.

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, like for example check other dependant properties that must be changed together.

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 subscribers, that will react to the new state and update Views, for example.

This function is wrapped in a struct to overcome some Swift limitations, for example, allowing us to compose multiple reducers into one (monoid operation, where two or more reducers become a single one) or lifting reducers from local types to global types.

The ability to lift reducers allow us to write fine-grained "sub-reducer" that will handle only a subset of the state and/or action, place it in different frameworks and modules, and later plugged into a bigger state and action handler by providing a way to map state and actions between the global and local ones. For more information about that, please checkLifting.

A possible implementation of a reducer would be:

letvolumeReducer=Reducer<VolumeAction,VolumeState>.reduce{ action, currentStateinswitch action{case.louder:        currentState=VolumeState(            isMute:false, // When increasing the volume, always unmute it.            volume:min(100, currentState.volume+5))case.quieter:        currentState=VolumeState(            isMute: currentState.isMute,            volume:max(0, currentState.volume-5))case.toggleMute:        currentState=VolumeState(            isMute: !currentState.isMute,            volume: currentState.volume)}}

Please notice from the example above the following good practices:

  • NoDispatchQueue, threading, operation queue, promises, reactive code in there.
  • All you need to implement this function is provided by the argumentsaction andcurrentState, don't use any other variable coming from global scope, not even for reading purposes. If you need something else, it should either be in the state or come in the action payload.
  • Do not start side-effects, requests, I/O, database calls.
  • Avoiddefault when writingswitch/case statements. That way the compiler will help you more.
  • Make the action and the state generic parameters as much specialised as you can. If volume state is part of a bigger state, you should not be tempted to pass the whole big state into this reducer. Make it short, brief and specialised, this also helps preventingdefault case or having to re-assign properties that are never mutated by this reducer.
                                                                                                                    ┌────────┐                                                                                            IO closure                                                ┌─▶│ View 1 │                                                           ┌─────┐                          (don't run yet)                       ┌─────┐             │  └────────┘                                                           │     │ handle  ┌──────────┐  ┌───────────────────────────────────────▶│     │ send        │  ┌────────┐                                                           │     ├────────▶│Middleware│──┘                                        │     │────────────▶├─▶│ View 2 │                                                           │     │ Action  │ Pipeline │──┐  ┌─────┐ reduce ┌──────────┐           │     │ New state   │  └────────┘                                                           │     │         └──────────┘  └─▶│     │───────▶│ Reducer  │──────────▶│     │             │  ┌────────┐                                         ┌──────┐ dispatch │     │                          │Store│ Action │ Pipeline │ New state │     │             └─▶│ View 3 │                                         │Button│─────────▶│Store│                          │     │ +      └──────────┘           │Store│                └────────┘                                         └──────┘ Action   │     │                          └─────┘ State                         │     │                                   dispatch    ┌─────┐                               │     │                                                                │     │       ┌─────────────────────────┐ New Action  │     │                               │     │                                                                │     │─run──▶│       IO closure        ├────────────▶│Store│─ ─ ▶ ...                      │     │                                                                │     │       │                         │             │     │                               │     │                                                                │     │       └─┬───────────────────────┘             └─────┘                               └─────┘                                                                └─────┘         │                     ▲                                                                                                                                     request│ side-effects        │side-effects                                                                                                                                ▼                      response                                                                                                                               ┌ ─ ─ ─ ─ ─                │                                                                                                                                         External │─ ─ async ─ ─ ─                                                                                                                                        │  World                                                                                                                                                            ─ ─ ─ ─ ─ ┘

Lifting

Lifting

An app can be a complex product, performing several activities that not necessarily are related. For example, the same app may need to perform a request to a weather API, check the current user location using CLLocation and read preferences from UserDefaults.

Although these activities are combined to create the full experience, they can be isolated from each other in order to avoid URLSession logic and CLLocation logic in the same place, competing for the same resources and potentially causing race conditions. Also, testing these parts in isolation is often easier and leads to more significant tests.

Ideally we should organise ourAppState andAppAction to account for these parts as isolated trees. In the example above, we could have 3 different properties in our AppState and 3 different enum cases in our AppAction to group state and actions related to the weather API, to the user location and to the UserDefaults access.

This gets even more helpful in case we split our app in 3 types ofReducer and 3 types ofMiddlewareProtocol, and each of them work not on the fullAppState andAppAction, but in the 3 paths we grouped in our model. The first pair ofReducer andMiddlewareProtocol would be generic overWeatherState andWeatherAction, the second pair overLocationState andLocationAction and the third pair overRepositoryState andRepositoryAction. They could even be in different frameworks, so the compiler will forbid us from coupling Weather API code with CLLocation code, which is great as this enforces better practices and unlocks code reusability. Maybe our CLLocation middleware/reducer can be useful in a completely different app that checks for public transport routes.

But at some point we want to put these 3 different types of entities together, and theStoreType of our app "speaks"AppAction andAppState, not the subsets used by the specialised handlers.

enumAppAction{case weather(WeatherAction)case location(LocationAction)case repository(RepositoryAction)}structAppState{letweather:WeatherStateletlocation:LocationStateletrepository:RepositoryState}

Given a reducer that is generic overWeatherAction andWeatherState, we can "lift" it to the global typesAppAction andAppState by telling this reducer how to find in the global tree the properties that it needs. That would be\AppAction.weather and\AppState.weather. The same can be done for the middleware, and for the other 2 reducers and middlewares of our app.

When all of them are lifted to a common type, they can be combined together usingReducer.compose(reducer1, reducer2, reducer3...) function, or the DSL form:

Reducer.compose{    reducer1    reducer2Reducer.login.lift(action: \.loginAction, state: \.loginState)Reducer.lifecycle.lift(action: \.lifecycleAction, state: \.lifecycleState)Reducer.appReducer.reduce{ action, statein        // some inline reducer}}

IMPORTANT: Because enums in Swift don't have KeyPath as structs do, we strongly recommend readingAction 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.

Let's explore how to lift reducers and middlewares.

Lifting Reducer

Reducer has AppAction INPUT, AppState INPUT and AppState OUTPUT, because it can only handle actions (never dispatch them), read the state and write the state.

The lifting direction, therefore, should be:

Reducer:- ReducerAction? ← AppAction- ReducerState ←→ AppState

Given:

//      type 1         type 2Reducer<ReducerAction,ReducerState>

Transformations:

                                                                                 ╔═══════════════════╗                                                                                 ║                   ║                       ╔═══════════════╗                                         ║                   ║                       ║    Reducer    ║ .lift                                   ║       Store       ║                       ╚═══════════════╝                                         ║                   ║                               │                                                 ║                   ║                                                                                 ╚═══════════════════╝                               │                                                           │                                                                                                                                               │                                                           │                                                                                               ┌───────────┐                             ┌─────┴─────┐   (AppAction) -> ReducerAction?               │           │    ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐    │  Reducer  │   { $0.case?.reducerAction }                  │           │        Input Action         │  Action   │◀──────────────────────────────────────────────│ AppAction │    └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘    │           │   KeyPath<AppAction, ReducerAction?>          │           │                             └─────┬─────┘   \AppAction.case?.reducerAction              │           │                                                                                         └───────────┘                                   │                                                           │                                                                                                                                               │         get: (AppState) -> ReducerState                   │                                                   { $0.reducerState }                         ┌───────────┐                             ┌─────┴─────┐   set: (inout AppState, ReducerState) -> Void │           │    ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐    │  Reducer  │   { $0.reducerState = $1 }                    │           │            State            │   State   │◀─────────────────────────────────────────────▶│ AppState  │    └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘    │           │   WritableKeyPath<AppState, ReducerState>     │           │                             └─────┬─────┘   \AppState.reducerState                      │           │                                                                                         └───────────┘                                   │                                                           │
Lifting Reducer using closures:
.lift(    actionGetter:{(action:AppAction)->ReducerAction? /* type 1 */in         // prism3 has associated value of ReducerAction,        // and whole thing is Optional because Prism is always optional        action.prism1?.prism2?.prism3},    stateGetter:{(state:AppState)->ReducerState /* type 2 */in         // property2: ReducerState        state.property1.property2},    stateSetter:{(state:inoutAppState, newValue:ReducerState /* type 2 */)->Voidin         // property2: ReducerState        state.property1.property2= newValue})

Steps:

  • Start plugging the 2 types from the Reducer into the 3 closure headers.
  • For type 1, find a prism that resolves from AppAction into the matching type.BE SURE TO RUN SOURCERY AND HAVING ALL ENUM CASES COVERED BY PRISM
  • For type 2 on the stateGetter closure, find lenses (property getters) that resolve from AppState into the matching type.
  • For type 2 on the stateSetter closure, find lenses (property setters) that can change the global state receive to the newValue received. Be sure that everything is writeable.
Lifting Reducer using KeyPath:
.lift(    action: \AppAction.prism1?.prism2?.prism3,    state: \AppState.property1.property2)

Steps:

  • Start with the closure example above
  • For action, we can use KeyPath from\AppAction traversing the prism tree
  • For state, we can use WritableKeyPath from\AppState traversing the properties as long as all of them are declared asvar, notlet.

Functional Helpers

Identity:

  • when some parts of your lift should be unchanged because they are already in the expected type
  • lift that usingidentity, which is{ $0 }

Xcode Snippets:

// Reducer closure.lift(    actionGetter:{(action: AppAction)-><#LocalAction#>? in action.<#something?.child#>},    stateGetter:{(state: AppState)-><#LocalState#> in state.<#something.child#>},    stateSetter:{(state:inoutAppState, newValue:<#LocalState#>)->Voidin state.<#something.child#>= newValue})// Reducer KeyPath:.lift(    action: \AppAction.<#something?.child#>,    state: \AppState.<#something.child#>)

Installation

Please use SwiftRex installation instructions, instead. Reducers are not supposed to be used independently from SwiftRex.

About

SwiftRex Reducer component

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp