66import Foundation
77import Combine
88import SwiftUI
9+ import os
910
1011/// Fx is a publisher that publishes actions and never fails.
1112public typealias Fx < Action> = AnyPublisher < Action , Never >
@@ -16,11 +17,15 @@ public protocol ModelProtocol: Equatable {
1617 associatedtype Action
1718 associatedtype Environment
1819
20+ associatedtype UpdateType : UpdateProtocol where
21+ UpdateType. Model == Self ,
22+ UpdateType. Action == Self . Action
23+
1924 static func update(
2025 state: Self ,
2126 action: Action ,
2227 environment: Environment
23- ) -> Update < Self >
28+ ) -> UpdateType
2429}
2530
2631extension ModelProtocol {
@@ -35,16 +40,16 @@ extension ModelProtocol {
3540 state: Self ,
3641 actions: [ Action ] ,
3742 environment: Environment
38- ) -> Update < Self > {
43+ ) -> UpdateType {
3944 actions. reduce (
40- Update ( state: state) ,
45+ UpdateType ( state: state) ,
4146 { result, action in
4247 let next = update (
4348 state: result. state,
4449 action: action,
4550 environment: environment
4651 )
47- return Update (
52+ return UpdateType (
4853 state: next. state,
4954 fx: result. fx. merge ( with: next. fx) . eraseToAnyPublisher ( ) ,
5055 transaction: next. transaction
@@ -74,70 +79,66 @@ extension ModelProtocol {
7479 state: Self ,
7580 action viewAction: ViewModel . Action ,
7681 environment: ViewModel . Environment
77- ) -> Update < Self > {
82+ ) -> UpdateType {
7883 // If getter returns nil (as in case of a list item that no longer
7984 // exists), do nothing.
8085 guard let inner = get ( state) else {
81- return Update ( state: state)
86+ return UpdateType ( state: state)
8287 }
8388 let next = ViewModel . update (
8489 state: inner,
8590 action: viewAction,
8691 environment: environment
8792 )
88- return Update (
93+ return UpdateType (
8994 state: set ( state, next. state) ,
9095 fx: next. fx. map ( tag) . eraseToAnyPublisher ( ) ,
9196 transaction: next. transaction
9297 )
9398 }
9499}
95100
96- /// Update represents a state change, together with an `Fx` publisher,
101+ /// `UpdateProtocol` represents a state change, together with an `Fx` publisher,
97102/// and an optional `Transaction`.
98- public struct Update < Model: ModelProtocol > {
99- /// `State` for this update
100- public var state : Model
101- /// `Fx` for this update.
102- /// Default is an `Empty` publisher (no effects)
103- public var fx : Fx < Model . Action >
104- /// The transaction that should be set during this update.
105- /// Store uses this value to set the transaction while updating state,
106- /// allowing you to drive explicit animations from your update function.
107- /// If left `nil`, store will defer to the global transaction
108- /// for this state update.
109- /// See https://developer.apple.com/documentation/swiftui/transaction
110- public var transaction : Transaction ?
111-
112- public init (
103+ public protocol UpdateProtocol {
104+ associatedtype Model
105+ associatedtype Action
106+
107+ init (
113108 state: Model ,
114- fx: Fx < Model . Action > ,
109+ fx: Fx < Action > ,
115110 transaction: Transaction ?
116- ) {
117- self . state = state
118- self . fx = fx
119- self . transaction = transaction
120- }
111+ )
121112
113+ var state : Model { get set }
114+ var fx : Fx < Action > { get set }
115+ var transaction : Transaction ? { get set }
116+ }
117+
118+ extension UpdateProtocol {
122119 public init ( state: Model , animation: Animation ? = nil ) {
123- self . state = state
124- self . fx = Empty ( completeImmediately: true ) . eraseToAnyPublisher ( )
125- self . transaction = Transaction ( animation: animation)
120+ self . init (
121+ state: state,
122+ fx: Empty ( completeImmediately: true ) . eraseToAnyPublisher ( ) ,
123+ transaction: Transaction ( animation: animation)
124+ )
126125 }
127-
126+
128127 public init (
129128 state: Model ,
130- fx: Fx < Model . Action > ,
129+ fx: Fx < Action > ,
131130 animation: Animation ? = nil
132131 ) {
133- self . state = state
134- self . fx = fx
135- self . transaction = Transaction ( animation: animation)
132+ self . init (
133+ state: state,
134+ fx: fx,
135+ transaction: Transaction ( animation: animation)
136+ )
136137 }
137-
138+
138139 /// Merge existing fx together with new fx.
139140 /// - Returns a new `Update`
140- public func mergeFx( _ fx: Fx < Model . Action > ) -> Update < Model > {
141+ public func mergeFx( _ fx: Fx < Action > ) -> Self {
141142 var this = self
142143 this. fx = self . fx. merge ( with: fx) . eraseToAnyPublisher ( )
143144 return this
@@ -153,6 +154,34 @@ public struct Update<Model: ModelProtocol> {
153154 }
154155}
155156
157+ /// Concrete implementation of `UpdateProtocol`.
158+ /// Update represents a state change, together with an `Fx` publisher,
159+ /// and an optional `Transaction`.
160+ public struct Update < Model: ModelProtocol > : UpdateProtocol {
161+ /// `State` for this update
162+ public var state : Model
163+ /// `Fx` for this update.
164+ /// Default is an `Empty` publisher (no effects)
165+ public var fx : Fx < Model . Action >
166+ /// The transaction that should be set during this update.
167+ /// Store uses this value to set the transaction while updating state,
168+ /// allowing you to drive explicit animations from your update function.
169+ /// If left `nil`, store will defer to the global transaction
170+ /// for this state update.
171+ /// See https://developer.apple.com/documentation/swiftui/transaction
172+ public var transaction : Transaction ?
173+
174+ public init (
175+ state: Model ,
176+ fx: Fx < Model . Action > ,
177+ transaction: Transaction ?
178+ ) {
179+ self . state = state
180+ self . fx = fx
181+ self . transaction = transaction
182+ }
183+ }
184+
156185/// A store is any type that can
157186/// - get a state
158187/// - send actions
@@ -175,17 +204,41 @@ public protocol StoreProtocol {
175204public final class Store < Model> : ObservableObject , StoreProtocol
176205where Model: ModelProtocol
177206{
178- /// Stores cancellables by ID
179- private( set) var cancellables : [ UUID : AnyCancellable ] = [ : ]
207+ /// Cancellable for fx subscription.
208+ private var cancelFx : AnyCancellable ?
209+
180210 /// Private for all actions sent to the store.
181- private var _actions : PassthroughSubject < Model . Action , Never >
211+ private var _actions = PassthroughSubject < Model . Action , Never > ( )
212+
182213 /// Publisher for all actions sent to the store.
183214 public var actions : AnyPublisher < Model . Action , Never > {
184215 _actions. eraseToAnyPublisher ( )
185216 }
217+
218+ /// Source publisher for batches of fx modeled as publishers.
219+ private var _fxBatches = PassthroughSubject < Fx < Model . Action > , Never > ( )
220+
221+ /// `fx` represents a flat stream of actions from all fx publishers.
222+ private var fx : AnyPublisher < Model . Action , Never > {
223+ _fxBatches
224+ . flatMap ( { publisher in publisher } )
225+ . receive ( on: DispatchQueue . main)
226+ . eraseToAnyPublisher ( )
227+ }
228+
229+ /// Publisher for updates performed on state
230+ private var _updates = PassthroughSubject < Model . UpdateType , Never > ( )
231+
232+ /// Publisher for updates performed on state.
233+ /// `updates` is guaranteed to fire after the state has changed.
234+ public var updates : AnyPublisher < Model . UpdateType , Never > {
235+ _updates. eraseToAnyPublisher ( )
236+ }
237+
186238 /// Current state.
187239 /// All writes to state happen through actions sent to `Store.send`.
188240 @Published public private( set) var state : Model
241+
189242 /// Environment, which typically holds references to outside information,
190243 /// such as API methods.
191244 ///
@@ -202,24 +255,47 @@ where Model: ModelProtocol
202255 /// app is stopped.
203256 public var environment : Model . Environment
204257
258+ /// Logger to log actions sent to store.
259+ private var logger : Logger
260+ /// Should log?
261+ var loggingEnabled : Bool
262+
205263 public init (
206264 state: Model ,
207- environment: Model . Environment
265+ environment: Model . Environment ,
266+ loggingEnabled: Bool = false ,
267+ logger: Logger ? = nil
208268 ) {
209269 self . state = state
210270 self . environment = environment
211- self . _actions = PassthroughSubject < Model . Action , Never > ( )
271+ self . loggingEnabled = loggingEnabled
272+ self . logger = logger ?? Logger (
273+ subsystem: " ObservableStore " ,
274+ category: " Store "
275+ )
276+
277+ self . cancelFx = self . fx
278+ . sink ( receiveValue: { [ weak self] action in
279+ self ? . send ( action)
280+ } )
212281 }
213282
214283 /// Initialize with a closure that receives environment.
215284 /// Useful for initializing model properties from environment, and for
216285 /// kicking off actions once at store creation.
217286 public convenience init (
218287 create: ( Model . Environment ) -> Update < Model > ,
219- environment: Model . Environment
288+ environment: Model . Environment ,
289+ loggingEnabled: Bool = false ,
290+ logger: Logger ? = nil
220291 ) {
221292 let update = create ( environment)
222- self . init ( state: update. state, environment: environment)
293+ self . init (
294+ state: update. state,
295+ environment: environment,
296+ loggingEnabled: loggingEnabled,
297+ logger: logger
298+ )
223299 self . subscribe ( to: update. fx)
224300 }
225301
@@ -229,69 +305,40 @@ where Model: ModelProtocol
229305 public convenience init (
230306 state: Model ,
231307 action: Model . Action ,
232- environment: Model . Environment
308+ environment: Model . Environment ,
309+ loggingEnabled: Bool = false ,
310+ logger: Logger ? = nil
233311 ) {
234- self . init ( state: state, environment: environment)
312+ self . init (
313+ state: state,
314+ environment: environment,
315+ loggingEnabled: loggingEnabled,
316+ logger: logger
317+ )
235318 self . send ( action)
236319 }
237320
238- /// Subscribe to a publisher of actions, piping them through to
239- /// the store.
240- ///
241- /// Holds on to the cancellable until publisher completes.
242- /// When publisher completes, removes cancellable.
321+ /// Subscribe to a publisher of actions, send the actions it publishes
322+ /// to the store.
243323 public func subscribe( to fx: Fx < Model . Action > ) {
244- // Create a UUID for the cancellable.
245- // Store cancellable in dictionary by UUID.
246- // Remove cancellable from dictionary upon effect completion.
247- // This retains the effect pipeline for as long as it takes to complete
248- // the effect, and then removes it, so we don't have a cancellables
249- // memory leak.
250- let id = UUID ( )
251-
252- // Receive Fx on main thread. This does two important things:
253- //
254- // First, SwiftUI requires that any state mutations that would change
255- // views happen on the main thread. Receiving on main ensures that
256- // all fx-driven state transitions happen on main, even if the
257- // publisher is off-main-thread.
258- //
259- // Second, if we didn't schedule receive on main, it would be possible
260- // for publishers to complete immediately, causing receiveCompletion
261- // to attempt to remove the publisher from `cancellables` before
262- // it is added. By scheduling to receive publisher on main,
263- // we force publisher to complete on next tick, ensuring that it
264- // is always first added, then removed from `cancellables`.
265- let cancellable = fx
266- . receive (
267- on: DispatchQueue . main,
268- options: . init( qos: . default)
269- )
270- . sink (
271- receiveCompletion: { [ weak self] _ in
272- self ? . cancellables. removeValue ( forKey: id)
273- } ,
274- receiveValue: { [ weak self] action in
275- self ? . send ( action)
276- }
277- )
278- self . cancellables [ id] = cancellable
324+ self . _fxBatches. send ( fx)
279325 }
280326
281327 /// Send an action to the store to update state and generate effects.
282328 /// Any effects generated are fed back into the store.
283329 ///
284330 /// Note: SwiftUI requires that all UI changes happen on main thread.
285- /// We run effects as-given, without forcing them on to main thread.
286- /// This means that main-thread effects will be run immediately, enabling
287- /// you to drive things like withAnimation via actions.
288- /// However it also means that publishers which run off-main-thread MUST
289- /// make sure that they join the main thread (e.g. with
290- /// `.receive(on: DispatchQueue.main)`).
331+ /// `send(_:)` is run *synchronously*. It is up to you to guarantee it is
332+ /// run on main thread when SwiftUI is being used.
291333 public func send( _ action: Model . Action ) {
292- /// Broadcast action to any outside subscribers
293- self . _actions. send ( action)
294- // Generate next state and effect
334+ if loggingEnabled {
335+ logger. log ( " Action: \( String ( describing: action) ) " )
336+ }
337+
338+ // Dispatch action before state change
339+ _actions. send ( action)
340+
341+ // Create next state update
295342 let next = Model . update (
296343 state: self . state,
297344 action: action,
@@ -319,8 +366,12 @@ where Model: ModelProtocol
319366 self . state = next. state
320367 }
321368 }
322- // Run effect
369+
370+ // Run effects
323371 self . subscribe ( to: next. fx)
372+
373+ // Dispatch update after state change
374+ self . _updates. send ( next)
324375 }
325376}
326377
0 commit comments