Итак, у вас есть некоторый тип 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
.