使用 Combine 的 Future 在 Swift 中复制异步等待 [英] Using Combine's Future to replicate async await in Swift

查看:37
本文介绍了使用 Combine 的 Future 在 Swift 中复制异步等待的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在创建一个联系人类来异步获取用户的电话号码.

I am creating a Contact Class to fetch user's phoneNumbers asynchronously.

我创建了 3 个函数来利用新的 Combine 框架的 Future.

I created 3 functions that leveraged on the new Combine framework's 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?

Now how do I combine these 3 Future into a single future?

1) 检查权限是否可用

1) Check if permission is available

2) 如果为真 fetchContacts 异步

2) If true fetchContacts asynchronously

3) 如果 requestAccess 异步为 false,则异步 fetchContacts

3) If false requestAccess asynchronously then fetchContacts asynchronously

也欢迎您提供有关如何更好地处理此问题的任何提示或技巧

Any tips or tricks of how you will handle this better are also welcomed

func getPhoneNumbers() -> Future<[String], Error> {
...
}

推荐答案

Future is a Publisher.要链接发布者,请使用 .flatMap.

Future is a Publisher. To chain Publishers, use .flatMap.

但是,在您的用例中不需要链接 futures,因为只有一个异步操作,即对 requestAccess 的调用.如果你想封装一个可能抛出错误的操作的结果,比如你的 fetchContacts,你想要返回的不是 Future 而是 Result.

However, there is no need to chain futures in your use case, because there is only one asynchronous operation, namely the call to requestAccess. If you want to encapsulate the result of an operation that might throw an error, like your fetchContacts, what you want to return is not a Future but a Result.

为了说明,我将创建一个可能的管道来执行您所描述的操作.在整个讨论过程中,我将首先展示一些代码,然后按该顺序讨论该代码.

To illustrate, I'll create a possible pipeline that does what you describe. Throughout the discussion, I'll first show some code, then discuss that code, in that order.

首先,我将准备一些我们可以在此过程中调用的方法:

First, I'll prepare some methods we can call along the way:

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中,我们查看是否有授权.感兴趣的只有两种情况;要么我们被授权,在这种情况下我们可以继续访问我们的联系人,或者我们不确定,在这种情况下我们可以要求用户授权.其他可能性无关紧要:我们知道我们没有授权,我们不能请求它.因此,正如我之前所说,我将结果描述为结果:

In checkAccess, we look to see whether we have authorization. There are only two cases of interest; either we are authorized, in which case we can proceed to access our contacts, or we are not determined, in which case we can ask the user for authorization. The other possibilities are of no interest: we know we have no authorization and we cannot request it. So I characterize the result, as I said earlier, as a Result:

  • .success(true) 表示我们有授权

.success(false) 表示我们没有授权,但我们可以要求

.success(false) means we don't have authorization but we can ask for it

.failure 表示没有授权,没有任何意义;我将其设为自定义错误,以便我们可以将其扔到我们的管道中,从而提前完成管道.

.failure means don't have authorization and there is no point going on; I make this a custom Error so we can throw it in our pipeline and thus complete the pipeline prematurely.

好的,进入下一个功能.

OK, on to the next function.

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 体现了唯一的异步操作,即请求用户访问.所以我生成了一个 Future.只有两种可能性:要么我们得到一个错误,要么我们得到一个 true 的 Bool.在任何情况下,我们都不会得到错误,但会出现 false Bool.所以我要么用错误调用 promise 的失败,要么用 Bool 调用它的成功,我碰巧知道它总是 true.

requestAccessFuture embodies the only asynchronous operation, namely requesting access from the user. So I generate a Future. There are only two possibilities: either we will get an error or we will get a Bool that is true. There are no circumstances under which we get no error but a false Bool. So I either call the promise's failure with the error or I call its success with the Bool, which I happen to know will always be 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.

getMyEmailAddresses is just a sample operation accessing the contacts. Such an operation can throw, so I express it once again as a Result.

好的,现在我们准备好构建管道了!来吧.

Okay, now we're ready to build the pipeline! Here we go.

self.checkAccess().publisher

我们对 checkAccess 的调用产生了一个结果.但是结果有发布者!所以那个发布者是我们链的开始.如果 Result 没有得到错误,这个发布者将发出一个 Bool 值.如果它确实出错,发布者会将其扔到管道中.

Our call to checkAccess yields a Result. But a Result has a publisher! So that publisher is the start of our chain. If the Result didn't get an error, this publisher will emit a Bool value. If it did get an error, the publisher will throw it down the pipeline.

.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 并返回它.但是如果 gotAccess 为真呢?我们仍然必须返回一个发布者,并且它需要与我们的 Future 类型相同.它实际上不必成为 Future,因为我们可以擦除到 AnyPublisher.但它必须是相同的类型,即 Bool 和 Error.

This is the only interesting step along the pipeline. We receive a Bool. If it is true, we have no work to do; but if it is false, we need to get our Future and publish it. The way you publish a publisher is with .flatMap; so if gotAccess is false, we fetch our Future and return it. But what if gotAccess is true? We still have to return a publisher, and it needs to be of the same type as our Future. It doesn't actually have to be a Future, because we can erase to AnyPublisher. But it must be of the same types, namely Bool and Error.

所以我们创建一个 Just 并返回它.特别是,我们返回Just(true),以表明我们已获得授权.但是我们必须跳过一些障碍才能将错误类型映射到 Error,因为 Just 的错误类型是 Never.我通过应用 setFailureType(to:) 来做到这一点.

So we create a Just and return it. In particular, we return Just(true), to indicate that we are authorized. But we have to jump through some hoops to map the error type to Error, because a Just's error type is Never. I do that by applying setFailureType(to:).

好的,剩下的就简单了.

Okay, the rest is easy.

.receive(on: DispatchQueue.global(qos: .userInitiated))

我们跳转到后台线程,这样我们就可以在不阻塞主线程的情况下与联系人存储对话.

We jump onto a background thread, so that we can talk to the contact store without blocking the main thread.

.compactMap { (auth:Bool) -> Result<[CNLabeledValue<NSString>], Error>? in
    if auth {
        return self.getMyEmailAddresses()
    }
    return nil
}

如果此时我们收到 true,则我们已获得授权,因此我们调用 getMyEmailAddress 并返回结果,您还记得,这是一个 Result.如果我们收到false,我们什么都不做;但是我们不允许从 map 返回任何内容,所以我们使用 compactMap 代替,这允许我们返回 nil 表示什么都不做";.因此,如果我们得到一个错误而不是一个布尔值,错误将原封不动地沿管道传递.

If we receive true at this point, we are authorized, so we call getMyEmailAddress and return the result, which, you recall, is a Result. If we receive false, we want to do nothing; but we are not allowed to return nothing from map, so we use compactMap instead, which allows us to return nil to mean "do nothing". Therefore, if we got an error instead of a Bool, the error will just pass on down the pipeline unchanged.

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

我们已经完成了,所以只需要准备好接收来自管道的错误或电子邮件(包含在结果中).举例来说,我这样做是为了简单地回到主线程并打印出管道中的内容.

We've finished, so it remains only to get ready to receive the error or the emails (wrapped in a Result) that have come down the pipeline. I do this, by way of illustration, simply by getting back onto the main thread and printing out what comes down the pipeline at us.

这个描述似乎不足以让一些读者有想法,所以我在 https://github.com/mattneub/CombineAuthorization.

This description doesn't seem quite enough to give some readers the idea, so I've posted an actual example project at https://github.com/mattneub/CombineAuthorization.

这篇关于使用 Combine 的 Future 在 Swift 中复制异步等待的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆