Cannot convert value of type `AuthResultCallback`? - swift

I know about other similar questions, but they are to do with Auth.auth().signIn and signUp respectively, where there is a completion handler argument present within the function.
I'm trying to include a sign in the anonymous function to my SessionStore class so that my app observes the state of the user, i.e., whether he is signed in (anonymously or otherwise) or signed out, and accordingly display the relevant view.
However, when trying to add anonymous sign up to my SessionStore class, I get the following error:
Cannot convert value of type 'AuthResultCallback' (aka '(Optional<User>, Optional<Error>) -> ()') to expected argument type 'AuthDataResultCallback?' (aka 'Optional<(Optional<AuthDataResult>, Optional<Error>) -> ()>')
My code is as follows:
class SessionStore: ObservableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
#Published var session: User? {didSet {self.didChange.send(self)}}
var handle: AuthStateDidChangeListenerHandle?
func listen() {
handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
if let user = user {
self.session = User(uid: user.uid, email: user.email)
} else {
self.session = nil
}
})
}
func signUp(email: String, password: String, handler: #escaping AuthDataResultCallback) {
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(email: String, password: String, handler: #escaping AuthDataResultCallback) {
Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}
func signOut() {
do {
try Auth.auth().signOut()
self.session = nil
} catch {
print("Error signing user out")
}
}
func signUpAnonymously(handler: #escaping AuthResultCallback) {
Auth.auth().signInAnonymously(completion: handler) // **ERROR APPEARS HERE**
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
unbind()
}
}
struct User {
var uid: String
var email: String?
init(uid: String, email: String?) {
self.uid = uid
self.email = email
}
}
Can someone tell me how to fix this error?

The code snippet you pasted is from an outdated tutorial. As of Xcode 11 beta 5, you no longer need to use PassthroughSubject to send notifications about property changes. Sarun's blog post has a great and concise section on what has changed, I encourage you to read it.
Please also note that you don't need to define your own User class if you only care about the user's ID and their email address, as these attributes are already defined on Firebase's own User type (by way of implementing the UserInfo protocol).
Here is a snippet that shows how to implement the callback you're interested in (see registerStateListener).
class AuthenticationService: ObservableObject {
#Published var user: User?
private var handle: AuthStateDidChangeListenerHandle?
init() {
registerStateListener()
}
func signIn() {
if Auth.auth().currentUser == nil {
Auth.auth().signInAnonymously()
}
}
func signOut() {
do {
try Auth.auth().signOut()
}
catch {
print("Error when trying to sign out: \(error.localizedDescription)")
}
}
func updateDisplayName(displayName: String, completionHandler: #escaping (Result<User, Error>) -> Void) {
if let user = Auth.auth().currentUser {
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = displayName
changeRequest.commitChanges { error in
if let error = error {
completionHandler(.failure(error))
}
else {
if let updatedUser = Auth.auth().currentUser {
print("Successfully updated display name for user [\(user.uid)] to [\(updatedUser.displayName ?? "(empty)")]")
// force update the local user to trigger the publisher
self.user = updatedUser
completionHandler(.success(updatedUser))
}
}
}
}
}
private func registerStateListener() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
self.handle = Auth.auth().addStateDidChangeListener { (auth, user) in
print("Sign in state has changed.")
self.user = user
if let user = user {
let anonymous = user.isAnonymous ? "anonymously " : ""
print("User signed in \(anonymous)with user ID \(user.uid). Email: \(user.email ?? "(empty)"), display name: [\(user.displayName ?? "(empty)")]")
}
else {
print("User signed out.")
self.signIn()
}
}
}
}
If you're interested in more background, check out my article series about building a to-do app with SwiftUI and Firebase: part 1 (building the UI using SwiftUI), part 2 (storing data in Firestore and using Firebase Anonymous Auth), part 3 (Sign in with Apple)

Related

Can someone explain what "extra trailing closure passed in call" means?

I'm receiving the error "Extra trailing closure passed in call" on the authViewModel.fetchUser() function. From what I've gathered researching online this means that fetchuser can't have the trailing closure (the brackets), I am confused about what in my fetchuser function says that it cannot have the {} after the call. Or maybe I'm not understanding the error at all. Thank you in advance!
FeedCellViewModel
import Foundation
class FeedCellViewModel: ObservableObject {
#Published var posts = [Post]()
let service = PostService()
let authViewModel = AuthViewModel()
init() {
fetchPosts()
}
func fetchPosts() {
service.fetchPosts { posts in
self.posts = posts
for i in 0 ..< posts.count {
let uid = posts[i].uid
self.authViewModel.fetchUser() { user in
self.posts[i].user = user
}
}
}
}
}
AuthViewModel
import SwiftUI
import FirebaseAuth
import FirebaseCore
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
class AuthViewModel: NSObject, ObservableObject {
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
#Published var selectedImage: UIImage?
#Published var didAuthenticateUser = false
private var tempUserSession: FirebaseAuth.User?
private let service = UserService()
static let shared = AuthViewModel()
override init() {
super.init()
userSession = Auth.auth().currentUser
fetchUser()
}
func login(withEmail email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
return
}
self.userSession = result?.user
self.fetchUser()
}
}
func register(withEmail email: String, password: String, fullname: String) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.tempUserSession = user
let data = ["email": email,
"fullname": fullname,
"uid": user.uid]
COLLECTION_USERS
.document(user.uid)
.setData(data) { _ in
self.didAuthenticateUser = true
}
self.uploadProfileImage(self.selectedImage)
self.fetchUser()
}
}
func signOut() {
// sets user session to nil so we show login view
self.userSession = nil
// signs user out on server
try? Auth.auth().signOut()
}
func uploadProfileImage(_ image: UIImage?) {
guard let uid = tempUserSession?.uid else { return }
ImageUploader.uploadImage(image: image) { profileImageUrl in
Firestore.firestore().collection("users")
.document(uid)
.updateData(["profileImageUrl": profileImageUrl]) { _ in
self.userSession = self.tempUserSession
}
}
}
func fetchUser() {
guard let uid = userSession?.uid else { return }
COLLECTION_USERS.document(uid).getDocument { snapshot, _ in
guard let user = try? snapshot?.data(as: User.self) else { return }
self.currentUser = user
}
}
}
You're seeing this error because your fetchUser() function doesn't take a closure parameter (or any parameters for that matter).
A trailing closure is just a nicer way of passing a closure as a parameter, given it's the last parameter to a method.
Try running this example in a playground to get a feel for this:
func hello(closure: () -> Void) {
print("calling closure")
closure()
print("finished")
}
// these are the same
hello(closure: { print("hello!!") })
hello { print("hello!!") }
If you want to provide the user in a closure to the caller, return the user as a parameter to the closure in addition to setting the currentUser.
func fetchUser(finishedFetching: #escaping (User) -> Void) {
guard let uid = userSession?.uid else { return }
COLLECTION_USERS.document(uid).getDocument { snapshot, _ in
guard let user = try? snapshot?.data(as: User.self) else { return }
self.currentUser = user
finishedFetching(user)
}
}
Read more about closures in the Swift language guide.
They are essentially unnamed functions that you can store and pass around.
You'll learn there why I marked our closure as #escaping.

How to check if a user already exists in a Firestore collection after signing in with Firebase Auth in SwiftUI

#Published var isNewUser: Bool?
init() {
self.isNewUser = false
}
func checkIfTheUserExistsInDataBase(
userID: String?, completion: #escaping (_ isNewuser: Bool) -> Void
) {
let docRef = db.collection("users").whereField("user_id", isEqualTo: userID!).limit(to: 1)
docRef.getDocuments { querySnapshot, error in
if error != nil {
print(error?.localizedDescription)
} else {
if let doc = querySnapshot?.documents, doc.isEmpty {
completion(true)
} else {
completion(false)
}
}
}
}
func login(
email: String, password: String,
completion: #escaping (_ error: Error?, _ isEmailVerified: Bool) -> Void
) {
Auth.auth().signIn(withEmail: email, password: password) { authDataResult, error in
if error == nil {
if authDataResult!.user.isEmailVerified {
DispatchQueue.main.async {
self.checkIfTheUserExistsInDataBase(userID: authDataResult?.user.uid) { isNewUser in
self.isNewUser = isNewUser
}
}
UserDefaults.standard.set(authDataResult?.user.uid, forKey: CurrentUserDefaults.userID)
completion(error, true)
} else {
print("Email not verified")
completion(error, false)
}
} else {
completion(error, false)
}
}
}
I tried to use DispatchSemaphore to let a longer running function execute first which is checkIfTheUserExistsInDataBase, but it froze my app. Is there a better way to do this?
Firebase supports async/await (see this short, this video, and this blog post I created to explain this in detail.
To answer your question: you should use async/await to call signing in the user, waiting for the result, checking if the user exists in your Firestore collection, and the updating the UI.
The following code snippet (which is based on this sample app) uses the new COUNT feature in Firestore to count the number of documents in the users collection to determine if there is at least one user with the ID of the user that has just signed in.
func isNewUser(_ user: User) async -> Bool {
let userId = user.uid
let db = Firestore.firestore()
let collection = db.collection("users")
let query = collection.whereField("userId", isEqualTo: userId)
let countQuery = query.count
do {
let snapshot = try await countQuery.getAggregation(source: .server)
return snapshot.count.intValue >= 0
}
catch {
print(error)
return false
}
}
func signInWithEmailPassword() async -> Bool {
authenticationState = .authenticating
do {
let authResult = try await Auth.auth().signIn(withEmail: self.email, password: self.password)
if await isNewUser(authResult.user) {
}
return true
}
catch {
print(error)
errorMessage = error.localizedDescription
authenticationState = .unauthenticated
return false
}
}
See this video for more details about how to implement Firebase Authentication in SwiftUI apps.

Why is my swift code executing out of order? Firebase authentication

I am calling Authenticator.loginUser() with the expectation that the method will log in a user then call getCurrentUser(). Based on printed output, getCurrentUser() is executing first. Is there a way to force it to execute in order?
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
#Published var documentId: String = ""
func loginUser (email: String, password: String, viewModel: UsersViewModel) {
FirebaseAuth.Auth.auth().signIn(withEmail: email, password: password, completion: { result, error in
guard error == nil else {
print ("error: \(error!)")
return
}
print ("user signed in")
})
self.user = self.getCurrentUser(viewModel: viewModel)
}
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
documentId = currentUser.documentId!
print("auth.documentId \(documentId)")
return userID
}
}
FirebaseAuth.Auth.auth().signIn is asynchronous - it runs in the background and not on the main thread. This means that signIn will be called some time in the future.
A solution is to put the call to getCurrentUser in the completion block:
func loginUser(email: String, password: String, viewModel: UsersViewModel) {
FirebaseAuth.Auth.auth().signIn(withEmail: email, password: password, completion: { result, error in
guard error == nil else {
print("error: \(error!)")
return
}
print("user signed in")
self.user = self.getCurrentUser(viewModel: viewModel) // move here
})
}

How to use Firebase Auth + Combine to Sign-In and prevent Sign-In

So I've been following Log-In SwiftUI tutorials for Firebase and it's doing what it's supposed to for the most part: An error pops up when one or more of the input fields are left blank.
The problem occurs however, when I fill in the username + password fields with random gibberish. The view changes rather than popping up an error saying that the username is invalid (which I see in my console).
I've done some research and found that the problem might be due to the asynchronous behavior of Firebase and I haven't necessarily connected the error toggle to the sign-In result. But as a noob, I don't know how to implement the trailing closure in my code, and unsure where to go from here.
What change do I need to make exactly to ensure that upon failure of signing in with firebase, the view does not change and error pops up?
Here's my Sign-In function:
func signIn(){
error = false
session.signIn(email: user_account, password: password){
(result, error) in
if let errornew = error {
self.inputerror = errornew.localizedDescription
print("\(String(describing:error))")
self.error = true
self.alert.toggle()
} else {
self.user_account = ""
self.password = ""
}
}
}
Parent View:
struct ContentView: View {
#EnvironmentObject var session:SessionStore
#State var setUp = false
func getUser(){
session.listen()
}
var body: some View {
Group{
if(session.session != nil){
Text("App Home Page")
Text("Welcome")
Text("Email: \(session.session?.email ?? "")")
} else {
OpeningView() // Sign-In function is in a child view under OpeningView()
}
}.onAppear(perform: self.getUser)
}
Session Class:
class SessionStore: ObservableObject{
#Published var isSetUp:Bool?
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? {didSet {self.didChange.send(self)}}
var handle: AuthStateDidChangeListenerHandle?
let user = Auth.auth().currentUser
//checks to see whether or not we have a user
func listen(){
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
if let user = user {
//if we have a user, create a new user model
print("Got the user: \(user)")
self.session = User(uid: user.uid, email: user.email!)
} else {
//if not, then session is nil
self.session = nil
}
})
}
func signUp(email: String, password: String, handler: #escaping AuthDataResultCallback){
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(email: String, password: String, handler: #escaping AuthDataResultCallback){
Auth.auth().signIn(withEmail: email, password: password, completion: handler)
}
func login(withEmail email: String, password: String, _ callback: ((Error?) ->())? = nil){
Auth.auth().signIn(withEmail: email, password: password){(user, error) in
if let e = error{
callback?(e)
return
}
callback?(nil)
print("Login Successful")
}
}
func signOut(){
do{
try Auth.auth().signOut()
self.session = nil
} catch {
print("Error Signing Out.")
}
}
func unbind(){
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit{
unbind()
}
}
struct User {
var uid : String
var email : String?
init(uid: String, email: String?){
self.uid = uid
self.email = email
}
}
Your View needs something like this on top to check whether you are logged in or not:
var body: some View {
ZStack {
if Auth.auth().currentUser != nil {
// Your View when you are logged in.
else {
SignInView()
} }

How open one screen if a new user registered successfully, and other when existing user logged in succesfully using SwiftUI?

I'm working on Log In and Sign Up pages using Firebase Auth by email&password.
What is done successfully:
If the session (login or sign up) isn't nil, then user goes to a Welcome Back View.
The question is next:
I don't want a new user to go to "Welcome Back " View, instead, I want him after pressing "Sign Up" button (is session isn't nil to go to a new View - "Upload Your Photo View"
My Session Store Class (#ObservableObject):
#Published var session: User? {didSet{self.didChange.send(self)}}
var didChange = PassthroughSubject<SessionStore, Never>()
var handle: AuthStateDidChangeListenerHandle?
let currentuser = Auth.auth().currentUser
func listen(){
handle = Auth.auth().addStateDidChangeListener({(auth, user) in
if let user = user{
self.session = User(uid: user.uid, email: user.email)
} else {
self.session = nil
}
})
}
func signUp(email: String, password: String, fullname: String, username: String, handler: #escaping AuthDataResultCallback){
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
func signIn(email: String, password: String, handler: #escaping AuthDataResultCallback){
Auth.auth().signIn(withEmail: email, password: password){ (res,err) in
if err != nil{
return
}
}
}
func signOut(){
do{
try Auth.auth().signOut()
self.session = nil
}catch{
print("Error signing out")
}
}
func unbind() {
if let handle = handle {
Auth.auth().removeStateDidChangeListener(handle)
}
}
deinit {
unbind()
}
func addUsername(username: String){
session?.username = username
}
func addFullname(fullname: String){
session?.fullname = fullname
}
class User:ObservableObject{
var uid: String
var email: String?
var username: String?
var fullname: String?
init(uid:String, email:String?){
self.uid = uid
self.email = email
}
}}
My Content View Looks like that :
Group{
if (session.session != nil){
Button(action: {
try! Auth.auth().signOut()
UserDefaults.standard.set(false, forKey: "status")
NotificationCenter.default.post(name: NSNotification.Name("statusChange"), object: nil)
}){
Text("Log out")
}
ExploreView()
}else{
AuthView()
}
}.onAppear(perform: getUser)
A possible solution would be a two way binding which just triggers if signUp was successful (so it is the first login ever in that moment).
Edit to your comment: In signUp-func you set a variable to "true" when signUp was successful and PhotoUploadView will pop up.
Add a variable in class User (which can be used then in your variable session):
#Published var firstLogin = false
Change firstLogin after successful signup in signUp-func:
session.firstLogin = true
Use something like this in ContentView:
//.....
else{
if session.firstLogin {
PhotoUploadView()
}
Set session.firstLogin to false afterwards, whenever you are done in PhotoUploadView.