Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency Management for Larger Apps #16

Open
danhalliday opened this issue Dec 30, 2020 · 0 comments
Open

Dependency Management for Larger Apps #16

danhalliday opened this issue Dec 30, 2020 · 0 comments

Comments

@danhalliday
Copy link

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)...)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant