Search code examples
swiftgenericscombinepublisher

How to combine two requests but return generic publisher? Details below


I have a generic function used to send requests to the server. Now before I send a request I need to check if the session token is expired and update it if needed.

my function looks like this
func upload<T: Decodable>(some parameters here) -> AnyPublisher<T, Error>

I wanted to check and update the token inside that function before calling the main request but in this case, I can not return AnyPublisher<T, Error>

func upload<T: Decodable>(some parameters here) -> AnyPublisher<T, Error> {
    if shouldUpdateToken {
        let request = // prepare request
        let session = // prepare session
        return session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: SomeTokenObject.self, decoder: JSONDecoder())
            // here I wanted to save token and continue with 
            // the previous request 
            // but using .map, .flatMap, .compactMap will not return needed publisher
            // the error message I'll post below
            .map {
                // update token with $0
                // and continue with the main request
            }
    } else {
        return upload() // this will return AnyPublisher<T, Error> so it's ok here
    }
}

This error I get when using .flatMap Cannot convert return expression of type 'Publishers.FlatMap<AnyPublisher<T, Error>, Publishers.Decode<Publishers.MapKeyPath<URLSession.DataTaskPublisher, JSONDecoder.Input>, SomeTokenObject, JSONDecoder>>' (aka 'Publishers.FlatMap<AnyPublisher<T, Error>, Publishers.Decode<Publishers.MapKeyPath<URLSession.DataTaskPublisher, Data>, SomeTokenObject, JSONDecoder>>') to return type 'AnyPublisher<T, Error>'
And similar for .map.

I added another function that was returning AnyPublisher<SomeTokenObject, Error> and thought to use inside shouldUpdateToken like that

func upload<T: Decodable>(some parameters here) -> AnyPublisher<T, Error> {
    if shouldUpdateToken {
        return refreshToken() // returns AnyPublisher<Void, Error>
            // now I need to continue with original request
            // and I'd like to use something like
            .flatMap { result -> AnyPublisher<T, Error>
                upload()
            }
            // but using .map, .flatMap, .compactMap will not return needed publisher
            // the error message I'll post below
            
    } else {
        return upload() // this will return AnyPublisher<T, Error> so it's ok here
    }
}

for flatMap: Cannot convert return expression of type 'Publishers.FlatMap<AnyPublisher<T, Error>, AnyPublisher<Void, Error>>' to return type 'AnyPublisher<T, Error>'
for map: Cannot convert return expression of type 'Publishers.Map<AnyPublisher<Void, Error>, AnyPublisher<T, Error>>' to return type 'AnyPublisher<T, Error>'

Maybe I need to change to another approach? I have a lot of requests all around the app so updating the token in one place is a good idea, but how can it be done?

Here is the refreshToken() function

func refreshToken() -> AnyPublisher<Void, Error> {
        let request = ...
        let session = ...
        return session.dataTaskPublisher(for: request)
            .map(\.data)
            .decode(type: SomeTokenObject.self, decoder: JSONDecoder())
            .map {
                // saved new token
            }
            .eraseToAnyPublisher()
    }

Solution

  • You're almost there. You need to eraseToAnyPublisher() to type-erase the returned publisher.

    Remember, that an operator like .flatMap (or .map and others) return their own publisher, like the type you see in the error Publishers.FlatMap<AnyPublisher<T, Error>, AnyPublisher<Void, Error>> - you need to type-erase that:

    func upload<T: Decodable>(some parameters here) -> AnyPublisher<T, Error> {
        if shouldUpdateToken {
            return refreshToken() // returns AnyPublisher<Void, Error>
                .flatMap { _ -> AnyPublisher<T, Error> in
                    upload()
                }
                .eraseToAnyPublisher() // <- type-erase here
                
        } else {
            return upload() // actually "return"
        }
    }
    

    (and make sure that you're not constantly calling the same upload function recursively without any stop condition)