将异步方法转换为组合

swa*_*sta 8 swift combine

我正试图将我的头围绕在 Combine 上。

这是我想转换为Combine 的方法,以便它返回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))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

整个过程在单独的调度队列上执行,因为从 Keychain 读取和解密可能很慢。

我第一次尝试拥抱结合

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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()
}
Run Code Online (Sandbox Code Playgroud)

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

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

Rob*_*Rob 4

假设您\xe2\x80\x99已经重构readTokenFromKeyChain, decrypt, 并fetchToken返回AnyPublisher<String, Error>自身,那么您可以执行以下操作:

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

这将读取钥匙串,如果成功,则解密它,如果\xe2\x80\x99不成功,它将调用fetchToken. 完成所有这些后,它将确保最终结果传递到主队列上。

\n\n
\n\n

我认为 \xe2\x80\x99 是正确的一般模式。现在,让我们谈谈这个dispatchQueue:坦率地说,我不确定我是否看到这里有任何保证在后台线程上运行的东西,但让 \xe2\x80\x99s 想象你想要的要在后台队列中启动它,然后,您readTokenFromKeyChain可以将其分派到后台队列:

\n\n
func readTokenFromKeyChain() -> AnyPublisher<String, Error> {\n    dispatchQueue.publisher { promise in\n        let query: [CFString: Any] = [\n            kSecReturnData: true,\n            kSecClass: kSecClassGenericPassword,\n            kSecAttrAccount: "token",\n            kSecAttrService: Bundle.main.bundleIdentifier!]\n\n        var extractedData: AnyObject?\n        let status = SecItemCopyMatching(query as CFDictionary, &extractedData)\n\n        if\n            status == errSecSuccess,\n            let retrievedData = extractedData as? Data,\n            let string = String(data: retrievedData, encoding: .utf8)\n        {\n            promise(.success(string))\n        } else {\n            promise(.failure(TokenError.failure))\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

顺便说一句,\xe2\x80\x99s 使用一个简单的小方法,publisher我添加到DispatchQueue

\n\n
extension DispatchQueue {\n    /// Dispatch block asynchronously\n    /// - Parameter block: Block\n\n    func publisher<Output, Failure: Error>(_ block: @escaping (Future<Output, Failure>.Promise) -> Void) -> AnyPublisher<Output, Failure> {\n        Future<Output, Failure> { promise in\n            self.async { block(promise) }\n        }.eraseToAnyPublisher()\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
func fetchToken() -> AnyPublisher<String, Error> {\n    let request = ...\n\n    return URLSession.shared\n        .dataTaskPublisher(for: request)\n        .map { $0.data }\n        .decode(type: ResponseObject.self, decoder: JSONDecoder())\n        .map { $0.payload.token }\n        .eraseToAnyPublisher()\n}\n
Run Code Online (Sandbox Code Playgroud)\n