Использование Combine's Future для репликации asyn c, ожидающих в Swift - PullRequest
0 голосов
/ 26 февраля 2020

Я создаю класс контактов для асинхронной выборки телефонных номеров пользователя.

Я создал 3 функции, которые использовали Future для новой платформы Combine.

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 Будущих в одно будущее?

1) Проверьте, доступно ли разрешение

2) Если true fetchContacts асинхронно

3) Если false requestAccess асинхронно, то fetchContacts асинхронно

Любые советы или рекомендации о том, как вы справитесь с этим, также приветствуются

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

1 Ответ

2 голосов
/ 26 февраля 2020

Будущее издателя. Чтобы связать издателей в цепочку, используйте .flatMap.

. Однако в вашем случае использования нет необходимости в цепочке фьючерсов, поскольку существует только одна асинхронная операция, а именно вызов requestAccess. Если вы хотите инкапсулировать результат операции, которая может выдать ошибку, например ваш fetchContacts, то, что вы хотите вернуть, - это не будущее, а результат.

Для иллюстрации я создам возможную Трубопровод, который делает то, что вы описываете. В ходе обсуждения я сначала покажу некоторый код, а затем обсудю этот код в указанном порядке.

Сначала я подготовлю несколько методов, которые мы можем вызывать по пути:

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 мы смотрим, есть ли у нас авторизация. Есть только два случая, представляющих интерес; либо мы авторизованы, и в этом случае мы можем перейти к доступу к нашим контактам, либо мы не определены, и в этом случае мы можем запросить у пользователя авторизацию. Другие возможности не представляют интереса: мы знаем, что у нас нет разрешения и мы не можем его запросить. Поэтому я характеризую результат, как я уже говорил ранее, как результат:

  • .success(true) означает, что у нас есть разрешение

  • .success(false) означает у нас нет авторизации, но мы можем запросить ее

  • .failure означает, что у нас нет авторизации, и нет никакого смысла в этом; Я делаю это пользовательскую ошибку, чтобы мы могли выбросить ее в наш конвейер и, таким образом, преждевременно завершить конвейер.

OK, перейдем к следующей функции.

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 воплощает единственную асинхронную операцию, а именно запрос доступа от пользователя. Так что я создаю будущее. Есть только две возможности: либо мы получим ошибку, либо мы получим Bool, равный true. Нет обстоятельств, при которых мы не получаем никакой ошибки, кроме false 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 - это просто пример операции доступа к контакты. Такую операцию можно выполнить, поэтому я express еще раз в качестве результата.

Хорошо, теперь мы готовы построить конвейер! Здесь мы go.

self.checkAccess().publisher

Наш вызов checkAccess дает Результат. Но у Результата есть издатель! Так что этот издатель - начало нашей цепочки. Если Результат не получил ошибку, этот издатель выдаст значение 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
    }
}

Это единственный интересный шаг по конвейеру. Мы получаем Бул. Если это правда, у нас нет работы; но если оно ложно, нам нужно получить наше будущее и опубликовать его. То, как вы публикуете sh издателя, - .flatMap; так что если gotAccess ложно, мы получаем наше будущее и возвращаем его. Но что, если gotAccess верно? Мы все еще должны вернуть издателя, и он должен быть того же типа, что и наше будущее. На самом деле он не должен быть Future, потому что мы можем стереть его в AnyPublisher. Но это должны быть те же типы , а именно Bool и Error.

Поэтому мы создаем Just и возвращаем его. В частности, мы возвращаем Just(true), чтобы указать, что мы авторизованы. Но мы должны перепрыгнуть через некоторые обручи, чтобы сопоставить тип ошибки с ошибкой, потому что тип ошибки Just - Никогда. Я делаю это, применяя 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 и возвращаем результат, который, как вы помните, является Результатом. Если мы получим 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)
    }
})

Мы закончили, так что осталось только подготовиться к получению ошибки или электронных писем (обернутых в Результат), которые пришли по конвейеру. Я делаю это в качестве иллюстрации, просто возвращаясь к основному потоку и распечатывая то, что происходит у нас по конвейеру.

...