SwiftUI - Firebase: Value of type 'String' has no member 'documentID' - swift

When I try to get user data from firebase I have an error. The error message is:
Value of type 'String' has no member 'documentID'
The line with the error is the line fetchUser(uid: uid.documentID) { (user) in:
let title = doc.document.data()["title"] as? String ?? "No Title"
let time = doc.document.data()["time"] as? Timestamp ?? Timestamp(date: Date())
let pic = doc.document.data()["url"] as? String ?? "No URL"
let uid = doc.document.data()["uid"] as? String ?? ""
// getting user Data...
fetchUser(uid: uid.documentID) { (user) in
And this is my FetchUser model:
// Global Refernce
let ref = Firestore.firestore()
func fetchUser(uid: String,completion: #escaping (UserModel) -> ()){
ref.collection("Users").document(uid).getDocument { (doc, err) in
guard let user = doc else{return}
let username = user.data()?["username"] as? String ?? "No Username"
let pic = user.data()?["imageurl"] as? String ?? "No image URL"
let bio = user.data()?["bio"] as? String ?? "No bio"
let uid = user.data()?["uid"] as? String ?? ""
DispatchQueue.main.async {
completion(UserModel(username: username, pic: pic, bio: bio, uid: uid))
}
}
}
Below is code to that checks the users uid. On the line that starts "ref.collection..." the error "Cannot find 'uid' in scope" is thrown.
func checkUser(){
let ref = Firestore.firestore()
if let currentUser = Auth.auth().currentUser {
let uid = currentUser.uid
} else {
print("No Authenticated User")
return
}
ref.collection("Users").whereField("uid", isEqualTo: uid).getDocuments { (snap, err) in
if err != nil{
// No Documents..
// No User Found...
self.registerUser.toggle()
self.isLoading = false
return
}
if snap!.documents.isEmpty{
self.registerUser.toggle()
self.isLoading = false
return
}
self.log_Status = true
}
}

Let me take the first part of your code to show where the issue is. Note how much easier it is to read when properly formatted
func checkUser() {
let ref = Firestore.firestore()
if let currentUser = Auth.auth().currentUser {
//NOTE! Note that this area is encapsulated with brackets { and }
//That means it's is own 'space' and anything defined in this area
//only exists in this area
let uid = currentUser.uid //<- uid only exists here
} else {
print("No Authenticated User")
return
}
//uid no longer exists and cannot be referenced
// e.g. it's not in 'scope' at this point
ref.collection("Users").whereField("uid", isEqualTo: uid)
However, if you look at where the let ref = Firestore line is located, it's at the top level within the checkUser function and will exist throughout the function.
There are many way to do this; here's one using a guard statement
func checkUser() {
let ref = Firestore.firestore()
guard let currentUser = Auth.auth().currentUser else {
print("no user!")
return
}
// currentUser flows through to here because it was created with
// a guard statement, so now we know it's populated and can
// get the uid property value from it
let uid = currentUser.uid
ref.collection("Users").whereField("uid", isEqualTo: uid)
guard is pretty neat in that it not only allows you to instantiate a var while a the same time as protecting your code from a nil situation, it also allows the var to flow through to the code following the guard.

I'm writting this answer as a community wiki, since the issue was resolved from the comments section, in order to provide a proper response to the issue reported.
The error came while trying to get the uid as fetchUser(uid: uid.documentID), instead the correct way is by doing fetchUser(uid: uid)
Then an error mentioning Document path cannot be empty appeared, which was mainly due to the fact that no entries with were loaded, the best way to avoid this is to load documents on the consulted path without nil values

Related

SwiftUI: Value of type 'DocumentSnapshot?' has no member 'document'

When trying to fetch a user, I am getting the error: "Value of type 'DocumentSnapshot?' has no member 'document'". The error occurs on the line that starts "let uid....".
import SwiftUI
import Firebase
// Global Refernce
let ref = Firestore.firestore()
func fetchUser(uid: String,completion: #escaping (UserModel) -> ()){
ref.collection("Users").document(uid).getDocument { (doc, err) in
guard let user = doc else{return}
let username = user.data()?["username"] as? String ?? "No Username"
let pic = user.data()?["imageurl"] as? String ?? "No image URL"
let bio = user.data()?["bio"] as? String ?? "No bio"
let uid = doc.document.data()["uid"] as! String ??
DispatchQueue.main.async {
completion(UserModel(username: username, pic: pic, bio: bio, uid: uid))
}
}
}
if uid is a field like others username and imageurl then use
let uid = user.data()["uid"] as? String ?? ""
if you have made that uid as id of the document then use
let uid = doc.documentID
See Docs

How to retrieve data from Firestore as soon as SwiftUI view appears?

I have this class CurrentUser that manages the currently logged in user and pulls the data for that user from Firebase.
One of CurrentUser's attributes is userEventIDs. I also have a collection of Events documents. Each user has their own array of event IDs that correspond to the events within the Events collection in my Firestore database.
On the MyAccount view struct I have an onAppear method that queries the Events collection based on the currentUser's array of eventIds, returns those Events, and then sorts them to be either before or after today based on the date of the event.
Currently the eventIds load in the first time this view is opened, but the query from the events comes back blank twice and only after the view is switched to another one and back to the MyAccount view will the page populate with these events.
Is there something I can do to make the events load on the first time the view is opened?
CurrentUser
class CurrentUser: ObservableObject {
let user = Auth.auth().currentUser
#Published var currentUserInformation = User(id: "", name: "", email: "'", accountType: "", profPicURL: "", coverPhotoURL: "", numberFollowers: nil, description: nil, location: nil, websiteLink: nil, orgID: nil, userEventIDs: [String](), userEvents: [Event]())
init() {
getUserInformation()
}
func getUserInformation() {
let UID = user!.uid
let database = Firestore.firestore()
database.collection("Organizers").whereField("Organizer ID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
self.currentUserInformation.name = document.get("Organization Name") as! String
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = document.get("Account Type") as! String
self.currentUserInformation.profPicURL = document.get("Profile Pic URL") as! String
self.currentUserInformation.coverPhotoURL = document.get("Cover Pic URL") as! String
self.currentUserInformation.numberFollowers = (document.get("Number of Followers") as! Int)
self.currentUserInformation.description = (document.get("Organization Description") as! String)
self.currentUserInformation.websiteLink = (document.get("Organization Website Link") as! String)
self.currentUserInformation.location = (document.get("Organization Location") as! String)
self.currentUserInformation.orgID = (document.get("Organizer ID") as! String)
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
self.currentUserInformation.accountType = "Organizer"
}
}
if self.currentUserInformation.id == "" {
database.collection("Activists").whereField("UID", isEqualTo: UID).getDocuments() { (querySnapshot, err) in
if err != nil {
print("Error getting documents: \(err!)")
}
for document in querySnapshot!.documents {
self.currentUserInformation.id = document.documentID
let firstName = document.get("First Name") as! String
let lastName = document.get("Last Name") as! String
self.currentUserInformation.name = "\(firstName) \(lastName)"
self.currentUserInformation.email = document.get("Email") as! String
self.currentUserInformation.accountType = "Activist"
self.currentUserInformation.profPicURL = document.get("Profile Pic") as! String
self.currentUserInformation.userEventIDs = (document.get("Events") as! [String])
}
}
}
}
func getUserEvents() {
let database = Firestore.firestore()
let eventRef = database.collection("Events")
for eventID in self.currentUserInformation.userEventIDs {
for event in self.currentUserInformation.userEvents {
if event.id == eventID {
break
}
}
eventRef.document(eventID).getDocument() { (document, error) in
if let document = document {
let id = document.documentID
let eventTitle = document.get("Name") as! String
let organizer = document.get("Organizer") as! String
let organizerID = document.get("Organizer ID") as! String
let eventDescription = document.get("Description") as! String
let date = document.get("Date") as! String
let time = document.get("Time") as! String
let location = document.get("Location") as! String
let numAttending = document.get("Number Attending") as! Int
let eventPhotoURL = document.get("Event Photo URL") as! String
self.currentUserInformation.userEvents.append(Event(id: id, eventTitle: eventTitle, eventOrganizer: organizer, eventOrganizerID: organizerID, eventDescription: eventDescription, date: date, time: time, location: location, numAttending: numAttending, eventPhotoURL: eventPhotoURL))
} else {
print("Document does not exist")
}
}
}
}
}
View
.onAppear() {
if currentActivist.currentUserInformation.userEvents.count != currentActivist.currentUserInformation.userEventIDs.count {
currentActivist.getUserEvents()
print("Getting user events")
}
pastEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Past"]!
futureEvents = MyAccountActivistView.getSortedEvent(actEvents: currentActivist.currentUserInformation.userEvents)["Upcoming"]!
}
A couple of quick notes:
Most Firebase calls are asynchronous (check out this article to understand why), so your call to Auth.auth().currentUser is most likely going to return nil. Instead, you should register an AuthenticationStateListener. See this sample code to see how it's done.
Instead of instantiating an empty User instance, make currentUserInformation optional
Mapping data is much easier using Firestore's support for Codable. I've written extensively about this, but the gist of it is, you'll be able to map documents with a single line of code (instead of having to manually map every single field). The Firestore documentation actually has a nice code snippet that you can adopt:
let docRef = db.collection("cities").document("BJ")
docRef.getDocument { (document, error) in
// Construct a Result type to encapsulate deserialization errors or
// successful deserialization. Note that if there is no error thrown
// the value may still be `nil`, indicating a successful deserialization
// of a value that does not exist.
//
// There are thus three cases to handle, which Swift lets us describe
// nicely with built-in Result types:
//
// Result
// /\
// Error Optional<City>
// /\
// Nil City
let result = Result {
try document?.data(as: City.self)
}
switch result {
case .success(let city):
if let city = city {
// A `City` value was successfully initialized from the DocumentSnapshot.
print("City: \(city)")
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding city: \(error)")
}
}
Avoid force unwrapping (using the ! operator), use optional unwrapping (using the ? operator), and the nil-coalescing operator (??) instead

How to automatically update information changed in Firestore

I have this code to display username that was stored in Firestore when signing up:
func displayUserName(){
let db = Firestore.firestore()
if let uid = user?.uid{
db.collection("PROFILE").document(uid).getDocument { (snap, err) in
guard let data = snap else {return}
let firstname = data.get("firstName") as! String
self.firstName = firstname
}
}
}
but, when I change the name in Firestore, I need to relaunch the app so it can update. is it possible to update this name without needing to relaunch the app?
Based on the comment given, I just changed getDocuments for addSnapshotListener
db.collection("PROFILE").document(uid).addSnapshotListener { (documentSnapshot, error) in
guard let datas = documentSnapshot else {return}
let firstname = datas.get("firstName") as! String
self.firstName = firstname
}

Else on If Else statement won't get triggered, can't understand why

I have this block of code:
func fetchFriends() {
if let window = UIApplication.shared.keyWindow {
guard let userId = Auth.auth().currentUser?.uid else { return }
DispatchQueue.main.async {
FirestoreService.shared.fetchFriendList(userId) { (fetchedFriends) in
//// WONT GET HERE ////
if fetchedFriends != nil {
self.fetchedFriends = fetchedFriends! // Can force unwrap here because we already know that fetchedFriends in not nil.
self.friendsTable.reloadData()
}else {
self.fetchedFriends = []
self.friendsTable.reloadData()
}
}
}
}
}
This block of code is using this function:
func fetchFriendList(_ id: String, completion: #escaping([Friend]?)->()) {
var fetchedFriends: [Friend] = []
db.collection(USERS_COLLECTION).document(id).getDocument { (doc, err) in
if err == nil && doc != nil {
guard let results = doc?.data()?[USER_FOLLOWING] as? [String: Any] else { return }
for result in results { // Getting the data in firebase
if let resultValue = result.value as? [String: Any] { // Getting only the value of the MAP data, we do not need the key.
//Getting the fields from the result
guard let id = resultValue[FRIEND_ID] as? String else { return }
guard let profilePic = resultValue[FRIEND_PROFILE_PIC] as? String else { return }
guard let username = resultValue[FRIEND_NAME] as? String else { return }
guard let email = resultValue[FRIEND_MAIL] as? String else { return }
//Creating a new Friend object from the fields
let friend = Friend(id: id, profilePicture: profilePic, username: username, email: email)
fetchedFriends.append(friend)
}
completion(fetchedFriends)
}
}else {
print(err!.localizedDescription)
completion(fetchedFriends)
}
}
}
Whats happening here, is that I'm going into a user's document, getting it's 'Friends' from a Map I have in the document, creating a Friend Array and sending it in the completion to the first function.
In the first function, I'm checking if what I got is nil, if not, I'm assigning it to an array, else, if it is nil, I want the array to be empty.
The purpose here is to show the "Friends" in the tableView if the user has any.
My problem is this situation:
For start, the list of friends is empty, adding a friend and viewing the list, the friend I just added is showing, which is good. the problem is, when I'm removing this friend from the list (and it is deleted in the Database in Firestore), showing the list again does not deletes him from the list and still showing it.
It seems that after removing a friend from the "following" section, and showing the list again, after FirestoreService.shared... it just returns and won't get to the "Won't get here" line.
The FetchFriends() function does gets called everytime I'm opening the FriendsList.
This is a picture of the list I'm referring to, this demouser is removed from the friends list but still showing up.
EDIT: Just noticed that when I have more than one user on the list, it does gets deleted and it works as I want. When I have just one user (or just one left on the list) it won't delete it.
fetchFriendList never calls the callback with a nil value:
var fetchedFriends: [Friend] = []
Therefore your else branch is unnecessary and the completion handler could be #escaping ([Friend]) -> Void without optionals.
By the way, there is also a situation when your method does not call completion at all:
guard let results = doc?.data()?[USER_FOLLOWING] as? [String: Any] else { return }
In general, there are many unsafe places. For example, when err is nil and doc is nil, then your else will crash unwraping err!.
A better alternative:
guard err == nil, let doc = doc else {
print(err?.localizedDescription)
completion([])
return
}
let results = (doc.data()?[USER_FOLLOWING] as? [String: Any]) ?? [:]
let fetchedFriends = results.compactMap { result in
guard
let resultValue = result.value as? [String: Any],
let id = resultValue[FRIEND_ID] as? String,
let profilePic = resultValue[FRIEND_PROFILE_PIC] as? String,
let username = resultValue[FRIEND_NAME] as? String,
let email = resultValue[FRIEND_MAIL] as? String
else { return nil }
return Friend(id: id, profilePicture: profilePic, username: username, email: email)
}
completion(fetchedFriends)

Firebase don't send me my value into my variable

I've got a code which normally should return to me a value from Firebase.
My Firebase struct is :
Experience{
UserId{
LDG_DAY: "4"
LDG_NIGHT: "0"
APCH_IFR: "0"
}
}
My code is :
func getUserExp(){
let ref = Database.database().reference()
let userID = Auth.auth().currentUser?.uid
let Date = self.flightDate.text
ref.child("Experience")/*.child(userID!)*/.observeSingleEvent(of: .value) {(snapshot) in
if snapshot.hasChild(userID!){
let value = snapshot.value as? NSDictionary
let ldg_day = value?["LDG_DAY"] as? String ?? "123"
let ldg_night = value?["LDG_NIGHT"] as? String ?? "0"
let apch_ifr = value?["APCH_IFR"] as? String ?? "0"
self.intLdgDay = Int(ldg_day)!
self.intLdgNight = Int(ldg_night)!
self.intApchIfr = Int(apch_ifr)!
print("string = \(ldg_day) int = \(self.intLdgDay)")
}
}
}
Now the code didn't work as I would like... In fact my code return the basic as? String ?? "123" value but the snapshot.value get the good value from firebase ...
What's wrong ? I use this code for many other part of my app and no problems about it ?
Thanks for your help
I believe you want to ensure the node exists before trying to read the child data.
NOTE:
I see the path to read has the uid commented out so it's unclear if you intended to read a single user (leaving in the uid) or if you actually wanted to load every user at one time (thousands). This answer assumes you are intending to read that specific user node only. See #Callam answer if you intended to read ALL of the users nodes at one time.
The code you have now is using snapshot.hasChild which looks within the node to see if the child, the users uid exists, and it doesn't so the code will always fail.
if snapshot.hasChild(userID!)
I think what you want to do is use snapshot.exists to ensure it's a valid node before reading. Here's the code:
let experienceRef = self.ref.child("Experience")
let usersExpRef = experienceRef.child(uid)
usersExpRef.observeSingleEvent(of: .value) { snapshot in
if snapshot.exists() {
let value = snapshot.value as! [String: Any]
let ldg_day = value["LDG_DAY"] as? String ?? "123"
print("string = \(ldg_day)")
} else {
print("the \(uid) node does not exist")
}
}
I would also suggest safely unwrapping options before attempting to work with them as they could be nil, and that would crash your code.
guard let thisUser = Auth.auth().currentUser else { return }
let uid = thisUser.uid
Note I also replaced the old objc NSDictionary with it's Swifty counterpart [String: Any]
Assuming your struct is from the root, and Experience contains more than one user ID, your code is currently observing the value for all user IDs since the /*.child(userID!)*/ is commented out.
Therefore you are requesting every user's experience and checking on the client if the current user exists as a child – this will succeed if the current user's ID is present at Experience/$uid.
ref.child("Experience")/*.child(userID!)*/.observeSingleEvent(of: .value) { (snapshot) in
if snapshot.hasChild(userID!) {
let value = snapshot.value as? NSDictionary
Now we have a snapshot with all Experiences and we've confirmed that it has a child for the current user's ID – we would need to get that child and cast the value of that to a dictionary.
let value = snapshot.childSnapshot(forPath: userID).value as? NSDictionary
This fixes the issue but obviously, we don't want to download every experience on a single user's device, and they maybe shouldn't even have the permission to request that reference location either.
So if you uncomment .child(userID!), the snapshot will be of just one Experience, so snapshot.hasChild(userID!) will fail. Instead, you can use snapshot.exists() and/or a conditional cast to determine if the snapshot for the userID is existent and/or thereby castable.
func getUserExp() {
let ref = Database.database().reference()
let userID = Auth.auth().currentUser?.uid
let Date = self.flightDate.text
ref.child("Experience").child(userID!).observeSingleEvent(of: .value) { snapshot in
if snapshot.exists() {
let value = snapshot.value as? [String:String]
let ldg_day = value?["LDG_DAY"] ?? "123"
let ldg_night = value?["LDG_NIGHT"] ?? "0"
let apch_ifr = value?["APCH_IFR"] ?? "0"
self?.intLdgDay = Int(ldg_day)!
self?.intLdgNight = Int(ldg_night)!
self?.intApchIfr = Int(apch_ifr)!
print("string = \(ldg_day) int = \(self.intLdgDay)")
} else {
print("experience for \(snapshot.key) doesn't exist")
}
}
}
You can clean this up a bit with a struct and extension.
// Experience.swift
struct Experience {
var ldg_day: String
var ldg_night: String
var apch_ifr: String
}
extension Experience {
static var currentUserRef: DatabaseReference? {
return Auth.auth().currentUser.flatMap {
return Database.database().reference(withPath: "Experience/\($0.uid)")
}
}
init?(snapshot: DataSnapshot) {
guard snapshot.exists() else { return nil }
let value = snapshot.value as? [String:String]
self.ldg_day = value?["LDG_DAY"] ?? "123"
self.ldg_night = value?["LDG_NIGHT"] ?? "0"
self.apch_ifr = value?["APCH_IFR"] ?? "0"
}
}
Et voilà,
func getUserExp() {
Experience.currentUserRef?.observeSingleEvent(of: .value, with: { [weak self] in
if let experience = Experience(snapshot: $0) {
self?.intLdgDay = Int(experience.ldg_day)!
self?.intLdgNight = Int(experience.ldg_night)!
self?.intApchIfr = Int(experience.apch_ifr)!
print("string = \(experience.ldg_day) int = \(self.intLdgDay)")
} else {
print("experience for \($0.key) doesn't exist")
}
})
}