SwiftUIFlux works great for apps which only need a singleton or two in their service layer, since you can just reference the singletons in AsyncActions to perform network requests etc. For apps with a larger dependency graph, I’ve been thinking about how to connect async actions to services without relying on singletons — and at the same time trying to avoid the age-old pitfall of creating another giant complex dependency injection system, or turning SwiftUIFlux into something much more complex than it needs to be.
I wanted something where I could wire up my dependency graph as usual, and handle SwiftUIFlux Actions in one place:
actions.publisher(for: LogIn.self) // Catch a `LogIn` action
.flatMapResult(userAuthenticator.logIn) // Log in using `userAuthenticator` and return a `Result`
.sink(receiveValue: store.dispatch) // Dispatch the result back to the store
.store(in: &subscriptions)
I put together a simple middleware which provides combine publishers for actions. The idea is you catch these actions and map them to service calls. The neat part is if you have your services also return Action publishers, you can then connect them back to the store for further dispatch.
Here’s a more full example showing an app with two dependencies (my app’s real dependency graph is obviously more complex and is several layers deep):
class AppContainer {
let store: Store<AppState>
private let actions: ActionPublisherMiddleware
private let userAuthenticator: UserAuthenticator
private let soundEffectsPlayer: SoundEffectsPlayer
private var subscriptions: [AnyCancellable] = []
init() {
actions = ActionPublisherMiddleware()
userAuthenticator = UserAuthenticator(/* Some dependency... */)
soundEffectsPlayer = SoundEffectsPlayer(/* Some other dependency... */)
store = AppStore(reducer: appReducer, middleware: [actions.handler], state: AppState())
// Subscriptions
actions.publisher(for: LogIn.self)
.flatMapResult(userAuthenticator.logIn)
.sink(receiveValue: store.dispatch)
.store(in: &subscriptions)
actions.publisher(for: LogInDidSucceed.self)
.map { _ in SoundEffectsPlayer.Sound.bing }
.sink(receiveValue: soundEffectsPlayer.play)
.store(in: &subscriptions)
actions.publisher(for: LogOut.self)
.flatMapResult(userAuthenticator.logOut)
.sink(receiveValue: store.dispatch)
.store(in: &subscriptions)
}
}
The middleware is trivial, and you could arrange it however you like (say as a free function with a global publisher if you preferred). I’ve done it as a class with a handler function:
class ActionPublisherMiddleware {
private let publisher = PassthroughSubject<Action, Never>()
func publisher<A>(for action: A.Type) -> AnyPublisher<A, Never> where A : Action {
publisher
.compactMap { $0 as? A }
.eraseToAnyPublisher()
}
func handler(dispatch: @escaping DispatchFunction, getState: @escaping () -> FluxState?) -> (@escaping DispatchFunction) -> DispatchFunction {
return { next in
return { action in
self.publisher.send(action)
return next(action)
}
}
}
}
The neat behaviour where the results of async service calls can be mapped back to the store dispatcher is also trivial — I used an overload on Store.dispatch that just dispatches from a result type where both cases are actions:
extension Store {
func dispatch<S, F>(result: Result<S, F>) where S : Action, F : Action {
switch result {
case .success(let action): dispatch(action: action)
case .failure(let action): dispatch(action: action)
}
}
}
I’ve started structuring my services to take and return actions. Actions are just plain values after all so it doesn’t pose any real problem for them to make their way into the service layer. Here’s an example:
// Service
class UserAuthenticator {
func logIn(action: LogIn) -> AnyPublisher<LogInDidSucceed, LogInDidFail>
func logOut(action: LogOut) -> AnyPublisher<LogOutDidSucceed, LogOutDidFail>
}
// Actions
struct LogIn: Action {
let username: String
let password: String
}
struct LogInDidSucceed: Action {
let user: User
}
struct LogInDidFail: Action, Error {
let error: Error
}
struct LogOut: Action {
// ...
}
struct LogOutDidSucceed: Action {
// ...
}
struct LogOutDidFail: Action, Error {
let error: Error
}
I would be interested to hear if anyone else has also done something like the above. Are there any parts of this that would be suitable for inclusion in SwiftUIFlux? Would the Combine publisher middleware be general enough to be attached to the store (eg. store.publisher(for: <Action>.self)...)?
SwiftUIFlux works great for apps which only need a singleton or two in their service layer, since you can just reference the singletons in
AsyncAction
s to perform network requests etc. For apps with a larger dependency graph, I’ve been thinking about how to connect async actions to services without relying on singletons — and at the same time trying to avoid the age-old pitfall of creating another giant complex dependency injection system, or turning SwiftUIFlux into something much more complex than it needs to be.I wanted something where I could wire up my dependency graph as usual, and handle SwiftUIFlux
Action
s in one place:I put together a simple middleware which provides combine publishers for actions. The idea is you catch these actions and map them to service calls. The neat part is if you have your services also return
Action
publishers, you can then connect them back to the store for further dispatch.Here’s a more full example showing an app with two dependencies (my app’s real dependency graph is obviously more complex and is several layers deep):
The middleware is trivial, and you could arrange it however you like (say as a free function with a global publisher if you preferred). I’ve done it as a class with a handler function:
The neat behaviour where the results of async service calls can be mapped back to the store dispatcher is also trivial — I used an overload on
Store.dispatch
that just dispatches from a result type where both cases are actions:I’ve started structuring my services to take and return actions. Actions are just plain values after all so it doesn’t pose any real problem for them to make their way into the service layer. Here’s an example:
I would be interested to hear if anyone else has also done something like the above. Are there any parts of this that would be suitable for inclusion in SwiftUIFlux? Would the Combine publisher middleware be general enough to be attached to the store (eg.
store.publisher(for: <Action>.self)...
)?