使用 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()

}