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

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

问题描述

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

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)如果为true,则fetchContacts异步

2) If true fetchContacts asynchronously

3)如果false requestAccess异步,则异步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> {
...
}

推荐答案

未来是发布者.要链接发布者,请使用.flatMap.

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

但是,在您的用例中不需要链接期货,因为只有一个异步操作,即对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体现了唯一的异步操作,即请求用户访问.所以我创造了一个未来.只有两种可能性:我们将得到一个错误,或者我们将得到一个true的布尔值.除了false Bool,在任何情况下我们都不会出错.因此,我将错误称为Promise的失败,或者将错误称为Bool,将其称为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只是访问联系人的示例操作.这样的操作可能会抛出,因此我再次将其表示为结果.

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的调用产生一个结果.但是结果有发布者!因此,发布者是我们链条的开始.如果结果没有出现错误,则此发布者将发出Bool值.如果它 did 出现错误,则发布者会将其丢弃.

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

这是管道中唯一有趣的步骤.我们收到一个布尔.如果这是真的,那么我们就没有工作要做.但是如果它是错误的,我们需要获取并发布它.您使用.flatMap来发布发布者.因此,如果gotAccess为false,我们将获取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并返回结果,您还记得这是一个结果.如果收到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.

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

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