使用 Combine 的 Future 在 Swift 中复制异步等待
Using Combine's Future to replicate async await in Swift
我正在创建联系人 Class 以异步获取用户的电话号码。
我创建了 3 个利用新 Combine 框架的 Future 的函数。
func checkContactsAccess() -> Future<Bool, Never> {
Future { resolve in
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
switch authorizationStatus {
case .authorized:
return resolve(.success(true))
default:
return resolve(.success(false))
}
}
}
func requestAccess() -> Future<Bool, Error> {
Future { resolve in
CNContactStore().requestAccess(for: .contacts) { (access, error) in
guard error == nil else {
return resolve(.failure(error!))
}
return resolve(.success(access))
}
}
}
func fetchContacts() -> Future<[String], Error> {
Future { resolve in
let contactStore = CNContactStore()
let keysToFetch = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey,
CNContactEmailAddressesKey,
CNContactThumbnailImageDataKey] as [Any]
var allContainers: [CNContainer] = []
do {
allContainers = try contactStore.containers(matching: nil)
} catch {
return resolve(.failure(error))
}
var results: [CNContact] = []
for container in allContainers {
let fetchPredicate = CNContact.predicateForContactsInContainer(withIdentifier: container.identifier)
do {
let containerResults = try contactStore.unifiedContacts(matching: fetchPredicate, keysToFetch: keysToFetch as! [CNKeyDescriptor])
results.append(contentsOf: containerResults)
} catch {
return resolve(.failure(error))
}
}
var phoneNumbers: [String] = []
for contact in results {
for phoneNumber in contact.phoneNumbers {
phoneNumbers.append(phoneNumber.value.stringValue.replacingOccurrences(of: " ", with: ""))
}
}
return resolve(.success(phoneNumbers))
}
}
现在如何将这 3 个 Future 组合成一个 Future?
1) 检查权限是否可用
2) 如果为真,则异步获取联系人
3) 如果 false requestAccess 异步则 fetchContacts 异步
也欢迎您提供有关如何更好地处理此问题的任何提示或技巧
func getPhoneNumbers() -> Future<[String], Error> {
...
}
未来是一个发布者。要链接发布者,请使用 .flatMap
.
但是,在您的用例中不需要链接 futures,因为只有一个异步操作,即对 requestAccess
的调用。如果你想封装一个可能引发错误的操作的结果,比如你的 fetchContacts
,你想要的 return 不是 Future 而是 Result。
为了说明,我将创建一个可能的管道来执行您描述的操作。在整个讨论过程中,我将首先展示一些代码,然后按顺序讨论该代码。
首先,我会准备一些我们可以一路调用的方法:
func checkAccess() -> Result<Bool, Error> {
Result<Bool, Error> {
let status = CNContactStore.authorizationStatus(for:.contacts)
switch status {
case .authorized: return true
case .notDetermined: return false
default:
enum NoPoint : Error { case userRefusedAuthorization }
throw NoPoint.userRefusedAuthorization
}
}
}
在checkAccess
中,我们看看是否有权限。只有两种情况值得关注;我们要么被授权,在这种情况下我们可以继续访问我们的联系人,要么我们不确定,在这种情况下我们可以要求用户授权。其他可能性没有兴趣:我们知道我们没有授权,我们不能请求它。所以我把结果定性,前面说了,就是一个Result:
.success(true)
表示我们有权限
.success(false)
表示我们没有授权,但我们可以要求
.failure
表示没有权限,没有意义;我将其设为自定义错误,以便我们可以将其放入我们的管道中,从而提前完成管道。
好的,进入下一个功能。
func requestAccessFuture() -> Future<Bool, Error> {
Future<Bool, Error> { promise in
CNContactStore().requestAccess(for:.contacts) { ok, err in
if err != nil {
promise(.failure(err!))
} else {
promise(.success(ok)) // will be true
}
}
}
}
requestAccessFuture
体现了唯一的异步操作,即向用户请求访问。所以我产生了一个未来。只有两种可能性:要么我们会得到一个错误,要么我们会得到一个 true
的布尔值。在任何情况下我们都会得到一个 false
Bool 错误。所以我要么用错误来调用 promise 的失败,要么用 Bool 来调用它的成功,我碰巧知道它总是 true
.
func getMyEmailAddresses() -> Result<[CNLabeledValue<NSString>], Error> {
Result<[CNLabeledValue<NSString>], Error> {
let pred = CNContact.predicateForContacts(matchingName:"John Appleseed")
let jas = try CNContactStore().unifiedContacts(matching:pred, keysToFetch: [
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor
])
guard let ja = jas.first else {
enum NotFound : Error { case oops }
throw NotFound.oops
}
return ja.emailAddresses
}
}
getMyEmailAddresses
只是访问联系人的示例操作。这样的操作会抛出,所以我再次表达为一个Result。
好的,现在我们准备好构建管道了!开始了。
self.checkAccess().publisher
我们对 checkAccess
的调用产生了一个结果。但是 Result 有一个发布者!因此,发布者是我们链条的起点。如果 Result 没有出错,则此发布者将发出一个 Bool 值。如果它 确实 出错,发布者将把它扔到管道中。
.flatMap { (gotAccess:Bool) -> AnyPublisher<Bool, Error> in
if gotAccess {
let just = Just(true).setFailureType(to:Error.self).eraseToAnyPublisher()
return just
} else {
let req = self.requestAccessFuture().eraseToAnyPublisher()
return req
}
}
这是管道中唯一有趣的步骤。我们收到一个布尔值。如果是真的,我们就没有工作可做;但如果它是假的,我们需要得到我们的 Future 并发布它。发布发布者的方式是 .flatMap
;因此,如果 gotAccess
为假,我们将获取我们的 Future 并 return 它。但是,如果 gotAccess
为真呢?我们仍然 必须return 一个发布者,并且它需要与我们的 Future 具有相同的类型。它实际上不必 成为 一个 Future,因为我们可以擦除到 AnyPublisher。但它必须是相同的类型,即Bool和Error。
所以我们创建了一个 Just 并 return 它。特别是我们returnJust(true)
,表示我们是授权的。但是我们必须跳过一些步骤才能将错误类型映射到 Error,因为 Just 的错误类型是 Never。我通过应用 setFailureType(to:)
.
来做到这一点
好了,剩下的就简单了
.receive(on: DispatchQueue.global(qos: .userInitiated))
我们跳转到后台线程,这样我们就可以在不阻塞主线程的情况下与联系人存储对话。
.compactMap { (auth:Bool) -> Result<[CNLabeledValue<NSString>], Error>? in
if auth {
return self.getMyEmailAddresses()
}
return nil
}
如果此时我们收到 true
,我们就被授权了,所以我们调用 getMyEmailAddress
和 return 结果,你还记得,这是一个结果。如果我们收到 false
,我们什么也不想做;但是我们不允许 map
什么都不做 return,所以我们使用 compactMap
来代替,这允许我们 return nil
表示“什么都不做”。因此,如果我们得到的是错误而不是布尔值,错误将原样传递到管道中。
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case let .failure(err) = completion {
print("error:", err)
}
}, receiveValue: { result in
if case let .success(emails) = result {
print("got emails:", emails)
}
})
我们已经完成,所以剩下的只是准备好接收错误或从管道中下来的电子邮件(包含在结果中)。我这样做,通过说明的方式,简单地回到主线程并打印出管道中的内容。
这个描述似乎不足以让一些读者了解,所以我在 https://github.com/mattneub/CombineAuthorization 上发布了一个实际的示例项目。
您可以将此框架用于 Swift 协程 - https://github.com/belozierov/SwiftCoroutine
当你调用 await 时,它不会阻塞线程,只会挂起协程,所以你也可以在主线程中使用它。
DispatchQueue.main.startCoroutine {
let future = checkContactsAccess()
let coFuture = future.subscribeCoFuture()
let success = try coFuture.await()
}
我正在创建联系人 Class 以异步获取用户的电话号码。
我创建了 3 个利用新 Combine 框架的 Future 的函数。
func checkContactsAccess() -> Future<Bool, Never> {
Future { resolve in
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
switch authorizationStatus {
case .authorized:
return resolve(.success(true))
default:
return resolve(.success(false))
}
}
}
func requestAccess() -> Future<Bool, Error> {
Future { resolve in
CNContactStore().requestAccess(for: .contacts) { (access, error) in
guard error == nil else {
return resolve(.failure(error!))
}
return resolve(.success(access))
}
}
}
func fetchContacts() -> Future<[String], Error> {
Future { resolve in
let contactStore = CNContactStore()
let keysToFetch = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey,
CNContactEmailAddressesKey,
CNContactThumbnailImageDataKey] as [Any]
var allContainers: [CNContainer] = []
do {
allContainers = try contactStore.containers(matching: nil)
} catch {
return resolve(.failure(error))
}
var results: [CNContact] = []
for container in allContainers {
let fetchPredicate = CNContact.predicateForContactsInContainer(withIdentifier: container.identifier)
do {
let containerResults = try contactStore.unifiedContacts(matching: fetchPredicate, keysToFetch: keysToFetch as! [CNKeyDescriptor])
results.append(contentsOf: containerResults)
} catch {
return resolve(.failure(error))
}
}
var phoneNumbers: [String] = []
for contact in results {
for phoneNumber in contact.phoneNumbers {
phoneNumbers.append(phoneNumber.value.stringValue.replacingOccurrences(of: " ", with: ""))
}
}
return resolve(.success(phoneNumbers))
}
}
现在如何将这 3 个 Future 组合成一个 Future?
1) 检查权限是否可用
2) 如果为真,则异步获取联系人
3) 如果 false requestAccess 异步则 fetchContacts 异步
也欢迎您提供有关如何更好地处理此问题的任何提示或技巧
func getPhoneNumbers() -> Future<[String], Error> {
...
}
未来是一个发布者。要链接发布者,请使用 .flatMap
.
但是,在您的用例中不需要链接 futures,因为只有一个异步操作,即对 requestAccess
的调用。如果你想封装一个可能引发错误的操作的结果,比如你的 fetchContacts
,你想要的 return 不是 Future 而是 Result。
为了说明,我将创建一个可能的管道来执行您描述的操作。在整个讨论过程中,我将首先展示一些代码,然后按顺序讨论该代码。
首先,我会准备一些我们可以一路调用的方法:
func checkAccess() -> Result<Bool, Error> {
Result<Bool, Error> {
let status = CNContactStore.authorizationStatus(for:.contacts)
switch status {
case .authorized: return true
case .notDetermined: return false
default:
enum NoPoint : Error { case userRefusedAuthorization }
throw NoPoint.userRefusedAuthorization
}
}
}
在checkAccess
中,我们看看是否有权限。只有两种情况值得关注;我们要么被授权,在这种情况下我们可以继续访问我们的联系人,要么我们不确定,在这种情况下我们可以要求用户授权。其他可能性没有兴趣:我们知道我们没有授权,我们不能请求它。所以我把结果定性,前面说了,就是一个Result:
.success(true)
表示我们有权限.success(false)
表示我们没有授权,但我们可以要求.failure
表示没有权限,没有意义;我将其设为自定义错误,以便我们可以将其放入我们的管道中,从而提前完成管道。
好的,进入下一个功能。
func requestAccessFuture() -> Future<Bool, Error> {
Future<Bool, Error> { promise in
CNContactStore().requestAccess(for:.contacts) { ok, err in
if err != nil {
promise(.failure(err!))
} else {
promise(.success(ok)) // will be true
}
}
}
}
requestAccessFuture
体现了唯一的异步操作,即向用户请求访问。所以我产生了一个未来。只有两种可能性:要么我们会得到一个错误,要么我们会得到一个 true
的布尔值。在任何情况下我们都会得到一个 false
Bool 错误。所以我要么用错误来调用 promise 的失败,要么用 Bool 来调用它的成功,我碰巧知道它总是 true
.
func getMyEmailAddresses() -> Result<[CNLabeledValue<NSString>], Error> {
Result<[CNLabeledValue<NSString>], Error> {
let pred = CNContact.predicateForContacts(matchingName:"John Appleseed")
let jas = try CNContactStore().unifiedContacts(matching:pred, keysToFetch: [
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor
])
guard let ja = jas.first else {
enum NotFound : Error { case oops }
throw NotFound.oops
}
return ja.emailAddresses
}
}
getMyEmailAddresses
只是访问联系人的示例操作。这样的操作会抛出,所以我再次表达为一个Result。
好的,现在我们准备好构建管道了!开始了。
self.checkAccess().publisher
我们对 checkAccess
的调用产生了一个结果。但是 Result 有一个发布者!因此,发布者是我们链条的起点。如果 Result 没有出错,则此发布者将发出一个 Bool 值。如果它 确实 出错,发布者将把它扔到管道中。
.flatMap { (gotAccess:Bool) -> AnyPublisher<Bool, Error> in
if gotAccess {
let just = Just(true).setFailureType(to:Error.self).eraseToAnyPublisher()
return just
} else {
let req = self.requestAccessFuture().eraseToAnyPublisher()
return req
}
}
这是管道中唯一有趣的步骤。我们收到一个布尔值。如果是真的,我们就没有工作可做;但如果它是假的,我们需要得到我们的 Future 并发布它。发布发布者的方式是 .flatMap
;因此,如果 gotAccess
为假,我们将获取我们的 Future 并 return 它。但是,如果 gotAccess
为真呢?我们仍然 必须return 一个发布者,并且它需要与我们的 Future 具有相同的类型。它实际上不必 成为 一个 Future,因为我们可以擦除到 AnyPublisher。但它必须是相同的类型,即Bool和Error。
所以我们创建了一个 Just 并 return 它。特别是我们returnJust(true)
,表示我们是授权的。但是我们必须跳过一些步骤才能将错误类型映射到 Error,因为 Just 的错误类型是 Never。我通过应用 setFailureType(to:)
.
好了,剩下的就简单了
.receive(on: DispatchQueue.global(qos: .userInitiated))
我们跳转到后台线程,这样我们就可以在不阻塞主线程的情况下与联系人存储对话。
.compactMap { (auth:Bool) -> Result<[CNLabeledValue<NSString>], Error>? in
if auth {
return self.getMyEmailAddresses()
}
return nil
}
如果此时我们收到 true
,我们就被授权了,所以我们调用 getMyEmailAddress
和 return 结果,你还记得,这是一个结果。如果我们收到 false
,我们什么也不想做;但是我们不允许 map
什么都不做 return,所以我们使用 compactMap
来代替,这允许我们 return nil
表示“什么都不做”。因此,如果我们得到的是错误而不是布尔值,错误将原样传递到管道中。
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
if case let .failure(err) = completion {
print("error:", err)
}
}, receiveValue: { result in
if case let .success(emails) = result {
print("got emails:", emails)
}
})
我们已经完成,所以剩下的只是准备好接收错误或从管道中下来的电子邮件(包含在结果中)。我这样做,通过说明的方式,简单地回到主线程并打印出管道中的内容。
这个描述似乎不足以让一些读者了解,所以我在 https://github.com/mattneub/CombineAuthorization 上发布了一个实际的示例项目。
您可以将此框架用于 Swift 协程 - https://github.com/belozierov/SwiftCoroutine
当你调用 await 时,它不会阻塞线程,只会挂起协程,所以你也可以在主线程中使用它。
DispatchQueue.main.startCoroutine {
let future = checkContactsAccess()
let coFuture = future.subscribeCoFuture()
let success = try coFuture.await()
}