nalexn / clean-architecture-swiftui

SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Chain one or more calls on completion of the first

phiberjenz opened this issue · comments

Hi @nalexn and thank you for providing your knowledge regarding Swift and SwiftUI! Really awesome work.

I have a question regarding chaining of multiple requests on completion of the first. I've looked into #21 and you provided a solution there (different interactors though, in my case it's multiple endpoint of the same WebRepository, and I'm using the MVVM variant of this repo).
Instead of using a LoadableSubject I would like to populate two or more Loadables in the AppState in a single "transaction", but since the first one will get a token and the second one will need this token to authenticate, it's imperative that the first call, getting and setting the token, is completed before the second one is fired.

Also, when trying to put a Loadable<(Token, User)>, with a tuple as is done in your example in #21, but there with a LoadableSubject instead, the AppState no longer complies with the Equatable protocol (both of the Codable models in the tuple are Equatable though). Why is this?

Is there another way of doing this chaining process which is in line with your philosophy? Or is it preferred that the UI layer chains the requests instead of the service or repository layer? (It seems wrong to me. I will always have to get the User after the Token is fetched, and also a few other requests that depends on another).

Best regards,
Jens

Hey @phiberjenz

Thank you for the questions!

AppState no longer complies with the Equatable protocol (both of the Codable models in the tuple are Equatable though). Why is this?

This is because tuples in Swift, such as (Token, User), (yet) cannot conform to protocols, including Equatable. A workaround is to use a struct instead of a tuple.

Instead of using a LoadableSubject I would like to populate two or more Loadables in the AppState in a single "transaction"

You certainly can do that. Moreover, it is perfectly fine to update the AppState several times for such chained requests, while for the active screen the whole chain would still look like a single transaction.

I'll consider separately the case where you need to authenticate to get the token and send another request, as this very example is a common problem and has a better-suited solution than the one I provide first, just to illustrate how you can update two Loadables within one transaction.

The sinkToLoadable helper function is designed to be used in the end of the chain:

func sinkToLoadable(_ completion: @escaping (Loadable<Output>) -> Void) -> AnyCancellable {
    return sink(receiveCompletion: { subscriptionCompletion in
        if let error = subscriptionCompletion.error {
            completion(.failed(error))
        }
    }, receiveValue: { value in
        completion(.loaded(value))
    })
}

however, you can introduce another function to inject result handling in the middle of the chain:

func handleAsLoadable(_ completion: @escaping (Loadable<Output>) -> Void) -> Publishers.HandleEvents<Self> {
    return handleEvents(receiveOutput: { value in
        completion(.loaded(value))
    }, receiveCompletion: { subscriptionCompletion in
        if let error = subscriptionCompletion.error {
            completion(.failed(error))
        }
    })
}

So the chain would look like this:

...
.flatMap {
    // request
}
.handleAsLoadable { value1.wrappedValue = $0 }
.flatMap {
    // request
}
.sinkToLoadable { value2.wrappedValue = $0 }
.store(in: cancelBag)

I haven't tested it, but I'm pretty sure it should work as expected.


Now the earlier announced solution for the case with authentication tokens. The best design I've seen so far is using the locks and keys principle for "unlocking" services that require the authenticated state.

The services and repositories would have to be re-grouped into two separate DIContainers, one of which can be initialized without any parameters, while the second would require Token as the init parameter. The beauty of this approach is that all the repositories in the second container can obtain the token upon creation, which saves from writing boilerplate code of extracting the token from somewhere and ensuring it is present for every request.

The exact implementation of this idea may vary, but I have a tiny sample project that illustrates it.

Just... wow! Thank you so much for you thorough answer, it's really helpful. I will definitely try out the handleAsLoadable straight away, this really saved my day 👍

Regarding the locks and keys principle, I will read through this and give it a go. At the moment I've solved it by injecting an "Authorization" header in the call function as such:

func call<Value>(endpoint: APICall, httpCodes: HTTPCodes = .success) -> AnyPublisher<Value, Error>
        where Value: Decodable {
        do {
            var request = try endpoint.urlRequest(baseURL: baseURL)
            if let userToken = appState.value.userData.userToken.value {
                request.addValue("Bearer \(userToken.key)", forHTTPHeaderField: "Authorization")
            }
            return session
                .dataTaskPublisher(for: request)
                .requestJSON(httpCodes: httpCodes)
        } catch let error {
            return Fail<Value, Error>(error: error).eraseToAnyPublisher()
        }
    }

This is actually pretty simple stuff, I just send the header as soon as the token is available in the AppState, for all requests (which is fine because my API requires auth for every endpoint but the login). It's not that robust though, since it relies on there actually being a token set, but it works for now. Though I like the idea of an "authorized" DIContainter.

Again, thank you @nalexn for your time and knowledge!