How to add an unescaped closure Swift Firebase - swift

When I compile I get this error "The path to the document cannot be empty".
To fix this, I should add an unescaped closure.
How to add an unescaped closure to the fetchUsers () function and call the GetCorsodiLaurea () function in the closure? In such a way, the compiler will not try to execute functions asynchronously.
LoginViewModel
import SwiftUI
import Firebase
import LocalAuthentication
class LoginViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#AppStorage("use_face_id") var useFaceID: Bool = false
#AppStorage("use_face_email") var faceIDEmail: String = ""
#AppStorage("use_face_password") var faceIDPassword: String = ""
//Log Status
#AppStorage("log_status") var logStatus: Bool = false
//MARK: Error
#Published var showError: Bool = false
#Published var errorMsg: String = ""
// MARK: Firebase Login
func loginUser(useFaceID: Bool,email: String = "",password: String = "")async throws{
let _ = try await Auth.auth().signIn(withEmail: email != "" ? email : self.email, password: password != "" ? password : self.password)
DispatchQueue.main.async {
if useFaceID && self.faceIDEmail == ""{
self.useFaceID = useFaceID
// MARK: Storing for future face ID Login
self.faceIDEmail = self.email
self.faceIDPassword = self.password
}
self.logStatus = true
}
}
//MARK: FaceID Usage
func getBioMetricStatus()->Bool{
let scanner = LAContext()
return scanner.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: .none)
}
// MARK: FaceID Login
func autenticationUser()async throws{
let status = try await LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "To Login Into App")
if status{
try await loginUser(useFaceID: useFaceID,email: self.faceIDEmail,password: self.faceIDPassword)
}
}
}
ProfileViewModel
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: userDegreeModel = .empty
#Published var isSignedIn = false
#Published var showError: Bool = false
#Published var errorMsg: String = ""
var uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
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.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.userInfo.Nomeintero = ""
}
}
fetchUser() { [self] in
fetchDegrees()
}
}
func fetchUser(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userInfo = try document.data(as: UserModel.self)!
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: userDegreeModel.self)!
}
catch {
print(error)
}
}
}
}
}
}
UserModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
public struct UserModel: Codable{
#DocumentID var id: String?
var Nome : String
var Cognome : String
var photoURL : String
var Nomeintero : String
var Corsodilaurea : String
var Tipocorso : String
}
extension UserModel {
static let empty = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
}
userDegreeModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
struct userDegreeModel: Codable {
#DocumentID var id: String?
var Name : String
var TotalSubjects : Int
}
extension userDegreeModel {
static let empty = userDegreeModel(Name: "", TotalSubjects: 0)
}
Error

A couple of notes:
Firestore calls return on the main dispatch queue already about this), so you don't need to manually switch to the main queue using DispatchQueue.async { }. See my Twitter thread for more details.
Instead of mapping Firestore documents manually, you can use Codable to do so. This means less code to write, and fewer typos :-) Here is an article that goes into much more detail: Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese
Accessing the signed in user using Auth.auth().currentUser!.uid might result in you app breaking if no user is signed in. I recommend implementing an authentication state listener instead.
Since all of Firebase's APIs are asynchronous (see my blog post about this: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await | Peter Friese), the result of fetchUser will take a short moment, so you want to make sure to only call fetchDegrees once that call has completed. One way to do this is to use a completion handler.
Lastly, I recommend following a styleguide like this one for naming your classes and attribute: Swift Style Guide
I've updated your code accordingly below.
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
public struct UserModel: Codable {
#DocumentID var id: String?
var firstName: String
var lastName: String
var photoUrl: String
// ...
}
extension UserModel {
static let empty = UserModel(firstName: "", lastName: "", photoUrl: "")
}
public struct UserDegreeModel: Codable {
#DocumentID var id: String?
var name: String
var totalSubjects: Int
// ...
}
extension UserDegreeModel {
static let empty = UserDegreeModel(name: "", totalSubjects: 0)
}
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: UserDegreeModel = .empty
#Published var isSignedIn = false
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
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.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.username = ""
}
}
fetchUser() {
fetchDegrees()
}
}
func fetchUser(completion: () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.user = try document.data(as: UserModel.self)
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: UserDegreeModel.self)
}
catch {
print(error)
}
}
}
}
}
}

Related

Why is FirebaseFirestoreSwift not loading into my editor?

I am able to successfully import the package FirebaseFirestoreSwift, however, any code I write using said package throws errors. The editor is acting as if the package is not imported.
import SwiftUI
import Firebase
import CryptoKit
import FirebaseFirestore
import FirebaseFirestoreSwift -- NO error on import
import AuthenticationServices
class SignInViewModel: ObservableObject {
#Published var showSignInView: Bool = false
#Published var nonce: String = ""
#Published var currentUserData: UserModel? = nil
private lazy var authRef = Auth.auth()
private lazy var usersCollection = Firestore.firestore().collection("users")
public func getCurrentUserData(_ user: User) {
guard let currentUID = authRef.currentUser?.uid else {
showSignInView = true
return
}
usersCollection.document(currentUID).getDocument { (snapshot, error) in
guard let document = snapshot, document.exists else {
self.createNewUserDocument(currentUID, user)
return
}
guard let userData = try? document.data(as: UserModel.self) else { return } -- ERROR #1
self.currentUserData = userData
showSignInView = false
}
}
private func createNewUserDocument(_ currentUID: String, _ user: User) {
do {
let userData = UserModel(id: UUID().uuidString, name: user.displayName ?? "") -- ERROR #2
try usersCollection.document(currentUID).setData(from: userData)
} catch {
print("Error creating new user document: \(error)")
}
}
}
Error #1: "Argument passed to call that takes no arguments"
Error #2: "No exact matches in call to instance method 'setData'"
From my understanding, these errors are throwing due to the fact that the package is not correctly loading. I have restarted, reimported the packages/frameworks to no success. Running Xcode 14 Firebase 9.6.0

How to grab the current users "firstname" from firebase store. Swift 5

I did more trial and error and a bit of online research and this is what I came back with:
func presentWelcomeMessage() {
//Get specific document from current user
let docRef = Firestore.firestore()
.collection("users")
.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
} else if querySnapshot!.documents.count != 1 {
print("More than one document or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
guard let firstname = dataDescription?["firstname"] else { return }
self.welcomeLabel.text = "Hey, \(firstname) welcome!"
}
}
It works, but am not sure if it is the most optimal solution.
First I should say firstname is not really the best way to store a var. I would recommend using firstName instead for readability. I also recommend getting single documents like I am, rather than using a whereField.
An important thing to note is you should create a data model like I have that can hold all of the information you get.
Here is a full structure of how I would get the data, display it, and hold it.
struct UserModel: Identifiable, Codable {
var id: String
var firstName: String
private enum CodingKeys: String, CodingKey {
case id
case firstName
}
}
import SwiftUI
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
class UserDataManager: ObservableObject {
private lazy var authRef = Auth.auth()
private lazy var userInfoCollection = Firestore.firestore().collection("users")
public func getCurrentUIDData(completion: #escaping (_ currentUserData: UserModel) -> Void) {
if let currentUID = self.authRef.currentUser?.uid {
self.userInfoCollection.document(currentUID).getDocument { (document, error) in
if let document = document {
if let userData = try? document.data(as: UserModel.self) {
completion(userData)
}
} else if let error = error {
print("Error getting current UID data: \(error)")
}
}
} else {
print("No current UID")
}
}
}
struct ContentView: View {
#State private var userData: UserModel? = nil
private let
var body: some View {
ZStack {
if let userData = self.userData { <-- safely unwrap data
Text("Hey, \(userData.firstName) welcome!")
}
}
.onAppear {
if self.userData == nil { <-- onAppear can call more than once
self.udm.getCurrentUIDData { userData in
self.userData = userData <-- pass data from func to view
}
}
}
}
}
Hopefully this can point you in a better direction of how you should be getting and displaying data. Let me know if you have any further questions or issues.

How to map an array in a Firestore document to Swift?

How can I map the arrays in the following Firestore documents to Swift?
Here is my data model in Swift:
import Foundation
struct CityList: Codable, Hashable {
var name: String
var latitude: String
var longitude: String
}
struct Cities: Codable, Identifiable {
var id: String = UUID().uuidString
var citiesList: [CityList]
}
and here is my view model:
class WeatherList: ObservableObject {
#Published var cities = [CityList]()
func fetchCities(userInfo: UserInfo) {
self.cities.removeAll()
let db = Firestore.firestore()
.collection("cities")
.document(userInfo.user.uid)
db.getDocument() { (document, error) in
if let document = document, document.exists {
guard let itemIDs = document.get("citiesList") else {
return
}
for i in itemIDs {
print(i.value)
}
}
else {
return
}
}
}
}
When executed, it displays an error: Protocol 'Any' as a type cannot conform to 'Sequence'
How can I map this document?
You're almost there, just need to call data(as:) to perform the mapping.
Here is the updated code:
import Foundation
struct City: Codable, Hashable {
var name: String
var latitude: String
var longitude: String
}
struct Cities: Codable, Identifiable {
#DocumentID var id: String?
var cities: [City]
}
class WeatherViewModel: ObservableObject {
#Published var cities = [City]()
func fetchCities(userInfo: UserInfo) {
let docRef = Firestore.firestore()
.collection("cities")
.document(userInfo.user.uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
let citiesDocument = try document.data(as: Cities.self)
self.cities = citiesDocument.cities
}
catch {
print(error)
}
}
}
}
}
}
For a comprehensive overview of how to map Firestore data to / from Swift, check out my blog post Mapping Firestore Data in Swift - The Comprehensive Guide

Swift 5.1 Combine Observable/Environment variable not updating when didSet

I am trying to figure out why my session is not updating despite didSet firing and presumably updating User.
I've removed superfluous comments and style from the snippets but this is what I am trying currently.
// SessionStore.swift
class User {
var uid: String
var phoneNumber: String
init(uid: String, phoneNumber: String) {
self.uid = uid
self.phoneNumber = phoneNumber
}
}
class SessionStore : ObservableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self); print("didSet()") }}
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = User(
uid: user.uid,
phoneNumber: ""
)
} else {
self.session = nil
}
}
}
func register(
phoneNumber: String,
handler: #escaping AuthDataResultCallback
){
PhoneAuthProvider.provider(auth: Auth.auth())
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let error = error {
return
}
UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")
let verificationCode = "123456"
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: verificationID!,
verificationCode: verificationCode
)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error)
return
}
self.listen()
return
}
}
}
}
and
struct Login: View {
#EnvironmentObject var session: SessionStore
#State var phoneNumber: String = ""
#State var loading = false
#State var error = false
func getUser () {
session.listen()
}
var body: some View {
Group {
// If the user is logged in
if(session.session != nil) {
Score()
// If the user is NOT logged in
} else {
VStack {
Text("Register!")
TextField("Phone Number", text: $phoneNumber)
MyButton(...).gesture(TapGesture().onEnded {
self.session.register(
phoneNumber: self.phoneNumber
){
(result, error) in
if(error != nil) {
self.error = true
} else {
self.phoneNumber = ""
self.getUser()
}
})
}
}
}
}.onAppear(perform: getUser)
}
}
Scratchy (only based on your snapshot)...
Instead of this
class SessionStore : ObservableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self); print("didSet()") }}
use this
class SessionStore : ObservableObject {
#Published var session: User?
as far as I see from snapshot no more changes should be needed, might be some type alignment in other parts.

SwiftUI - Dynamically add #State for UI Toggle

I am currently getting a list of sites from a Firebase Firestore and then returning them to a list in SwiftUI. Each list item has a label and Toggle. The list of sites is dynamic so could be anywhere from 1-30+. How can I create an #State or similar bindable to observe each toggle's state.
I am currently rendering to UI with the following
#State private var SiteA = false
Form {
Section (header: Text("Select Sites")) {
ForEach(siteData.sites) { site in
HStack {
Toggle(isOn: self.$SiteA) {
Text(site.name)
Spacer()
}
}
}
}
}
Sites are retrieved using a Bindable object
import SwiftUI
import Combine
import Firebase
import FirebaseFirestore
struct Message: Identifiable {
var title: String
var messageBody: String
var sentBy: String
var targetSite: String
var expired: Bool
var timeStamp: Timestamp
var emergency: Bool
var id: String
}
struct Site: Identifiable {
var id: String
var name: String
}
class FirestoreMessages : ObservableObject {
var db = Firestore.firestore()
var didChange = PassthroughSubject<FirestoreMessages, Never>()
#Published var messages: [Message] = [] {
didSet{ didChange.send(self) }
}
#Published var sites: [Site] = [] {
didSet { didChange.send(self) }
}
func listen() {
db.collection("messages")
.whereField("expired", isEqualTo: false)
.addSnapshotListener { (snap, error) in
if error != nil {
print("Firebase Snapshot Error: \(error?.localizedDescription ?? "")")
} else {
self.messages.removeAll()
for doc in snap!.documents {
let title = doc["title"] as! String
let messageBody = doc["body"] as! String
let sentBy = doc["sentBy"] as! String
let targetSite = doc["targetSite"] as! String
let expired = doc["expired"] as! Bool
let timeStamp = doc["timeStamp"] as! Timestamp
let emergency = doc["emergency"] as! Bool
let id = doc.documentID
let message = Message(
title: title,
messageBody: messageBody,
sentBy: sentBy,
targetSite: targetSite,
expired: expired,
timeStamp: timeStamp,
emergency: emergency,
id: id)
self.messages.append(message)
}
}
}
}
func getSites() {
db.collection("sites")
.order(by: "name", descending: false)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting docs: \(err)")
} else {
self.sites.removeAll()
for document in querySnapshot!.documents {
let doc = document.data()
let id = document.documentID
let name = doc["name"] as! String
let site = Site(id: id, name: name)
self.sites.append(site)
}
}
}
}
}
How can I create an #State unique to each list item to monitor their states individually?
The answer to your problem is composition. Move the HStack and enclosed Toggle to a SiteRow view where each row has its own State.
struct SiteRow: View {
#State private var state: Bool = false
private let site: Site
init(_ site: Site) {
self.site = site
self.state = site.isOn
}
var body: some View {
HStack {
Toggle(isOn: self.$state) {
Text(site.name)
Spacer()
}
}
}
}
Then...
ForEach(siteData.sites) { site in SiteRow(site) }