1

I have a subscriber to an @Published text field. I add the subscriber through the init() function of my class.

The map of this subscriber scans all users in Firebase and checks for the "email" field. If the email inputted by the user is in one of these fields, then I set valid to false (as the email is already in use).

However, when I run this code, the second print line is executed first, with the default value. Then, after some time, the print("This email is valid") line is run.

I believe this has to do with the fact that a Firebase call is an asynchronous task.

How can I solve this so that the return value returns the result of the Firebase call? I'm using Xcode Version 12.5.1.

Also, i'm not sure if this is the best way to check if an email is already in use, but it's the only solution I could find :)

Code:

func addEmailSubscriber() {
        
        $email
            .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
            .map { email -> Bool in
                
                var valid: Bool = false
                
                Firestore.firestore()
                    .collection("users")
                    .whereField("email", isEqualTo: email)
                    .getDocuments { (snapshot, err) in
                        
                        guard let snapshot = snapshot else { print("Error getting snapshot"); return }
                        
                        if err != nil {
                            print("Error occured")
                            return
                        }
                        
                        if snapshot.documents.count == 0 {
                            print("This email is valid") // This is successfully printed after some time
                            valid = true
                            return
                        }
                    }
                
                print(valid) // This gets printed first, with the value of "false"
                return valid
                
            }
            .sink { [weak self] isValid in
                self?.emailIsValid = isValid
                
            }
            .store(in: &cancellabes)
        
    }

Log: enter image description here

4

1 回答 1

1

Combine is a great tool for things like this. In this case, you want to translate your $email Publisher into a new Publisher that returns the value from your asynchronous Firebase call. You can use map, switchToLatest, and Future to do this.

Here's a basic example showing the concept (not using your code):

class ViewModel : ObservableObject {
    @Published var email = ""
    
    private var cancellable : AnyCancellable?
    
    init() {
        cancellable = $email
            .debounce(for: .seconds(1), scheduler: RunLoop.main)
            .map { val in
                Future<Bool,Never> { promise in
                    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                        promise(.success(true))
                    }
                }
            }
            .switchToLatest()
            .sink(receiveValue: { val in
                print("Valid?",val)
            })
    }
}

struct ContentView : View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        TextField("", text: $viewModel.email)
    }
}

Translated to your code, it'll look something like (see inline comments):

func addEmailSubscriber() {
    
    $email
        .debounce(for: .seconds(1.0), scheduler: DispatchQueue.main)
        .map { email -> Bool in
            Future<Bool,Never> { promise in
                Firestore.firestore()
                    .collection("users")
                    .whereField("email", isEqualTo: email)
                    .getDocuments { (snapshot, err) in
                        
                        guard let snapshot = snapshot else {
                            print("Error getting snapshot")
                            promise(.success(false)) //you may want to consider using `.failure` here, but keep in mind that that'll cancel the Publisher chain unless you erase the errors later on
                            return
                        }
                        
                        if err != nil {
                            print("Error occured")
                            promise(.success(false)) //see above comment
                            return
                        }
                        
                        if snapshot.documents.count == 0 {
                            print("This email is valid") // This is successfully printed after some time
                            promise(.success(true))
                        }
                    }
            }
        }
        .switchToLatest()
        .sink { [weak self] isValid in
            self?.emailIsValid = isValid
        }
        .store(in: &cancellabes) //I kept the spelling the same, but this looks like a typo
}
于 2021-06-22T16:35:36.993 回答