将异步方法转换为 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 发布者中调用函数才能很好地组合它们。
可能某处应该有错误处理,但这是我接下来要学习的东西。
假设您已经将 readTokenFromKeyChain
、decrypt
和 fetchToken
重构为 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()
}
我正在努力思考 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 发布者中调用函数才能很好地组合它们。
可能某处应该有错误处理,但这是我接下来要学习的东西。
假设您已经将 readTokenFromKeyChain
、decrypt
和 fetchToken
重构为 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()
}