Translating async method into Combine

Assuming you’ve refactored readTokenFromKeyChain, decrypt, and fetchToken to return AnyPublisher<String, Error> themselves, you can then do:

func getToken() -> AnyPublisher<String, Error> {
    readTokenFromKeyChain()
        .flatMap { self.tokenCryptoHelper.decrypt(encryptedToken: $0) }
        .catch { _ in self.fetchToken() }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
}

That will read the keychain, if it succeeded, decrypt it, and if it didn’t succeed, it will call fetchToken. And having done all of that, it will make sure the final result is delivered on the main queue.


I think that’s the right general pattern. Now, let's talk about that dispatchQueue: Frankly, I’m not sure I’m seeing anything here that warrants running on a background thread, but let’s imagine you wanted to kick this off in a background queue, then, you readTokenFromKeyChain might dispatch that to a background queue:

func readTokenFromKeyChain() -> AnyPublisher<String, Error> {
    dispatchQueue.publisher { promise in
        let query: [CFString: Any] = [
            kSecReturnData: true,
            kSecClass: kSecClassGenericPassword,
            kSecAttrAccount: "token",
            kSecAttrService: Bundle.main.bundleIdentifier!]

        var extractedData: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &extractedData)

        if
            status == errSecSuccess,
            let retrievedData = extractedData as? Data,
            let string = String(data: retrievedData, encoding: .utf8)
        {
            promise(.success(string))
        } else {
            promise(.failure(TokenError.failure))
        }
    }
}

By the way, that’s using a simple little method, publisher that I added to DispatchQueue:

extension DispatchQueue {
    /// Dispatch block asynchronously
    /// - Parameter block: Block

    func publisher<Output, Failure: Error>(_ block: @escaping (Future<Output, Failure>.Promise) -> Void) -> AnyPublisher<Output, Failure> {
        Future<Output, Failure> { promise in
            self.async { block(promise) }
        }.eraseToAnyPublisher()
    }
}

For the sake of completeness, this is a sample fetchToken implementation:

func fetchToken() -> AnyPublisher<String, Error> {
    let request = ...

    return URLSession.shared
        .dataTaskPublisher(for: request)
        .map { $0.data }
        .decode(type: ResponseObject.self, decoder: JSONDecoder())
        .map { $0.payload.token }
        .eraseToAnyPublisher()
}

Tags:

Swift

Combine