使用 Combine 的 Future 在 Swift 中复制异步等待(async await)的功能

4

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

我创建了三个函数,利用了新的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))
        }
}

现在,我该如何将这三个 Future 合并为一个 Future 呢?
1)检查权限是否可用 2)如果为真,则异步获取联系人 3)如果为假,则先异步请求访问权限,然后再异步获取联系人
欢迎分享你更好的处理方法。
func getPhoneNumbers() -> Future<[String], Error> {
...
}

请展示实际的代码,而不是三个省略号。 - matt
我更新了实际代码@matt。 - kelvinleeweisern
你应该直接调用 resolve。不要返回调用它的“结果”。删除所有的 return - matt
另外,您的授权状态代码有误。只有在“未确定”情况下才能继续请求授权,但您没有区分这种情况。 - matt
同时调用 authorizationStatus(for:) 并不是异步的,因此将其转换为 Future 没有意义。 - matt
好的,我提供了一些示例代码,请看看是否有帮助。 - matt
2个回答

10

Future是一个发布者。如果要链式调用多个发布者,可以使用.flatMap

但在您的用例中没有必要链式调用Future,因为只有一个异步操作,即对requestAccess的调用。如果您想封装可能会引发错误的操作的结果,比如fetchContacts,您想要返回的不是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代表唯一的异步操作,即从用户那里请求访问权限。因此我生成了一个 Future。只有两种可能性:要么会出现错误,要么会得到一个为true的 Bool 值。没有任何情况下我们会得到没有错误却是false的 Bool 值。所以我要么使用错误调用 promise 的失败方法,要么使用 Bool 调用其成功方法。我碰巧知道这个 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)具有发布者(publisher)!因此,该发布者是我们链条的起点。如果结果(Result)没有出现错误,则此发布者将发出布尔值。如果它确实出现了错误,则发布者将将其沿着管道抛出。

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

这是管道中唯一有趣的步骤。我们收到一个布尔值,如果它是 true,那么我们就没有工作要做;但如果它是 false,我们需要获取我们的 Future 并发布它。你使用 .flatMap 来发布一个发布者;所以如果 gotAccess 是 false,我们获取我们的 Future 并返回它。但是如果 gotAccess 是 true 呢?我们仍然必须返回一个发布者,并且它的类型必须与我们的 Future 相同。它实际上不必是一个 Future,因为我们可以擦除为 AnyPublisher。但它必须是相同的类型,即 Bool 和 Error。

因此,我们创建了一个 Just 并将其返回。特别是,我们返回 Just(true),表示我们已经获得了授权。但我们必须通过应用 setFailureType(to:) 来将错误类型映射到 Error,因为 Just 的错误类型是 Never。

好了,剩下的都很容易了。

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

我们跳到后台线程,以便可以与联系人存储库进行通信,而不会阻塞主线程。

.compactMap { (auth:Bool) -> Result<[CNLabeledValue<NSString>], Error>? in
    if auth {
        return self.getMyEmailAddresses()
    }
    return nil
}
如果此时我们收到了 true ,那么我们就有授权,所以我们调用 getMyEmailAddress 并返回结果,你可能会记得这个结果是一个 Result。如果我们收到了 false ,那么我们不想做任何事情;但是我们不能从 map 返回空值,因此我们使用 compactMap,它允许我们返回 nil 来表示“不做任何事情”。因此,如果我们收到的不是 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)
    }
})

我们已经完成了,现在只需要准备接收错误或已传输到管道中的电子邮件(封装在Result中)。我通过示例方式来做到这一点,只需返回到主线程并打印出传输到我们这里的内容。


这个描述似乎不足以让某些读者理解,因此我在https://github.com/mattneub/CombineAuthorization上发布了一个实际的示例项目。


嘿,马特,谢谢你的回答。我使用 future 访问联系人的原因是为了异步运行代码片段。但在不同的线程中接收也可以。 至于错误,我相信有一个 .setFailureType(to: Error.self) 可以做到这一点。 - kelvinleeweisern
好的,我会尝试一下,谢谢!——嗯,完美,我会修改代码的那一部分。 - matt
我的观点是,仅仅把某物变成Future,并不能在任何有用的方面使其异步化;特别是当你与联系人存储库通信时,它并没有帮助你进入后台线程,这是_必要的_。我希望这个例子已经向你展示了flatMap如何帮助链接发布者,这似乎是你正在苦苦思考的难题的一部分。你也会注意到,我的答案在所有情况下都能顺利运行:我们已经得到授权;我们未被授权,但我们可以请求授权;还有,我们未被授权,并且我们无法请求授权。 - matt

-1

你可以使用这个框架来进行 Swift 协程 - https://github.com/belozierov/SwiftCoroutine

当你调用 await 时,它不会阻塞线程,而只是暂停协程,因此你也可以在主线程中使用。

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

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接