66import Foundation
77import Combine
88import SwiftUI
9- import os
109
1110/// Fx is a publisher that publishes actions and never fails.
1211public typealias Fx < Action> = AnyPublisher < Action , Never >
@@ -17,15 +16,11 @@ public protocol ModelProtocol: Equatable {
1716 associatedtype Action
1817 associatedtype Environment
1918
20- associatedtype UpdateType : UpdateProtocol where
21- UpdateType. Model == Self ,
22- UpdateType. Action == Self . Action
23-
2419 static func update(
2520 state: Self ,
2621 action: Action ,
2722 environment: Environment
28- ) -> UpdateType
23+ ) -> Update < Self >
2924}
3025
3126extension ModelProtocol {
@@ -40,16 +35,16 @@ extension ModelProtocol {
4035 state: Self ,
4136 actions: [ Action ] ,
4237 environment: Environment
43- ) -> UpdateType {
38+ ) -> Update < Self > {
4439 actions. reduce (
45- UpdateType ( state: state) ,
40+ Update ( state: state) ,
4641 { result, action in
4742 let next = update (
4843 state: result. state,
4944 action: action,
5045 environment: environment
5146 )
52- return UpdateType (
47+ return Update (
5348 state: next. state,
5449 fx: result. fx. merge ( with: next. fx) . eraseToAnyPublisher ( ) ,
5550 transaction: next. transaction
@@ -79,66 +74,70 @@ extension ModelProtocol {
7974 state: Self ,
8075 action viewAction: ViewModel . Action ,
8176 environment: ViewModel . Environment
82- ) -> UpdateType {
77+ ) -> Update < Self > {
8378 // If getter returns nil (as in case of a list item that no longer
8479 // exists), do nothing.
8580 guard let inner = get ( state) else {
86- return UpdateType ( state: state)
81+ return Update ( state: state)
8782 }
8883 let next = ViewModel . update (
8984 state: inner,
9085 action: viewAction,
9186 environment: environment
9287 )
93- return UpdateType (
88+ return Update (
9489 state: set ( state, next. state) ,
9590 fx: next. fx. map ( tag) . eraseToAnyPublisher ( ) ,
9691 transaction: next. transaction
9792 )
9893 }
9994}
10095
101- /// `UpdateProtocol` represents a state change, together with an `Fx` publisher,
96+ /// Update represents a state change, together with an `Fx` publisher,
10297/// and an optional `Transaction`.
103- public protocol UpdateProtocol {
104- associatedtype Model
105- associatedtype Action
106-
107- init (
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 (
108113 state: Model ,
109- fx: Fx < Action > ,
114+ fx: Fx < Model . Action > ,
110115 transaction: Transaction ?
111- )
112-
113- var state : Model { get set }
114- var fx : Fx < Action > { get set }
115- var transaction : Transaction ? { get set }
116- }
116+ ) {
117+ self . state = state
118+ self . fx = fx
119+ self . transaction = transaction
120+ }
117121
118- extension UpdateProtocol {
119122 public init ( state: Model , animation: Animation ? = nil ) {
120- self . init (
121- state: state,
122- fx: Empty ( completeImmediately: true ) . eraseToAnyPublisher ( ) ,
123- transaction: Transaction ( animation: animation)
124- )
123+ self . state = state
124+ self . fx = Empty ( completeImmediately: true ) . eraseToAnyPublisher ( )
125+ self . transaction = Transaction ( animation: animation)
125126 }
126-
127+
127128 public init (
128129 state: Model ,
129- fx: Fx < Action > ,
130+ fx: Fx < Model . Action > ,
130131 animation: Animation ? = nil
131132 ) {
132- self . init (
133- state: state,
134- fx: fx,
135- transaction: Transaction ( animation: animation)
136- )
133+ self . state = state
134+ self . fx = fx
135+ self . transaction = Transaction ( animation: animation)
137136 }
138-
137+
139138 /// Merge existing fx together with new fx.
140139 /// - Returns a new `Update`
141- public func mergeFx( _ fx: Fx < Action > ) -> Self {
140+ public func mergeFx( _ fx: Fx < Model . Action > ) -> Update < Model > {
142141 var this = self
143142 this. fx = self . fx. merge ( with: fx) . eraseToAnyPublisher ( )
144143 return this
@@ -154,34 +153,6 @@ extension UpdateProtocol {
154153 }
155154}
156155
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-
185156/// A store is any type that can
186157/// - get a state
187158/// - send actions
@@ -204,43 +175,17 @@ public protocol StoreProtocol {
204175public final class Store < Model> : ObservableObject , StoreProtocol
205176where Model: ModelProtocol
206177{
207- private var cancelTransactions : AnyCancellable ?
208-
209- /// Cancellable for fx subscription.
210- private var cancelFx : AnyCancellable ?
211-
178+ /// Stores cancellables by ID
179+ private( set) var cancellables : [ UUID : AnyCancellable ] = [ : ]
212180 /// Private for all actions sent to the store.
213- private var _actions = PassthroughSubject < Model . Action , Never > ( )
214-
181+ private var _actions : PassthroughSubject < Model . Action , Never >
215182 /// Publisher for all actions sent to the store.
216183 public var actions : AnyPublisher < Model . Action , Never > {
217184 _actions. eraseToAnyPublisher ( )
218185 }
219-
220- /// Source publisher for batches of fx modeled as publishers.
221- private var _fxBatches = PassthroughSubject < Fx < Model . Action > , Never > ( )
222-
223- /// `fx` represents a flat stream of actions from all fx publishers.
224- private var fx : AnyPublisher < Model . Action , Never > {
225- _fxBatches
226- . flatMap ( { publisher in publisher } )
227- . receive ( on: DispatchQueue . main)
228- . eraseToAnyPublisher ( )
229- }
230-
231- /// Publisher for updates performed on state
232- private var _updates = PassthroughSubject < Model . UpdateType , Never > ( )
233-
234- /// Publisher for updates performed on state.
235- /// `updates` is guaranteed to fire after the state has changed.
236- public var updates : AnyPublisher < Model . UpdateType , Never > {
237- _updates. eraseToAnyPublisher ( )
238- }
239-
240186 /// Current state.
241187 /// All writes to state happen through actions sent to `Store.send`.
242188 @Published public private( set) var state : Model
243-
244189 /// Environment, which typically holds references to outside information,
245190 /// such as API methods.
246191 ///
@@ -257,47 +202,24 @@ where Model: ModelProtocol
257202 /// app is stopped.
258203 public var environment : Model . Environment
259204
260- /// Logger to log actions sent to store.
261- private var logger : Logger
262- /// Should log?
263- var loggingEnabled : Bool
264-
265205 public init (
266206 state: Model ,
267- environment: Model . Environment ,
268- loggingEnabled: Bool = false ,
269- logger: Logger ? = nil
207+ environment: Model . Environment
270208 ) {
271209 self . state = state
272210 self . environment = environment
273- self . loggingEnabled = loggingEnabled
274- self . logger = logger ?? Logger (
275- subsystem: " ObservableStore " ,
276- category: " Store "
277- )
278-
279- self . cancelFx = self . fx
280- . sink ( receiveValue: { [ weak self] action in
281- self ? . send ( action)
282- } )
211+ self . _actions = PassthroughSubject < Model . Action , Never > ( )
283212 }
284213
285214 /// Initialize with a closure that receives environment.
286215 /// Useful for initializing model properties from environment, and for
287216 /// kicking off actions once at store creation.
288217 public convenience init (
289218 create: ( Model . Environment ) -> Update < Model > ,
290- environment: Model . Environment ,
291- loggingEnabled: Bool = false ,
292- logger: Logger ? = nil
219+ environment: Model . Environment
293220 ) {
294221 let update = create ( environment)
295- self . init (
296- state: update. state,
297- environment: environment,
298- loggingEnabled: loggingEnabled,
299- logger: logger
300- )
222+ self . init ( state: update. state, environment: environment)
301223 self . subscribe ( to: update. fx)
302224 }
303225
@@ -307,40 +229,69 @@ where Model: ModelProtocol
307229 public convenience init (
308230 state: Model ,
309231 action: Model . Action ,
310- environment: Model . Environment ,
311- loggingEnabled: Bool = false ,
312- logger: Logger ? = nil
232+ environment: Model . Environment
313233 ) {
314- self . init (
315- state: state,
316- environment: environment,
317- loggingEnabled: loggingEnabled,
318- logger: logger
319- )
234+ self . init ( state: state, environment: environment)
320235 self . send ( action)
321236 }
322237
323- /// Subscribe to a publisher of actions, send the actions it publishes
324- /// to the store.
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.
325243 public func subscribe( to fx: Fx < Model . Action > ) {
326- self . _fxBatches. send ( fx)
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
327279 }
328280
329281 /// Send an action to the store to update state and generate effects.
330282 /// Any effects generated are fed back into the store.
331283 ///
332284 /// Note: SwiftUI requires that all UI changes happen on main thread.
333- /// `send(_:)` is run *synchronously*. It is up to you to guarantee it is
334- /// run on main thread when SwiftUI is being used.
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)`).
335291 public func send( _ action: Model . Action ) {
336- if loggingEnabled {
337- logger. log ( " Action: \( String ( describing: action) ) " )
338- }
339-
340- // Dispatch action before state change
341- _actions. send ( action)
342-
343- // Create next state update
292+ /// Broadcast action to any outside subscribers
293+ self . _actions. send ( action)
294+ // Generate next state and effect
344295 let next = Model . update (
345296 state: self . state,
346297 action: action,
@@ -368,12 +319,8 @@ where Model: ModelProtocol
368319 self . state = next. state
369320 }
370321 }
371-
372- // Run effects
322+ // Run effect
373323 self . subscribe ( to: next. fx)
374-
375- // Dispatch update after state change
376- self . _updates. send ( next)
377324 }
378325}
379326
0 commit comments