Reputation: 941
I have a Firebase function in a class (listenToUser) that works fine but I noticed that the next code (the IF ELSE) does not wait for it to complete before continuing. How can I wait for my function to be completed before continuing my code?
Portion of code of my main view :
...
@EnvironmentObject var firebaseSession: FirebaseSession_VM
...
.onAppear {
firebaseSession.listenToUser()
if firebaseSession.firebaseUser == nil {
showSignInView = true
} else {
showSignInStep1View = true
}
}
My function :
import SwiftUI
import Combine
import FirebaseAuth
class FirebaseSession_VM: ObservableObject {
static let instance = FirebaseSession_VM()
var didChange = PassthroughSubject<FirebaseSession_VM, Never>()
@Published var firebaseUser: FirebaseUser_M? {
didSet {
self.didChange.send(self)
}
}
var handle: AuthStateDidChangeListenerHandle?
func listenToUser () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.firebaseUser = FirebaseUser_M(
id: user.uid,
email: user.email
)
} else {
self.firebaseUser = nil
}
}
}
}
Upvotes: 0
Views: 1057
Reputation: 7254
Most of Firebase's API calls are asynchronous, which is why you need to either register a state listener or use callbacks.
Two side notes:
ObservableObjects
as singletons. Use @StateObject
instead, to make sure SwiftUI can properly manage its state.PassthroughSubject
directly. It's easier to use the @Published
property wrapper instead.That being said, here are a couple of code snippets that show how you can implement Email/Password Authentication with SwiftUI:
The main view shows if you're sign in. If you're not signed in, it will display a button that will open a separate sign in screen.
import SwiftUI
struct ContentView: View {
@StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("👋🏻 Hello!")
.font(.title3)
switch viewModel.isSignedIn {
case true:
VStack {
Text("You're signed in.")
Button("Tap here to sign out") {
viewModel.signOut()
}
}
default:
VStack {
Text("It looks like you're not signed in.")
Button("Tap here to sign in") {
viewModel.signIn()
}
}
}
}
.sheet(isPresented: $viewModel.isShowingLogInView) {
SignInView()
}
}
}
The main view's view model listens for any auth state changes and updates the isSignedIn
property accordingly. This drives the ContentView
and what it displays.
import Foundation
import Firebase
class ContentViewModel: ObservableObject {
@Published var isSignedIn = false
@Published var isShowingLogInView = false
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.isSignedIn = true
}
else {
self.isSignedIn = false
}
}
}
/// Show the sign in screen
func signIn() {
isShowingLogInView = true
}
/// Sign the user out
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print("Error while trying to sign out: \(error)")
}
}
}
The SignInView
shows a simple email/password form with a button. The interesting thing to note here is that it listens for any changes to the viewModel.isSignedIn
property, and calls the dismiss
action (which it pulls from the environment). Another option would be to implement a callback as a trailing closure on the view model's signIn()
method.
struct SignInView: View {
@Environment(\.dismiss) var dismiss
@StateObject var viewModel = SignInViewModel()
var body: some View {
VStack {
Text("Hi!")
.font(.largeTitle)
Text("Please sign in.")
.font(.title3)
Group {
TextField("Email", text: $viewModel.email)
.disableAutocorrection(true)
.autocapitalization(.none)
SecureField("Password", text: $viewModel.password)
}
.padding()
.background(Color(UIColor.systemFill))
.cornerRadius(8.0)
.padding(.bottom, 8)
Button("Sign in") {
viewModel.signIn()
}
.foregroundColor(Color(UIColor.systemGray6))
.padding(.vertical, 16)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(8)
}
.padding()
.onChange(of: viewModel.isSignedIn) { signedIn in
dismiss()
}
}
}
The SignInViewModel
has a method signIn
that performs the actual sign in process by calling Auth.auth().signIn(withEmail:password:)
. As you can see, it will change the view model's isSignedIn
property to true
if the user was authenticated.
import Foundation
import FirebaseAuth
class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var isSignedIn: Bool = false
func signIn() {
Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in
if let error = error {
print("There was an issue when trying to sign in: \(error)")
return
}
guard let user = authDataResult?.user else {
print("No user")
return
}
print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
}
}
import Foundation
import FirebaseAuth
import FirebaseAuthCombineSwift
class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var isSignedIn: Bool = false
// ...
func signIn() {
Auth.auth().signIn(withEmail: email, password: password)
.map { $0.user }
.replaceError(with: nil)
.print("User signed in")
.map { $0 != nil }
.assign(to: &$isSignedIn)
}
}
import Foundation
import FirebaseAuth
class SignInViewModel: ObservableObject {
@Published var email: String = ""
@Published var password: String = ""
@Published var isSignedIn: Bool = false
@MainActor
func signIn() async {
do {
let authDataResult = try 3 await 1 Auth.auth().signIn(withEmail: email, password: password)
let user = authDataResult.user
print("Signed in as user \(user.uid), with email: \(user.email ?? "")")
self.isSignedIn = true
}
catch {
print("There was an issue when trying to sign in: \(error)")
self.errorMessage = error.localizedDescription
}
}
}
I wrote an article about this in which I explain the individual techniques in more detail: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await. If you'd rather watch a video, I've got you covered as well: 3 easy tips for calling async APIs
Upvotes: 3