将回调方法转换为与 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()
        )
    }
}

这就是我想出的:

马特的参考资料: 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([=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
    }
}

我认为您的最终目标是发出 AccountDetailsPublisher。但是由于并不总是有登录用户,它应该真正发出 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 上编写 isInDatabasePublisheraddToDatabase 方法。我没有您的 checkIfUserIsInDatabasecreateEmptyUser 函数的源代码,因此我无法将它们直接转换为发布者。但是我们可以使用 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()
    }
}

请注意,由于您的示例代码忽略了 createEmptyUserBool 输出,因此我写了 addToDatabase 来输出 Void