Changing from Firebase to Firestore caused Fatal error: Unexpectedly found nil ... in a query - swift

I am trying to retrieve a user's data from firestore, after migrating my data there from firebase. The code below was working fine with firebase, and retrieved the user's data.
However, after changing the query to firestore query, I got this error.
Print statement here "document.data()" contains the data, But I got this error. I don't know where this error is coming from.
When I compare document.data() with nil, I got "Document data: contains nil".
I don't know how I suppoused to get the data.
here is the code where I get the error,
static func getUser(uid: String, setUserDefaults: #escaping (NormalUser) -> Void){
DataService.ds.REF_USERS_NORMAL.document(uid).getDocument { (document, error) in
if error != nil{
print("\(String(describing: error?.localizedDescription))")
}else{
if document != nil{
let data = document?.data() as! [String: String]
print("Document data: \(String(describing: document?.data() as! [String: String]))")
let user = NormalUser(userData: (data as Dictionary<String, AnyObject>))
setUserDefaults(user)
}else{
print("Document data: contains nil")
}
}
}
}
Here is how I defined the variables,
import Foundation
import Firebase
class NormalUser: User {
private var _email: String?
private var _city: String?
private var _country: String?
private var _name: String?
private var _phone: String?
private var _profileImgUrl: String?
var email: String {
return _email!
}
var city: String {
return _city!
}
var country: String {
return _country!
}
var name: String {
return _name!
}
var phone: String {
return _phone!
}
var profileImgUrl: String {
set{
self.profileImgUrl = _profileImgUrl!
}
get{
if let pI = _profileImgUrl{
return pI
}
return ""
}
}
init(userData: Dictionary<String, AnyObject>) {
super.init(userId: userData["uid"] as! String, user: userData)
if let email = userData["email"] as? String {
self._email = email
}
if let city = userData["city"] as? String {
self._city = city
}
if let country = userData["country"] as? String {
self._country = country
}
if let name = userData["name"] as? String {
self._name = name
}
if let phone = userData["phone"] as? String {
self._phone = phone
}
if let profileImgUrl = userData["imgUrl"] as? String {
self._profileImgUrl = profileImgUrl
}
}
static func createNormalUser(uid: String, userData: Dictionary<String, String>) {
// add user to database
//DataService.ds.REF_USERS_NORMAL.child(uid).setValue(userData)
DataService.ds.REF_USERS_NORMAL.document(uid).setData(userData) { (err) in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
addUserToGroup(uid:uid, group:"normal")
}
static func updateUserProfile(uid: String, userData: Dictionary<String, String>) {
//DataService.ds.REF_USERS_NORMAL.child(uid).updateChildValues(userData)
DataService.ds.REF_USERS_NORMAL.document(uid).updateData(userData)
}
static func getUser(uid: String, setUserDefaults: #escaping (NormalUser) -> Void){
DataService.ds.REF_USERS_NORMAL.document(uid).getDocument { (document, error) in
if error != nil{
print("\(String(describing: error?.localizedDescription))")
}else{
if document != nil{
let data = document?.data() as! [String: String]
print("Document data: \(String(describing: document?.data() as! [String: String]))")
let user = NormalUser(userData: (data as Dictionary<String, AnyObject>))
setUserDefaults(user)
}else{
print("Document data: contains nil")
}
}
}
}
}

I solved the issue!
For any one who may come across this error, the problem was with userId. I was getting the user id from firebase, which, in my case, no longer serves my query, and eventually getting a nil. When I got the ID directly from authentication,
let userID = Auth.auth().currentUser?.uid
It SOLVED the issue!

Related

How do I read a User's Firestore Map to a Swift Dictionary?

I have my user struct with has a dictionary of all their social medias.
struct User: Identifiable {
var id: String { uid }
let uid, email, name, bio, profileImageUrl: String
let numSocials, followers, following: Int
var socials: [String: String]
init(data: [String: Any]) {
self.uid = data["uid"] as? String ?? ""
self.email = data["email"] as? String ?? ""
self.name = data["name"] as? String ?? ""
self.bio = data["bio"] as? String ?? ""
self.profileImageUrl = data["profileImageURL"] as? String ?? ""
self.numSocials = data["numsocials"] as? Int ?? 0
self.followers = data["followers"] as? Int ?? 0
self.following = data["following"] as? Int ?? 0
self.socials = data["socials"] as? [String: String] ?? [:]
}
}
The idea is for socials (the dictionary), to be dynamic, since users can add and remove social medias. Firestore looks like this:
The dictionary is initialized as empty. I have been able to add elements to the dictionary with this function:
private func addToStorage(selectedMedia: String, username: String) -> Bool {
if username == "" {
return false
}
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
print("couldnt get uid")
return false
}
FirebaseManager.shared.firestore.collection("users").document(uid).setData([ "socials": [selectedMedia:username] ], merge: true)
print("yoo")
return true
}
However I can't seem to read the firestore map into my swiftui dictionary. I want to do this so that I can do a ForEach loop and list all of them. If the map is empty then the list would be empty too, but I can't figure it out.
Just in case, here is my viewmodel.
class MainViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var user: User?
init() {
DispatchQueue.main.async {
self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
}
fetchCurrentUser()
}
func fetchCurrentUser() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
print("FAILED TO FIND UID")
return
}
FirebaseManager.shared.firestore.collection("users").document(uid).getDocument { snapshot, error in
if let error = error {
self.errorMessage = "failed to fetch current user: \(error)"
print("failed to fetch current user: \(error)")
return
}
guard let data = snapshot?.data() else {
print("no data found")
self.errorMessage = "No data found"
return
}
self.user = .init(data: data)
}
}
}
TLDR: I can't figure out how to get my firestore map as a swiftui dictionary. Whenever I try to access my user's dictionary, the following error appears. If I force unwrap it crashes during runtime. I tried to coalesce with "??" but I don't know how to make it be the type it wants.
ForEach(vm.user?.socials.sorted(by: >) ?? [String:String], id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
error to figure out
Please be patient. I have been looking for answers through SO and elsewhere for a long time. This is all new to me. Thanks in advance.
This is a two part answer; Part 1 addresses the question with a known set of socials (Github, Pinterest, etc). I included that to show how to map a Map to a Codable.
Part 2 is the answer (TL;DR, skip to Part 2) so the social can be mapped to a dictionary for varying socials.
Part 1:
Here's an abbreviated structure that will map the Firestore data to a codable object, including the social map field. It is specific to the 4 social fields listed.
struct SocialsCodable: Codable {
var Github: String
var Pinterest: String
var Soundcloud: String
var TikTok: String
}
struct UserWithMapCodable: Identifiable, Codable {
#DocumentID var id: String?
var socials: SocialsCodable? //socials is a `map` in Firestore
}
and the code to read that data
func readCodableUserWithMap() {
let docRef = self.db.collection("users").document("uid_0")
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: UserWithMapCodable.self)
print(user.socials) //the 4 socials from the SocialsCodable object
}
}
}
Part 2:
This is the answer that treats the socials map field as a dictionary
struct UserWithMapCodable: Identifiable, Codable {
#DocumentID var id: String?
var socials: [String: String]?
}
and then the code to map the Firestore data to the object
func readCodableUserWithMap() {
let docRef = self.db.collection("users").document("uid_0")
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: UserWithMapCodable.self)
if let mappedField = user.socials {
mappedField.forEach { print($0.key, $0.value) }
}
}
}
}
and the output for part 2
TikTok ogotok
Pinterest pintepogo
Github popgit
Soundcloud musssiiiccc
I may also suggest taking the socials out of the user document completely and store it as a separate collection
socials
some_uid
Github: popgit
Pinterest: pintepogo
another_uid
Github: git-er-done
TikTok: dancezone
That's pretty scaleable and allows for some cool queries: which users have TikTok for example.

Firestore Swift update text realtime

I have this way of collecting information.
struct MainText {
var mtext: String
var memoji: String
}
class MainTextModel: ObservableObject {
#Published var maintext : MainText!
init() {
updateData()
}
func updateData() {
let db = Firestore.firestore()
db.collection("maintext").document("Main").getDocument { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
let memoji = snap?.get("memoji") as! String
let mtext = snap?.get("mtext") as! String
DispatchQueue.main.async {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}
}
}
And such a way of displaying.
#ObservedObject private var viewModel = MainTextModel()
self.viewModel.maintext.memoji
self.viewModel.maintext.mtext
How can I update online without rebooting the view?
Instead of using getDocument, which only gets the document once and doesn't return updates, you'll want to add a snapshot listener.
Here's the Firestore documentation for that: https://firebase.google.com/docs/firestore/query-data/listen
In your case, you'll want to do something like:
db.collection("maintext").document("Main")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
if let memoji = data["memoji"] as? String, let mtext = data["mtext"] as? String {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}

Swift: Issue getting data from Firestore into an array

Completion Func to get data from firestore
struct User {
let docID: String
let firstName: String
}
var userArray = [User]()
// ...
//....
func getUserData(completion: #escaping ([User]) -> Void) {
var result = [User]()
guard let userID = Auth.auth().currentUser?.uid else {return}
let db = Firestore.firestore()
db.collection("users").whereField("uid", isEqualTo: userID)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
completion(result)
return
}
else {
if let snapshot = querySnapshot {
for document in snapshot.documents {
let data = document.data()
let docid = data["docID"] as? String ?? ""
let firstName = data["firstName"] as? String ?? ""
let lastName = data["lastName"] as? String ?? ""
let email = data["email"] as? String ?? ""
let UData = User(docID: docid, firstName: firstName)
result.append(UData)
print(result)
}
}
} //end ELSE
completion(result)
}
}
Calling it here in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
getUserData{ (result) in
self.userArray.append(contentsOf: result)
print(self.userArray)
}
}
Prints the following
[DaDoc.HomeViewController.User(docID: "avGhsRI1x0c", firstName: "Raul")]
Question
Why is "DaDoc.HomeViewController.User" This being appended to the array?
i can't unwrap it with "!" throws an error "Cannot force unwrap value of non-optional type '[HomeViewController.User]'"
How can i access keys in here (docID, firstName)
[DaDoc.HomeViewController.User(docID: "avGhsRI1x0c", firstName: "Raul")]
Anyhelp is appreciated!
UPDATE:
Thanks for the suggestions #jawadAli - Moving struct outside the class worked
Even thought my result still printed as below
[DaDoc.User(docID: "avGhsRI1x0c", firstName: "Raul")].
I was able to access keys to add to values to a tableView like below:
cell.firstNameLabel?.text = userArray[indexPath.row].firstName

Firebase - How do I read this map via embedded structs?

I am reading data from Firestore to be able to populate into expanding tableview cells. I have a really simple struct:
protocol PlanSerializable {
init?(dictionary:[String:Any])
}
struct Plan{
var menuItemName: String
var menuItemQuantity: Int
var menuItemPrice: Double
var dictionary: [String: Any] {
return [
"menuItemName": menuItemName,
"menuItemQuantity": menuItemQuantity,
"menuItemPrice": menuItemPrice
]
}
}
extension Plan : PlanSerializable {
init?(dictionary: [String : Any]) {
guard let menuItemName = dictionary["menuItemName"] as? String,
let menuItemQuantity = dictionary["menuItemQuantity"] as? Int,
let menuItemPrice = dictionary["menuItemPrice"] as? Double
else { return nil }
self.init(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity, menuItemPrice: menuItemPrice)
}
}
And this is embedded in this struct:
protocol ComplainSerializable {
init?(dictionary:[String:Any])
}
struct Complain{
var status: Bool
var header: String
var message: String
var timeStamp: Timestamp
var email: String
var planDetails: Plan
var dictionary: [String: Any] {
return [
"status": status,
"E-mail": header,
"Message": message,
"Time_Stamp": timeStamp,
"User_Email": email,
"planDetails": planDetails
]
}
}
extension Complain : ComplainSerializable {
init?(dictionary: [String : Any]) {
guard let status = dictionary["status"] as? Bool,
let header = dictionary["E-mail"] as? String,
let message = dictionary["Message"] as? String,
let timeStamp = dictionary["Time_Stamp"] as? Timestamp,
let email = dictionary["User_Email"] as? String,
let planDetails = dictionary["planDetails"] as? Plan
else { return nil }
self.init(status: status, header: header, message: message, timeStamp: timeStamp, email: email, planDetails: planDetails)
}
}
However, I am not able to query any data from Firestore which looks like this:
Here is my query, although I am just reading all the files:
let db = Firestore.firestore()
var messageArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
self.messageArray = documentSnapshot!.documents.compactMap({Complain(dictionary: $0.data())})
for plan in self.messageArray {
print("\(plan.email)")
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
What am I doing wrong?
EDIT:
As suggested, here is the updated embedded struct:
import Foundation
// MARK: - Complain
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: PlanDetails
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
Using quicktype.io, you can generate the struct. From there, all you need to do is run this tiny fragment of code within your response handler.
var compainArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
guard let snapshot = documentSnapshot else {return}
for document in snapshot.documents {
if let jsonData = try? JSONSerialization.data(withJSONObject: document.data()){
if let converted = try? JSONDecoder().decode(Complain.self, from: jsonData){
self.compainArray.append(converted)
}
}
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
Which will handle the looping, and mapping of certain variables. Let me know if you have any trouble with this.

Always getting nil in completion

I'm trying to get Map data I have in Firestore, this is how it looks:
I'm trying to get the data, and create an array of Friend Object and return the array in the completion handler.
This is what I have:
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(nil)
}
}
}
I tried printing the results, and resultValue etc, they are not nil.
But, after trying to append and print the fetchedFriends Array, I get nil, and the completion is also nil.
I don't really understand why this is happening.
The problem is that you haven't initialized variable fetchedFriends and you have used optional type when appending data to it. Since it has not been initialized, it will skip appending to it. You should initialize it in the beginning. The updated code would be as follows.
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(nil)
}
}
}
Hope it helps.