|
1 | 1 | import Combine |
2 | 2 |
|
3 | | -/// Middleware is a dependency injection structure that allows you to transform raw actions into refined ones, |
4 | | -/// Refined actions produced by Middleware are then forwarded to the main reducer. |
| 3 | +/// A dependency injection structure where you transform raw actions, into refined actions which are sent to the store's `Reducer`. |
5 | 4 | /// |
| 5 | +/// The middleware is where you handle side effects, asynchronous calls, and generally code which interacts with the outside world (ie: making a network call, loading app data from disk, getting the user's current location), and also aggregate operations like resetting the state. Much like the rest of Recombine, `Middleware` harnesses Combine and its publishers to represent these interactions. |
| 6 | +/// |
| 7 | +///`Middleware` is generic over 3 types: |
| 8 | +/// * `State`: The data structure which represents the current app state. |
| 9 | +/// * `Input`: Most commonly raw actions, this is the value that will be transformed into the `Output`. |
| 10 | +/// * `Output`: Most commonly refined actions, this is the result of the `Input`'s transformation, which is then sent to the store's `Reducer` |
| 11 | +/// |
| 12 | +/// When creating the middleware, you pass in the `State`, `Input`, and `Output` in the angle brackets, and then a closure which takes two arguments – a publisher of `State`, the `Input`, and which returns an `AnyPublisher` of the `Output`. |
| 13 | +/// |
| 14 | +/// Critically, you don't have access to the current state itself – only a "stream" where you can send refined actions. |
| 15 | +/// |
| 16 | +/// Because you need to return an `AnyPublisher`, you usually make your asynchronous calls using Combine publishers, which you can `flatMap(_:)` into the `statePublisher` to return a refined action. It is recommended to make publisher extensions on common types which don't already have one, like `FileManager` or `CLLocationManager`. |
| 17 | +/// |
| 18 | +/// For example, a middleware which handles making a network call and resetting the app's state: |
| 19 | +/// |
| 20 | +/// static let middleware = Middleware<State, Action.Raw, Action.Refined> { statePublisher, action -> AnyPublisher<Action.Refined, Never> in |
| 21 | +/// switch action { |
| 22 | +/// case let networkCall(url): |
| 23 | +/// URLSession.shared.dataTaskPublisher(for: url) |
| 24 | +/// .map(\.data) |
| 25 | +/// .decode(type: MyModel.self, decoder: JSONDecoder()) |
| 26 | +/// .replaceError(with: MyModel()) |
| 27 | +/// .flatMap { myModel in |
| 28 | +/// statePublisher.map { _ in |
| 29 | +/// return .setModel(myModel) |
| 30 | +/// } |
| 31 | +/// } |
| 32 | +/// .eraseToAnyPublisher() |
| 33 | +/// } |
| 34 | +/// case resetAppState: |
| 35 | +/// return [ |
| 36 | +/// .setModel(MyModel.empty), |
| 37 | +/// .usernameModification(.delete)) |
| 38 | +/// ] |
| 39 | +/// .publisher |
| 40 | +/// .eraseToAnyPublisher() |
| 41 | +/// } |
| 42 | +/// } |
| 43 | +/// In the code above, the network call is made in the form of `URLSession`'s `dataTaskPublisher(for:)`. We decode the data and change the publisher's error type using `replaceError(with:)` (since the returned `AnyPublisher`'s error type must be `Never` – this can be done with other operators like `catch(:)` and `mapError(_:)`). |
| 44 | +/// |
| 45 | +/// Then, we replace the `URLSession` publisher with the `statePublisher` using `flatMap(_:)`, which itself returns a refined action: `.setModel(MyModel)`. |
| 46 | +/// |
| 47 | +/// This middleware also handles an aggregate operation, resetting the app state. It simply returns an array of refined actions, which is turned into a publisher using the `publisher` property on the `Sequence` protocol. |
6 | 48 | public struct Middleware<State, Input, Output> { |
7 | 49 | public typealias StatePublisher = Publishers.First<Published<State>.Publisher> |
8 | | - public typealias Function = (StatePublisher, Input) -> AnyPublisher<Output, Never> |
9 | 50 | public typealias Transform<Result> = (StatePublisher, Output) -> Result |
10 | | - internal let transform: Function |
| 51 | + /// The closure which takes in the `StatePublisher` and `Input`, and transforms it into an `AnyPublisher<Output, Never>`; the heart of the middleware. |
| 52 | + internal let transform: (StatePublisher, Input) -> AnyPublisher<Output, Never> |
11 | 53 |
|
12 | | - /// Create a passthrough Middleware. |
| 54 | + /// Create an empty passthrough `Middleware.` |
| 55 | + /// |
| 56 | + /// The input type must be equivalent to the output type. |
| 57 | + /// |
| 58 | + ///For example: |
| 59 | + /// |
| 60 | + /// static let passthroughMiddleware = Middleware<State, Action.Refined, Action.Refined>() |
13 | 61 | public init() where Input == Output { |
14 | 62 | self.transform = { Just($1).eraseToAnyPublisher() } |
15 | 63 | } |
16 | 64 |
|
17 | | - /// Initialises the middleware with a transformative function. |
18 | | - /// - parameter transform: The function that will be able to modify passed actions. |
| 65 | + /// Initialises the middleware with a closure which handles transforming the raw actions and returning refined actions. |
| 66 | + /// - parameter transform: The closure which takes a publisher of `State`, and the `Middleware`'s `Input`, and returns a publisher who's output is the `Middleware`'s `Output`. |
| 67 | + /// |
| 68 | + /// The `transform` closure takes two parameters: |
| 69 | + /// * A publisher wrapping over the state that was passed into the `Middleware`'s angle brackets. |
| 70 | + /// * The middleware's input – most commonly raw actions. |
| 71 | + /// |
| 72 | + /// The closure then returns a publisher who's output is equivalent to the `Middleware`'s `Output` – most commonly refined actions. |
| 73 | + /// |
| 74 | + /// For example: |
| 75 | + /// |
| 76 | + /// static let middleware = Middleware<State, Action.Raw, Action.Refined> { statePublisher, action -> AnyPublisher<Action.Refined, Never> in |
| 77 | + /// switch action { |
| 78 | + /// case let findCurrentLocation(service): |
| 79 | + /// CLLocationManager.currentLocationPublisher(service: service) |
| 80 | + /// .map { LocationModel(location: $0) } |
| 81 | + /// .flatMap { location in |
| 82 | + /// statePublisher.map { _ in |
| 83 | + /// return .setLocation(to: location) |
| 84 | + /// } |
| 85 | + /// } |
| 86 | + /// .catch { err in |
| 87 | + /// return Just(.locationError(err)) |
| 88 | + /// } |
| 89 | + /// .eraseToAnyPublisher() |
| 90 | + /// For a more detailed explanation, go to the `Middleware` documentation. |
19 | 91 | public init<P: Publisher>( |
20 | 92 | _ transform: @escaping (StatePublisher, Input) -> P |
21 | 93 | ) where P.Output == Output, P.Failure == Never { |
22 | 94 | self.transform = { transform($0, $1).eraseToAnyPublisher() } |
23 | 95 | } |
24 | 96 |
|
25 | | - /// Concatenates the transform function of the passed `Middleware` onto the callee's transform. |
| 97 | + /// Adds two middlewares together, concatenating the passed-in middleware's closure to the caller's own closure. |
| 98 | + /// - Parameter other: The other middleware, who's `State`, `Input`, and `Output` must be equivalent to the callers'. |
| 99 | + /// - Returns: A `Middleware` who's closure is the result of concatenating the caller's closure and the passed in middleware's closure. |
| 100 | + /// |
| 101 | + /// Use this function when you want to break up your middleware code to make it more compositional. |
| 102 | + /// |
| 103 | + /// For example: |
| 104 | + /// |
| 105 | + /// static let middleware = Middleware<State, Action.Raw, Action.Refined> { statePublisher, action -> AnyPublisher<Action.Refined, Never> in |
| 106 | + /// switch action { |
| 107 | + /// case loadAppData: |
| 108 | + /// FileManager.default.loadPublisher(from: "appData.json", in: .applicationSupportDirectory) |
| 109 | + /// .decode(type: State.self, decoder: JSONDecoder()) |
| 110 | + /// // etc... |
| 111 | + /// default: |
| 112 | + /// break |
| 113 | + /// } |
| 114 | + /// } |
| 115 | + /// .concat( |
| 116 | + /// Middleware<State, Action.Raw, Action.Refined> { statePublisher, action -> AnyPublisher<Action.Refined, Never> in |
| 117 | + /// switch action { |
| 118 | + /// case let displayBluetoothPeripherals(services: services): |
| 119 | + /// CBCentralManager.peripheralsPublisher(services: services) |
| 120 | + /// .map(\.peripheralName) |
| 121 | + /// // etc... |
| 122 | + /// default: |
| 123 | + /// break |
| 124 | + /// ) |
26 | 125 | public func concat<Result>(_ other: Middleware<State, Output, Result>) -> Middleware<State, Input, Result> { |
27 | 126 | .init { state, action in |
28 | 127 | self.transform(state, action).flatMap { |
|
0 commit comments