Swift Firebase returning empty array - swift

I am getting an empty array back from firestore. Specifically from the sub model "Coor" inside of "EventModel".
Data Model:
struct EventModel: Identifiable, Codable {
var id: String
var isLive: Bool
var coors: [Coor]
var eventCenterCoor: [Double]
var hostTitle: String
var eventName: String
var eventDescription: String
var isEventPrivate: Bool
var eventGuestsJoined: [String]
var eventEndDate: String
private enum CodingKeys: String, CodingKey {
case id
case isLive
case coors
case eventCenterCoor
case hostTitle
case eventName
case eventDescription
case isEventPrivate
case eventGuestsJoined
case eventEndDate
}
}
struct Coor: Identifiable, Codable {
var id = UUID()
var coorDoubles: [Double]
private enum CodingKeys: String, CodingKey {
case coorDoubles
}
}
Firestore request:
public func getEventData(completion: #escaping (_ eventModel: [EventModel]) -> Void) {
var eventRef: [EventModel] = []
self.isLoading = true
self.loadingMess = "Finding events.."
self.eventsDataCollection.whereField("isLive", isEqualTo: true)
.getDocuments { (document, error) in
if let document = document, error == nil {
for doc in document.documents {
let data = doc.data()
let id = data["id"] as? String ?? ""
let isLive = data["isLive"] as? Bool ?? false
let coors = data["coors"] as? [Coor] ?? []
let eventCenterCoor = data["eventCenterCoor"] as? [Double] ?? []
let hostTitle = data["hostTitle"] as? String ?? ""
let eventName = data["eventName"] as? String ?? ""
let eventDescription = data["eventDescription"] as? String ?? ""
let isEventPrivate = data["isEventPrivate"] as? Bool ?? false
let eventGuestsJoined = data["eventGuestsJoined"] as? [String] ?? []
let eventEndDate = data["eventEndDate"] as? String ?? ""
eventRef.append(EventModel(id: id, isLive: isLive, coors: coors, eventCenterCoor: eventCenterCoor, hostTitle: hostTitle, eventName: eventName, eventDescription: eventDescription, isEventPrivate: isEventPrivate, eventGuestsJoined: eventGuestsJoined, eventEndDate: eventEndDate))
}
completion(eventRef)
} else if error != nil {
print(error ?? "Error getting events where 'isLive' == true")
self.isLoading = false
}
}
}
Here is the firestore data:
Printing "coors" returns an empty array. The empty array is because this line:
let coors = data["coors"] as? [Coor] ?? []
is defaulting to the empty array. This must mean the data type of [Coor] is incorrect?

You are decoding an array called coorsDouble with this line:
let coorsDouble = data["coorsDouble"] as? [Coor] ?? []
If you look at your data structure, each element is made up of this structure:
{ coolDouble: [Double] }
But, in your Coor model, you have this structure:
struct Coor: Identifiable, Codable {
var id = UUID()
var coor: [Double]
private enum CodingKeys: String, CodingKey {
case coor
}
}
In order to decode the Coor, you'd need its structure to match. So, change it to:
struct Coor: Identifiable, Codable {
var id = UUID()
var coorDouble: [Double]
private enum CodingKeys: String, CodingKey {
case coorDouble
}
}
Next, CodingKeys only take effect when using Codable. Right now, you're just trying to cast a dictionary value (of type Any) to Coor, which isn't going to work. In general, I'd say you should use Codable with your entire structure, but if you wanted to avoid it for some reason, you'd need to do this:
//replacing let coorsDouble = data["coorsDouble"] as? [Coor] ?? []
let coorsDoubleArray = data["coorsDouble"] as? [[String:Any]] ?? []
let coorsDouble = coorsDoubleArray.compactMap { item in
guard let doublesArray = item["coorDoubles"] as? [Double] else {
return nil
}
return Coor(coorDoubles: doublesArray)
}

Related

How can I fetch data from a Firestore reference?

I have the chat collection that has the following fields: hasUnreadMessage as a Bool, isActive as a Bool, person as a reference to
person collection, messages as an array of references to message collection.
Here are some screenshots
I want to create a function to fetch all the messages but for example in the person reference when I print directly the call of the imgString or name it's correct, but when I add them to the ChatModel they are missing.
Here is the function that I created.
func fetchMessages() {
db.collection("chat").getDocuments { snapshot, err in
if let error = err {
debugPrint("Error fething documents: \(error)")
} else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
var chatMessages: [Message] = []
var chat: ChatModel = ChatModel(person: Person(name: "", imgString: ""), messages: [], hasUnreadMessage: false, isActive: false)
let personRef = (document.get("person") as? DocumentReference ?? nil)?.getDocument(completion: { personSnapshot, personErr in
if let personError = personErr {
debugPrint("Error getting chat person: \(personError)")
} else {
guard let personSnap = personSnapshot else { return }
let personData = personSnap.data()
chat.person.imgString = personData?["imgString"] as? String ?? ""
chat.person.name = personData?["name"] as? String ?? ""
}
})
let messages = document.get("messages") as? [DocumentReference] ?? []
for message in messages {
message.getDocument { messageSnapshot, messageErr in
if let messageError = messageErr {
debugPrint("Error getting message: \(messageError)")
} else {
guard let messageSnap = messageSnapshot else { return }
let messageData = messageSnap.data()
let date = messageData?["date"] as? String ?? ""
let text = messageData?["text"] as? String ?? ""
let type = messageData?["type"] as? String ?? ""
chatMessages.append(Message(text, type: type, date: date))
}
}
}
chat.hasUnreadMessage = data["hasUnreadMessage"] as? Bool ?? false
chat.isActive = data["isActive"] as? Bool ?? false
chat.messages = chatMessages
self.chats.append(chat)
}
}
}
}
Edit:
Here are the outputs. I saw that I called the imgString print before the ChatModel print and they appeared in the opposite order
BT_Tech.ChatModel(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("D195D8D8-F401-4C71-B571-4877E6574B68")), person: BT_Tech.Person(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("46768262-EB88-42B3-A9FF-4F9FF3A7B7F1")), name: "", imgString: ""), messages: [], hasUnreadMessage: true, isActive: true)
"imgString: girl1"
Here is the ChatModel
struct ChatModel: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var person: Person
var messages: [Message]
var hasUnreadMessage: Bool
var isActive: Bool
}
struct Person: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var name: String
var imgString: String
}
struct Message: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var date: String
var text: String
var type: String
init(_ text: String, type: String, date: String) {
self.text = text
self.type = type
self.date = date
}
init(_ text: String, type: String) {
self.init(text, type: type, date: "")
}
}

Unpacking Firestore array with objects to a model in swift

I have a project in swift with Firestore for the database. My firestore dataset of a user looks like this. User details with an array that contains objects.
I have a function that gets the specifick user with all firestore data:
func fetchUser(){
db.collection("users").document(currentUser!.uid)
.getDocument { (snapshot, error ) in
do {
if let document = snapshot {
let id = document.documentID
let firstName = document.get("firstName") as? String ?? ""
let secondName = document.get("secondName") as? String ?? ""
let imageUrl = document.get("imageUrl") as? String ?? ""
let joinedDate = document.get("joinedDate") as? String ?? ""
let coins = document.get("coins") as? Int ?? 0
let challenges = document.get("activeChallenges") as? [Challenge] ?? []
let imageLink = URL(string: imageUrl)
let imageData = try? Data(contentsOf: imageLink!)
let image = UIImage(data: imageData!) as UIImage?
let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 }
print("array without opt", arrayWithNoOptionals)
self.user = Account(id: id, firstName: firstName, secondName: secondName, email: "", password: "", profileImage: image ?? UIImage(), joinedDate: joinedDate, coins: coins, activeChallenges: challenges)
}
else {
print("Document does not exist")
}
}
catch {
fatalError()
}
}
}
This is what the user model looks like:
class Account {
var id: String?
var firstName: String?
var secondName: String?
var email: String?
var password: String?
var profileImage: UIImage?
var coins: Int?
var joinedDate: String?
var activeChallenges: [Challenge]?
init(id: String, firstName: String,secondName: String, email: String, password: String, profileImage: UIImage, joinedDate: String, coins: Int, activeChallenges: [Challenge]) {
self.id = id
self.firstName = firstName
self.secondName = secondName
self.email = email
self.password = password
self.profileImage = profileImage
self.joinedDate = joinedDate
self.coins = coins
self.activeChallenges = activeChallenges
}
init() {
}
}
The problem is I don't understand how to map the activeChallenges from firestore to the array of the model. When I try : let challenges = document.get("activeChallenges") as? [Challenge] ?? []
The print contains an empty array, but when i do: let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 } print("array without opt", arrayWithNoOptionals)
This is the output of the flatmap:
it returns an optional array
System can not know that activeChallenges is array of Challenge object. So, you need to cast it to key-value type (Dictionary) first, then map it to Challenge object
let challengesDict = document.get("activeChallenges") as? [Dictionary<String: Any>] ?? [[:]]
let challenges = challengesDict.map { challengeDict in
let challenge = Challenge()
challenge.challengeId = challengeDict["challengeId"] as? String
...
return challenge
}
This is the same way that you cast snapshot(document) to Account object

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.

Change a value in my UserModel (class) based on a userid

I have a UserModel:
class UserModel {
var uid: String?
var username : String?
var email: String?
var profileImageUrl: String?
var dateOfBirth: String?
var registrationDate: Int?
var isFollowing: Bool?
var accessLevel: Int?
var onlineStatus: Bool?
init(dictionary: [String : Any]) {
uid = dictionary["uid"] as? String
username = dictionary["username"] as? String
email = dictionary["email"] as? String
profileImageUrl = dictionary["profileImageUrl"] as? String
dateOfBirth = dictionary["dateOfBirth"] as? String
registrationDate = dictionary["userRegistrationDate"] as? Int
accessLevel = dictionary["accessLevel"] as? Int
onlineStatus = dictionary["onlineStatus"] as? Bool
}
}
And I also have a value like [12ih12isd89 : True]
I want to change the value "onlineStatus" for the user "12ih12isd89" to True and I thought the right way to do this is updateValue(:forKey:). But my class UserModel does not have updateValue(:forKey:).
How can I use this in my existing model?
Edit:
How I get the data:
func fetchAllUsers (completion: #escaping ([UserModel]) -> Void) {
let dispatchGroup = DispatchGroup()
var model = [UserModel]()
let db = Firestore.firestore()
let docRef = db.collection("users")
dispatchGroup.enter()
docRef.getDocuments { (querySnapshot, err) in
for document in querySnapshot!.documents {
let dic = document.data()
model.append(UserModel(dictionary: dic))
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(model)
}
}
To me it looks like you need to find the right object in the array and update the property
let dict = ["12ih12isd89" : true]
var model = [UserModel]()
if let user = model.first(where: {$0.uid == dict.keys.first!}) {
user.onlineStatus = dict.values.first!
}
Depending on what ["12ih12isd89" : true] really is you might want to change the access from dict.keys.first! that I used
If your value dictionary contains more than one user, you can use a for loop like this:
var model = [UserModel]()
//Some initalization ...
let values = ["12ih12isd89" : true]
for (k, v) in values {
model.filter({$0.uid == k}).first?.onlineStatus = v
}

Get data from Firestore and properly append to array

I'm trying to fetch data from Firestore. I've already got the following code but how do I properly append to shelters?
Current error:
Value of type '[String : Any]' has no member 'title'
class FirebaseSession: ObservableObject {
#Published var shelters: [Shelter] = []
let ref = Firestore.firestore().collection("shelters")
getShelters() {
ref.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let value = document.data()
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
self.$shelters.append(shelter)
}
}
}
}
}
class Shelter {
var id: Int
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
init?(id: Int, title: String, image: String, availableSpaces: Int, distance: Double, gender: String?) {
if id < 0 || title.isEmpty || image.isEmpty || availableSpaces < 0 || distance < 0 {
return nil
}
self.id = id
self.title = title
self.image = image
self.availableSpaces = availableSpaces
self.distance = distance
self.gender = gender
}
}
EDIT:
let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "")
let shelter = Shelter(id: Int(value.id), title: value.title, image: value.image, availableSpaces: value.available, distance: value.distance, gender: value.gender)
Here value is of type [String:Any]. So you cant do value.title . You need to do value["title"] as? String ?? "" and Similarly for id,image,distance,etc.
So the final code becomes:
let shelter = Shelter(id: Int(value["id"], title: value["title"], image: value["image"], availableSpaces: value["available"], distance: value["distance"], gender: value["gender"])
Downcast it accordingly.
UPDATE
replace your code with this
if let shelter = Shelter(id: value["id"] as? Int ?? -1, title: value["title"] as? String ?? "", image: value["image"] as? String ?? "", available: value["available"] as? Int ?? -1, distance: value["distance"] as? Double ?? -1, gender: value["gender"] as? String ?? "") {
self.shelters.append(shelter)
} else {
print("provided data is wrong.")
}
There are a number of issues with the original code:
Instead of implementing a function to fetch the shelters, the following snippet in your code creates a computed property - not sure this is what you intended:
getShelters() {
...
}
I'd recommend replacing this with a proper function.
No need to use a class for your data model - especially as you seem to be using SwiftUI.
Instead of mapping the fetched documents manually (and having to deal with nil values, type conversion etc. yourself, I'd recommend using Firestore's Codable support.
I've written about this extensively in my article SwiftUI: Mapping Firestore Documents using Swift Codable - Application Architecture for SwiftUI & Firebase | Peter Friese.
Here's how your code might look like when applying my recommendations:
struct Shelter: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var image: String
var availableSpaces: Int
var distance: Double
var gender: String?
}
class FirebaseSession: ObservableObject {
#Published var shelters = [Shelter]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
deinit {
unsubscribe()
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("shelters").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.shelters = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Shelter.self)
}
}
}
}
}
Note that:
we're able to do away with the constructor for Shelter
the shelter property will now be automatically updated whenever a shelter is added to the shelter collection in Firestore
the code won't break if a document doesn't match the expected data structure
I marked the Shelter struct as identifiable, so that you can directly use it inside a List view. #DocumentID instructs Firestore to map the document ID to the respective attribute on the struct.