8

我正试图将我的头包裹在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))
            }
        }
    }
}

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

我第一次尝试拥抱联合

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

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

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

4

2 回答 2

4

假设您已经重构readTokenFromKeyChain,decryptfetchToken返回AnyPublisher<String, Error>自己,那么您可以执行以下操作:

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

这将读取钥匙串,如果成功,解密它,如果没有成功,它会调用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 { $0.data }
        .decode(type: ResponseObject.self, decoder: JSONDecoder())
        .map { $0.payload.token }
        .eraseToAnyPublisher()
}
于 2019-08-12T04:25:11.963 回答
1

我想我可以找到解决方案


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: $0)
        }
        .subscribe(on: dispatchQueue)
        .eraseToAnyPublisher()
}

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

于 2019-08-10T14:35:56.217 回答