Преобразуйте подход обратного вызова в реактивный с Combine - PullRequest
0 голосов
/ 07 марта 2020

Это то, что я делаю:

-> Вход / Регистрация в Firebase с использованием FirebaseAuthentification

-> Перечисление в AuthStateDidChangeListenerHandle

-> Я храню дополнительную информацию о пользователе в Firestore, поэтому я проверяю, существует ли пользователь в Firestore

-> Если пользователь не существует, я создаю пустого пользователя

-> Если все прошло успешно, я возвращаю Future Publisher через callback ( Я тоже хочу это изменить)

Это функция checkLoginState:

func checkLoginState(completion: @escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
    self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
        guard let safeSelf = self else { return }
        completion(Future<AccountDetails,Error> { promise in
            if let user = user {
                print(user)
                print(auth)

                safeSelf.checkIfUserIsInDatabase(user: user.uid) { result in
                    switch result {
                    case .success(let isAvailable):
                        if isAvailable {
                             promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
                        } else {
                            safeSelf.createEmptyUser(user: user.uid,email: user.email) { result in
                                switch result {
                                case .success(_):
                                    promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
                                case .failure(let error):
                                    print(error)
                                }
                            }
                        }
                    case .failure(let error):
                        print(error)
                    }
                }
            } else {
                promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
            }
            }.eraseToAnyPublisher()
        )
    }
}

Это мои текущие функции:

private func checkIfUserIsInDatabase(user id: String, completion: @escaping (Result<Bool,Error>) -> Void)

private func createEmptyUser(user id: String, email:String?, completion: @escaping (Result<Bool,Error>) -> Void)

Это то, что я хочу использовать :

private func checkIfUserIsInDatabase(user id: String) -> AnyPublisher<Bool,Error>

private func createEmptyUser(user id: String) -> AnyPublisher<Bool,Error>

func checkLoginState() -> AnyPublisher<AccountDetails,Error>

У меня было что-то подобное, но оно не работает, тоже выглядит запутанно:

func checkLoginState(completion: @escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
    self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
        guard let safeSelf = self else { return }
        completion(Future<AccountDetails,Error> { promise in
            if let user = user {
                print(user)
                print(auth)

                safeSelf.checkIfUserIsInDatabase(user: user.uid)
                    .sinkToResult { value in
                        switch value {
                        case .success(let isUserInDatabase):
                            if isUserInDatabase {
                                promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
                            } else {
                                safeSelf.createEmptyUser(user: user.uid)
                                    .sinkToResult { value in
                                        switch value {
                                        case .success( _):
                                            promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
                                        case .failure(let error):
                                            print(error)
                                        }
                                }
                            }
                        case .failure(let error):
                            print(error)
                        }

                }
            } else {
                promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
            }
        }.eraseToAnyPublisher()
        )
    }
}

Ответы [ 2 ]

1 голос
/ 08 марта 2020

Итак, у вас есть некоторый тип AccountDetails:

import Combine
import FirebaseAuth

struct AccountDetails {
    var userId: String
    var name: String?
    var isLoggedIn: Bool
    var isPremiumUser: Bool
}

Давайте расширим его на init, что займет User, потому что это упростит вещи позже:

extension AccountDetails {
    init(user: User) {
        self.userId = user.uid
        self.name = user.displayName
        self.isLoggedIn = true
        self.isPremiumUser = false
    }
}

Я думаю, что вашей конечной целью является Publisher, который испускает AccountDetails. Но поскольку вошедший в систему пользователь не всегда, он должен действительно выдавать Optional<AccountDetails>, чтобы он мог выдавать nil, когда пользователь выходит из системы.

Давайте начнем с переноса API addStateDidChangeListener в Publisher. Мы не можем использовать Future для этого, потому что Future излучает не более одного выхода, но addStateDidChangeListener может генерировать несколько событий. Так что вместо этого мы будем использовать CurrentValueSubject. Это означает, что нам нужно место для хранения предмета и AuthStateDidChangeListenerHandle. Вы можете хранить их как глобальные переменные, либо в своем AppDelegate, либо там, где считаете нужным. Для этого ответа давайте создадим класс Demo для их хранения:

class Demo {
    static let shared = Demo()

    let userPublisher: AnyPublisher<User?, Error>

    private let userSubject = CurrentValueSubject<User?, Error>(nil)
    private var tickets: [AnyCancellable] = []

    private init() {
        userPublisher = userSubject.eraseToAnyPublisher()
        let handle = Auth.auth().addStateDidChangeListener { [userSubject] (_, user) in
            userSubject.send(user)
        }
        AnyCancellable { Auth.auth().removeStateDidChangeListener(handle) }
            .store(in: &tickets)
    }
}

Так что теперь вы можете получить Publisher зарегистрированного пользователя (или nil, если ни один пользователь не вошел в систему), например это:

let loggedInUserPublisher: AnyPublisher<User?, Error> = Demo.shared.userPublisher

Но вы действительно хотите издателя AccountDetails?, а не User? издателя, как это:

let accountDetailsPublisher: AnyPublisher<AccountDetails?, Error> = Demo.shared
    .accountDetailsPublisher()

Итак, нам нужно написать метод accountDetailsPublisher который отображает User? в AccountDetails?.

Если User? равен нулю, мы просто хотим испустить nil. Но если User? равно .some(user), нам нужно выполнить больше асинхронных действий: нам нужно проверить, есть ли пользователь в базе данных, и добавить пользователя, если нет. Оператор flatMap позволяет объединять асинхронные действия, но есть некоторая сложность, потому что нам нужно выполнять различные действия в зависимости от выходных данных вышестоящего издателя.

Мы действительно хотели бы скрыть сложность и просто написать это:

extension Demo {
    func loggedInAccountDetailsPublisher() -> AnyPublisher<AccountDetails?, Error> {
        return userPublisher
            .flatMap(
                ifSome: { $0.accountDetailsPublisher().map { Optional.some($0) } },
                ifNone: { Just(nil).setFailureType(to: Error.self) })
            .eraseToAnyPublisher()
    }
}

Но тогда нам нужно написать flatMap(ifSome:ifNone:). Вот оно:

extension Publisher {
    func flatMap<Wrapped, Some: Publisher, None: Publisher>(
        ifSome: @escaping (Wrapped) -> Some,
        ifNone: @escaping () -> None
    ) -> AnyPublisher<Some.Output, Failure>
        where Output == Optional<Wrapped>, Some.Output == None.Output, Some.Failure == Failure, None.Failure == Failure
    {
        return self
            .flatMap { $0.map { ifSome($0).eraseToAnyPublisher() } ?? ifNone().eraseToAnyPublisher() }
            .eraseToAnyPublisher()
    }
}

Теперь нам нужно реализовать accountDetailsPublisher в расширении User. Что нужно сделать этому методу? Необходимо проверить, находится ли User в базе данных (асинхронное действие), и, если нет, добавить User (другое асинхронное действие). Поскольку нам нужно связать асинхронные действия, нам снова нужно flatMap. Но нам бы очень хотелось написать:

extension User {
    func accountDetailsPublisher() -> AnyPublisher<AccountDetails, Error> {
        return isInDatabasePublisher()
            .flatMap(
                ifTrue: { Just(AccountDetails(user: self)).setFailureType(to: Error.self) },
                ifFalse: { self.addToDatabase().map { AccountDetails(user: self) } })
    }
}

Вот flatMap(ifTrue:ifFalse:):

extension Publisher where Output == Bool {
    func flatMap<True: Publisher, False: Publisher>(
        ifTrue: @escaping () -> True,
        ifFalse: @escaping () -> False
    ) -> AnyPublisher<True.Output, Failure>
        where True.Output == False.Output, True.Failure == Failure, False.Failure == Failure
    {
        return self
            .flatMap { return $0 ? ifTrue().eraseToAnyPublisher() : ifFalse().eraseToAnyPublisher() }
            .eraseToAnyPublisher()
    }
}

Теперь нам нужно написать isInDatabasePublisher и addToDatabase методы для User. У меня нет исходного кода для ваших функций checkIfUserIsInDatabase и createEmptyUser, поэтому я не могу напрямую преобразовать их в издатели. Но мы можем обернуть их, используя Future:

extension User {
    func isInDatabasePublisher() -> AnyPublisher<Bool, Error> {
        return Future { promise in
            checkIfUserIsInDatabase(user: self.uid, completion: promise)
        }.eraseToAnyPublisher()
    }

    func addToDatabase() -> AnyPublisher<Void, Error> {
        return Future { promise in
            createEmptyUser(user: self.uid, email: self.email, completion: promise)
        } //
            .map { _ in } // convert Bool to Void
            .eraseToAnyPublisher()
    }
}

Обратите внимание, что, поскольку ваш пример кода игнорирует вывод Bool createEmptyUser, я написал addToDatabase для вывода Void.

0 голосов
/ 08 марта 2020

Вот что я придумал:

Ссылка по мату: http://www.apeth.com/UnderstandingCombine/operators/operatorsflatmap.html#SECSerializingAsynchronicity { ссылка }

 var handler: AuthStateDidChangeListenerHandle?
 var storage = Set<AnyCancellable>()

    func checkLoginState(completion: @escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
        self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
            guard let safeSelf = self else { return }
            completion(Future<AccountDetails,Error> { promise in
                if let user = user {
                    safeSelf.handleUserInDatabase(user: user.uid)
                    .sink(receiveCompletion: { completion in
                        if let error = completion.error  {
                            print(error.localizedDescription)
                            promise(.failure(error))
                        }
                    }, receiveValue: { result in
                        if result {
                            promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
                        }
                    }).store(in: &safeSelf.storage)
                } else {
                    promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
                }
            }.eraseToAnyPublisher())
        }
    }


    /// Checks if User exists in Firestore, if not creates an empty User and returns true
    private func handleUserInDatabase(user: String) -> AnyPublisher<Bool,Error> {
        return Future<Bool,Error>( { [weak self] promise in
            guard let safeSelf = self else { return }
            safeSelf.checkIfUserIsInDatabase(user: user)
                .flatMap { result -> AnyPublisher<Bool,Error> in
                    if result == false {
                        return safeSelf.createEmptyUser(user: user).eraseToAnyPublisher()
                    } else {
                        promise(.success(true))
                        return Empty<Bool,Error>(completeImmediately: true).eraseToAnyPublisher()
                    }}
                .sink(receiveCompletion: { completion in
                    if let error = completion.error {
                        promise(.failure(error))
                    }}, receiveValue: {promise(.success($0))})
                .store(in:&safeSelf.storage)
            }
        ).eraseToAnyPublisher()
    }
...