简体   繁体   English

使用 Combine 的 Future 在 Swift 中复制异步等待

[英]Using Combine's Future to replicate async await in Swift

I am creating a Contact Class to fetch user's phoneNumbers asynchronously.我正在创建一个联系人类来异步获取用户的电话号码。

I created 3 functions that leveraged on the new Combine framework's Future.我创建了 3 个利用新组合框架的 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))
        }
}

Now how do I combine these 3 Future into a single future?现在我如何将这 3 个未来组合成一个未来?

1) Check if permission is available 1)检查权限是否可用

2) If true fetchContacts asynchronously 2) 如果真 fetchContacts 异步

3) If false requestAccess asynchronously then fetchContacts asynchronously 3) 如果异步 requestAccess 为 false,则异步获取 fetchContacts

Any tips or tricks of how you will handle this better are also welcomed也欢迎您提供有关如何更好地处理此问题的任何提示或技巧

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

Future is a Publisher.未来是出版商。 To chain Publishers, use .flatMap .要链接发布者,请使用.flatMap

However, there is no need to chain futures in your use case, because there is only one asynchronous operation, namely the call to requestAccess .但是,在您的用例中不需要链接期货,因为只有一个异步操作,即对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.如果你想封装一个可能抛出错误的操作的结果,比如你的fetchContacts ,你想要返回的不是一个 Future 而是一个结果。

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

In checkAccess , we look to see whether we have authorization.checkAccess ,我们查看是否有授权。 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) means we have authorization .success(true)表示我们有授权

  • .success(false) means we don't have authorization but we can ask for it .success(false)表示我们没有授权,但我们可以要求它

  • .failure means don't have authorization and there is no point going on; .failure意味着没有授权,没有任何意义; 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 embodies the only asynchronous operation, namely requesting access from the user. requestAccessFuture体现了唯一的异步操作,即请求用户访问。 So I generate a Future.所以我生成了一个 Future。 There are only two possibilities: either we will get an error or we will get a Bool that is true .只有两种可能性:要么我们得到一个错误,要么我们得到一个为true的 Bool 。 There are no circumstances under which we get no error but a false Bool.有没有什么情况下,我们没有错误,但一个false布尔。 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 .所以我要么用错误调用 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 is just a sample operation accessing the contacts. getMyEmailAddresses只是访问联系人的示例操作。 Such an operation can throw, so I express it once again as a Result.这样的操作可以抛出,所以我再次将其表示为 Result。

Okay, now we're ready to build the pipeline!好的,现在我们准备好构建管道了! Here we go.开始了。

self.checkAccess().publisher

Our call to checkAccess yields a Result.我们对checkAccess调用会产生一个结果。 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.如果 Result 没有得到错误,这个发布者将发出一个 Bool 值。 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
    }
}

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.但如果它是假的,我们需要获取我们的 Future 并发布它。 The way you publish a publisher is with .flatMap ;您发布发布者的方式是使用.flatMap so if gotAccess is false, we fetch our Future and return it.所以如果gotAccess为假,我们获取我们的 Future 并返回它。 But what if gotAccess is true?但是如果gotAccess为真呢? We still have to return a publisher, and it needs to be of the same type as our Future.我们仍然需要返回一个发布者,它需要与我们的 Future 类型相同。 It doesn't actually have to be a Future, because we can erase to AnyPublisher.它实际上不必Future,因为我们可以擦除 AnyPublisher。 But it must be of the same types , namely Bool and Error.但它必须是相同的类型,即 Bool 和 Error。

So we create a Just and return it.所以我们创建一个 Just 并返回它。 In particular, we return Just(true) , to indicate that we are authorized.特别是,我们返回Just(true) ,以表明我们已获得授权。 But we have to jump through some hoops to map the error type to Error, because a Just's error type is Never.但是我们必须跳过一些障碍才能将错误类型映射到 Error,因为 Just 的错误类型是 Never。 I do that by applying setFailureType(to:) .我通过应用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
}

If we receive true at this point, we are authorized, so we call getMyEmailAddress and return the result, which, you recall, is a Result.如果此时我们收到true ,则我们已获得授权,因此我们调用getMyEmailAddress并返回结果,您还记得,这是一个 Result。 If we receive false , we want to do nothing;如果我们收到false ,我们什么都不想做; 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".但是我们不允许从map返回任何内容,因此我们使用compactMap代替,这允许我们返回nil表示“什么都不做”。 Therefore, if we got an error instead of a Bool, the error will just pass on down the pipeline unchanged.因此,如果我们得到一个错误而不是一个 Bool,那么这个错误就会原封不动地沿管道传递下去。

.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.为了说明这一点,我只是通过回到主线程并打印出管道中的内容来实现这一点。


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 .这个描述似乎不足以让一些读者了解这个想法,所以我在https://github.com/mattneub/CombineAuthorization 上发布了一个实际的示例项目。

You can use this framework for Swift coroutines - https://github.com/belozierov/SwiftCoroutine您可以将此框架用于 Swift 协程 - https://github.com/belozierov/SwiftCoroutine

When you call await it doesn't block the thread but only suspends coroutine, so you can use it in the main thread as well.当您调用 await 它不会阻塞线程而只会挂起协程,因此您也可以在主线程中使用它。

DispatchQueue.main.startCoroutine {
    let future = checkContactsAccess()
    let coFuture = future.subscribeCoFuture()
    let success = try coFuture.await()

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM