将回调方法转换为与 Combine 反应
Transform callback approach to reactive with Combine
这就是我正在做的事情:
-> Login/Signup 使用 FirebaseAuthentification
到 Firebase
-> 监听 AuthStateDidChangeListenerHandle
-> 我在 Firestore 中存储了额外的用户信息,因此我检查用户是否存在于 Firestore 中
-> 如果用户不存在我创建一个空用户
-> 如果一切顺利,我 return 通过回调的未来发布者(我也想改变它)
这是 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()
)
}
}
这就是我想出的:
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([=10=]))})
.store(in:&safeSelf.storage)
}
).eraseToAnyPublisher()
}
所以你有一些 AccountDetails
类型:
import Combine
import FirebaseAuth
struct AccountDetails {
var userId: String
var name: String?
var isLoggedIn: Bool
var isPremiumUser: Bool
}
让我们用一个 User
来扩展它,因为它会在以后简化事情:
extension AccountDetails {
init(user: User) {
self.userId = user.uid
self.name = user.displayName
self.isLoggedIn = true
self.isPremiumUser = false
}
}
我认为您的最终目标是发出 AccountDetails
的 Publisher
。但是由于并不总是有登录用户,它应该真正发出 Optional<AccountDetails>
,以便在用户注销时它可以发出 nil
。
让我们先将 addStateDidChangeListener
API 包装在 Publisher
中。我们不能为此使用 Future
,因为 Future
最多发出一个输出,但 addStateDidChangeListener
可以发出多个事件。所以我们将使用 CurrentValueSubject
代替。这意味着我们需要一个地方来存储主题和 AuthStateDidChangeListenerHandle
。您可以将它们存储为全局变量,或者存储在您的 AppDelegate
中,或者您认为合适的任何地方。对于这个答案,让我们创建一个 Demo
class 来保存它们:
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,我们只想发出 nil
。但是如果User?
是.some(user)
,我们需要做更多的异步操作:我们需要检查用户是否在数据库中,如果没有则添加用户。 flatMap
运算符允许您链接异步操作,但存在一些复杂性,因为我们需要根据上游发布者的输出采取不同的操作。
我们真的很想隐藏复杂性,只写这个:
extension Demo {
func loggedInAccountDetailsPublisher() -> AnyPublisher<AccountDetails?, Error> {
return userPublisher
.flatMap(
ifSome: { [=15=].accountDetailsPublisher().map { Optional.some([=15=]) } },
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 { [=16=].map { ifSome([=16=]).eraseToAnyPublisher() } ?? ifNone().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
现在我们需要在 User
扩展中实现 accountDetailsPublisher
。这个方法需要做什么?它需要检查 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 [=18=] ? ifTrue().eraseToAnyPublisher() : ifFalse().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
现在我们需要在 User
上编写 isInDatabasePublisher
和 addToDatabase
方法。我没有您的 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()
}
}
请注意,由于您的示例代码忽略了 createEmptyUser
的 Bool
输出,因此我写了 addToDatabase
来输出 Void
。
这就是我正在做的事情:
-> Login/Signup 使用 FirebaseAuthentification
到 Firebase-> 监听 AuthStateDidChangeListenerHandle
-> 我在 Firestore 中存储了额外的用户信息,因此我检查用户是否存在于 Firestore 中
-> 如果用户不存在我创建一个空用户
-> 如果一切顺利,我 return 通过回调的未来发布者(我也想改变它)
这是 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()
)
}
}
这就是我想出的:
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([=10=]))})
.store(in:&safeSelf.storage)
}
).eraseToAnyPublisher()
}
所以你有一些 AccountDetails
类型:
import Combine
import FirebaseAuth
struct AccountDetails {
var userId: String
var name: String?
var isLoggedIn: Bool
var isPremiumUser: Bool
}
让我们用一个 User
来扩展它,因为它会在以后简化事情:
extension AccountDetails {
init(user: User) {
self.userId = user.uid
self.name = user.displayName
self.isLoggedIn = true
self.isPremiumUser = false
}
}
我认为您的最终目标是发出 AccountDetails
的 Publisher
。但是由于并不总是有登录用户,它应该真正发出 Optional<AccountDetails>
,以便在用户注销时它可以发出 nil
。
让我们先将 addStateDidChangeListener
API 包装在 Publisher
中。我们不能为此使用 Future
,因为 Future
最多发出一个输出,但 addStateDidChangeListener
可以发出多个事件。所以我们将使用 CurrentValueSubject
代替。这意味着我们需要一个地方来存储主题和 AuthStateDidChangeListenerHandle
。您可以将它们存储为全局变量,或者存储在您的 AppDelegate
中,或者您认为合适的任何地方。对于这个答案,让我们创建一个 Demo
class 来保存它们:
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,我们只想发出 nil
。但是如果User?
是.some(user)
,我们需要做更多的异步操作:我们需要检查用户是否在数据库中,如果没有则添加用户。 flatMap
运算符允许您链接异步操作,但存在一些复杂性,因为我们需要根据上游发布者的输出采取不同的操作。
我们真的很想隐藏复杂性,只写这个:
extension Demo {
func loggedInAccountDetailsPublisher() -> AnyPublisher<AccountDetails?, Error> {
return userPublisher
.flatMap(
ifSome: { [=15=].accountDetailsPublisher().map { Optional.some([=15=]) } },
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 { [=16=].map { ifSome([=16=]).eraseToAnyPublisher() } ?? ifNone().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
现在我们需要在 User
扩展中实现 accountDetailsPublisher
。这个方法需要做什么?它需要检查 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 [=18=] ? ifTrue().eraseToAnyPublisher() : ifFalse().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
现在我们需要在 User
上编写 isInDatabasePublisher
和 addToDatabase
方法。我没有您的 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()
}
}
请注意,由于您的示例代码忽略了 createEmptyUser
的 Bool
输出,因此我写了 addToDatabase
来输出 Void
。