How can I fetch data from a Firestore reference? - swift

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: "")
}
}

Related

Swift: Downcasting a Binding within a List while retaining link

I have the following view code (unworking):
import SwiftUI
struct SearchFilterView: View {
#Binding var filters: [any SourceFilter]
var body: some View {
List($filters, id: \.id) { filter in
switch filter.wrappedValue {
case var textFilter as SourceTextFilter:
TextField(
textFilter.name,
text: Binding(get: { textFilter.value }, set: { textFilter.value = $0 })
)
case var toggleFilter as SourceToggleFilter:
Toggle(
toggleFilter.name,
isOn: Binding(get: { toggleFilter.value }, set: { toggleFilter.value = $0 })
)
case var segmentFilter as SourceSegmentFilter:
Picker(
segmentFilter.name,
selection: Binding(get: { segmentFilter.value }, set: { segmentFilter.value = $0 })
) {
ForEach(segmentFilter.selections, id: \.self) { selection in
Text(selection).tag(selection)
}
}.pickerStyle(.segmented)
default:
EmptyView()
}
}
}
}
The code above compiles, but even though the values change for each filter, as soon as the view reloads, all of the changes are lost. The code for the SourceFilters are below.
protocol JSObjectEncodable {
var object: [String: Any] { get }
}
protocol JSObjectDecodable {
init?(from object: [String: Any])
}
protocol JSObjectCodable: JSObjectDecodable, JSObjectEncodable {}
protocol SourceFilter<ValueType>: JSObjectCodable, Sendable {
associatedtype ValueType
var id: String { get }
var value: ValueType { get set }
var name: String { get }
}
struct SourceTextFilter: SourceFilter {
init?(from object: [String: Any]) {
guard let id = object["id"] as? String,
let value = object["value"] as? String,
let name = object["name"] as? String else { return nil }
self.id = id
self.value = value
self.name = name
}
let id: String
var value: String
let name: String
var object: [String: Any] {
[
"id": id,
"value": value,
"name": name
]
}
}
struct SourceToggleFilter: SourceFilter {
init?(from object: [String: Any]) {
guard let id = object["id"] as? String,
let value = object["value"] as? Bool,
let name = object["name"] as? String else { return nil }
self.id = id
self.value = value
self.name = name
}
let id: String
var value: Bool
let name: String
var object: [String: Any] {
[
"id": id,
"value": value,
"name": name
]
}
}
struct SourceSegmentFilter: SourceFilter {
init?(from object: [String: Any]) {
guard let id = object["id"] as? String,
let value = object["value"] as? String,
let name = object["name"] as? String,
let selections = object["selections"] as? [String] else { return nil }
self.id = id
self.value = value
self.name = name
self.selections = selections
}
let id: String
var value: String
let name: String
let selections: [String]
var object: [String: Any] {
[
"id": id,
"value": value,
"name": name,
"selections": selections
]
}
}
How do I have it so that my changes to the filter values are saved, even after being downcast to their respective filter type? Thanks in advance.

How To Remove and Add Double Nested Elements in Firestore Array

I have a nested Codable Object In another object array in another object. I don't see how I can use FieldValue.arrayRemove[element]. Anyone know to do this? Thanks. I am trying to make it so that I can remove a cardField element in the LevelCard element in the job array.
Here is my code
struct Job: Identifiable, Codable {
var id: String? = UUID().uuidString
var uid: String = ""
var title: String = ""
var description: String = ""
var images: [ImagesForJob] = []
var levelCards: [LevelCard] = []
var tags: [Tag] = []}
struct LevelCard: Identifiable, Codable {
var id = UUID().uuidString
var name: String = ""
var color: String = "A7D0FF"
var fields: [CardField] = []}
struct CardField: Identifiable, Codable {
var id = UUID().uuidString
var name: String = ""
var value: String = ""
var type: FieldType = .Text}
func removeExistingCard(id: String, card: LevelCard) {
var data: [String: Any] = ["":""]
do {
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(card)
data = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String : Any]
} catch {
print("Error encoding account info\(error.localizedDescription)")
}
db
.collection("listings")
.document(id)
.updateData(["levelCards": FieldValue.arrayRemove([data])]) {err in
if let err = err {
withAnimation {
self.errMsg = "Failed to delete card: \(err.localizedDescription)"
self.showErrMsg = true
}
return
}
self.getUsrLstngs()
}
}
func removeExistingField(id: String, field: CardField) {
var data: [String: Any] = ["":""]
do {
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(field)
data = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String : Any]
} catch {
print("Error encoding account info\(error.localizedDescription)")
}
db
.collection("listings")
.document(id)
.updateData(["levelCards": FieldValue.arrayRemove([data])]) {err in
if let err = err {
withAnimation {
self.errMsg = "Failed to delete card: \(err.localizedDescription)"
self.showErrMsg = true
}
return
}
self.getUsrLstngs()
}
}
Also, Bonus, Does anyone know how to ignore the "id" variable when encoding all of my objects to Firestore? Thanks again.

Swift Firebase returning empty array

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)
}

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

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.