将异步方法转换为 Combine

Translating async method into Combine

我正在努力思考 Combine。

这是一个我想转换成 Combine 的方法,这样它就可以 return AnyPublisher。

func getToken(completion: @escaping (Result<String, Error>) -> Void) {
    dispatchQueue.async {
        do {
            if let localEncryptedToken = try self.readTokenFromKeychain() {
                let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
                DispatchQueue.main.async {
                    completion(.success(decryptedToken))
                }
            } else {
                self.fetchToken(completion: completion)
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

整个事情在一个单独的调度队列上执行,因为从钥匙串读取和解密可能很慢。

我第一次尝试拥抱Combine

func getToken() -> AnyPublisher<String, Error> {
    do {
        if let localEncryptedToken = try readTokenFromKeychain() {
            let decryptedToken = try tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
            return Result.success(decryptedToken).publisher.eraseToAnyPublisher()
        } else {
            return fetchToken() // also rewritten to return AnyPublisher<String, Error>
        }
    } catch {
        return Result.failure(error).publisher.eraseToAnyPublisher()
    }
}

但是我如何将读取从钥匙串和解密移动到单独的队列?它可能应该看起来像

func getToken() -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        self.dispatchQueue.async {
            do {
                if let localEncryptedToken = try self.readTokenFromKeychain() {
                    let decryptedToken = try self.tokenCryptoHelper.decrypt(encryptedToken: localEncryptedToken)
                    promise(.success(decryptedToken))
                } else {
                    // should I fetchToken().sink here?
                }
            } catch {
                promise(.failure(error))
            }
        }    
    }.eraseToAnyPublisher()
}

我如何从我的私有方法调用中 return 一个发布者? (见代码注释)

有没有更漂亮的解决方案?

我想我可以找到解决办法


private func readTokenFromKeychain() -> AnyPublisher<String?, Error> {
    ...
}

func getToken() -> AnyPublisher<String, Error> {
    return readTokenFromKeychain()
        .flatMap { localEncryptedToken -> AnyPublisher<String, Error> in
            if let localEncryptedToken = localEncryptedToken {
                return Result.success(localEncryptedToken).publisher.eraseToAnyPublisher()
            } else {
                return self.fetchToken()
            }
        }
        .flatMap {
            return self.tokenCryptoHelper.decrypt(encryptedToken: [=10=])
        }
        .subscribe(on: dispatchQueue)
        .eraseToAnyPublisher()
}

但我也必须在 getToken() return 发布者中调用函数才能很好地组合它们。 可能某处应该有错误处理,但这是我接下来要学习的东西。

假设您已经将 readTokenFromKeyChaindecryptfetchToken 重构为 return AnyPublisher<String, Error>,那么您可以执行以下操作:

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

那会读取keychain,成功就解密,不成功就调用fetchToken。完成所有这些后,它将确保最终结果传递到主队列。


我认为这是正确的一般模式。现在,让我们谈谈 dispatchQueue:坦率地说,我不确定我在这里看到的任何东西都可以在后台线程上保证 运行,但假设您想在后台队列中启动它,然后,您 readTokenFromKeyChain 可能会将其分派到后台队列:

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))
        }
    }
}

顺便说一句,这是使用一个简单的小方法,publisher 我添加到 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()
    }
}

为了完整起见,这是一个示例 fetchToken 实施:

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

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