Handling documentID in FirebaseFirestoreSwift is really confusing - swift

I have to query a slew of collections and the models are always defined something like this:
struct Order : Identifiable, Codable {
#DocumentID var id : String?
let fieldOne : String?
let fieldTwo : Int?
enum CodingKeys : String, CodingKey {
case id // (had to comment this out)
case fieldOne
case fieldTwo
}
}
Today, I spent all day trying to figure out why I couldn't load documents from for one particular collection. I was getting a snapshot with documents but could not convert and populate them into an array. After hours of trial and error I commented out the "case id" in the enum and got it to work.
Any idea why this is happening?
Here's a query which works WITH the case id:
listener = db.collection("Meal_Plans").whereField("userId", isEqualTo: userEmail).order(by: "timeOfCreation", descending: true).addSnapshotListener({ (querySnapshot, error) in
if let error = error {
print("error in mp query: \(error.localizedDescription)")
} else {
guard let documents = querySnapshot?.documents else {
print("No mealplans")
return
}
let mealplanArray: [Mealplan] = documents.compactMap { queryDocumentSnapshot -> Mealplan? in
return try? queryDocumentSnapshot.data(as: Mealplan.self)
}
let planViewModel = mealplanArray.map({return PlanViewModel(mealplan: $0)})
DispatchQueue.main.async {
if mealplanArray.count > 0 {
self.planViewModelDelegate?.plansFetched(self.updateHour(sourcePlans: planViewModel))
}
}
}
})
And this is the one WITHTOUT:
listener = db.collection("Placed_Orders").whereField("userId", isEqualTo: userId).whereField("status", isLessThan: 410).order(by: "status", descending: false).order(by: "nextOrderDate", descending: false).addSnapshotListener({ (documentSnapshot, error) in
if let error = error {
print("error")
self.orderCallback?(error.localizedDescription, nil, .error)
} else {
print("empty")
guard let documents = documentSnapshot?.documents else {
return }
if documents.isEmpty {
self.orderCallback?("No orders found", nil, .error)
return
} else {
print("snapshot count: \(documents.count)")
let orderArray: [Order] = documents.compactMap { queryDocumentSnapshot -> Order? in
return try? queryDocumentSnapshot.data(as: Order.self)
}
let orderViewModel = orderArray.map({ return OrderViewModel(order: $0)})
print("array count: \(orderArray.count)")
DispatchQueue.main.async {
print("status: \(orderViewModel)")
self.orderCallback?(nil, orderViewModel, .success)
}
}
}
})
The differences are rather subtle. In both cases I am using a snapshot listener to query the snapshot and populate it into an array and then map that array into a view model.
Yet, in the latter case, I have to comment out the case id for the identifiable field. I need the ID so need to see if it's working but would like to know why I have to comment out the case id.

Related

SiwftUI Firebase modifying variable inside snapshot change not working

The following is a function to add a listener to a query. Whenever a document is added/removed I make some changes on two arrays (one of the user Ids and one of the user details). As you can see I tried printing everything: I correctly receive the data whenever it is added/removed, I can retrieve the document ID I need but whenever I append it to the usersReqestedUIDs array it always prints it as empty, even if I try to append a random string in it. Why is that?
func addRequestedUsersSnapshot() {
let db = Firestore.firestore()
let userRef = db.collection("user").document(user.UID)
let userRequestedRef = userRef.collection("friends").whereField("status", isEqualTo: "request")
// First query to fetch all friendIDs
userRequestedRef.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: (error!)")
return
}
snapshot.documentChanges.forEach { diff in
print("ids : (self.usersReqestedUIDs)")
print("type : (diff.type)")
if diff.type == .added {
print("doc id (diff.document.documentID)")
self.usersReqestedUIDs.append("hello")
print("added (diff.document.data())")
print("ids : (self.usersReqestedUIDs)")
self.fetchUserDetailsByUID(uid: diff.document.documentID) { result in
switch result {
case let .success(user):
self.usersReqestedDetails.append(user)
case let .failure(error):
print(error)
}
}
}
if diff.type == .removed {
print("removed (diff.document.data())")
self.usersReqestedDetails.removeAll(where: { $0.UID == diff.document.documentID })
self.usersReqestedUIDs.removeAll(where: { $0 == diff.document.documentID })
}
if diff.type == .modified {
print("modified (diff.document.data())")
}
}
}
}

Sorting fetched data from Firebase in Swift

When I retrieving data from firebase I tried to sort by price to descending order. but its not working. I tried the below query.
func sortPriceMaxtoMin(){
db.collection("Ads").order(by: "price", descending: true).getDocuments(){ (QuerySnapshot, error) in
if let error = error {
print("error getting documents")
}
else{
self.ads1 = QuerySnapshot!.documents.map({ queryDocumentSnapshot in
let val = try! queryDocumentSnapshot.data(as: Ad.self)
print("id is : \(val.id)=> title: \(val.title)")
return AdsData(title: val.title, seller: val.seller, image: val.image, price: val.price
, district: val.district,address: val.address, landSize: val.landSize)
}
)}
}
}

Removing an array item from Firestore

I've spent days researching this including various answers like: Firebase Firestore: Append/Remove items from document array
but can't work out how to actually get this working.
I have two structs:
struct TestList : Codable {
var title : String
var color: String
var number: Int
}
struct TestGroup: Codable {
var items: [TestList]
}
I am able to add data using FieldValue.arrayUnion:
#objc func addNewItem() {
let testList = TestList(title: "Testing", color: "blue", number: Int.random(in: 1..<999))
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.updateData([
"items" : FieldValue.arrayUnion([["title":testList.title,
"color":testList.color,
"number":testList.number]])
])
}
The above works as reflected in the Firestore dashboard:
But if I try and remove one of the items in the array, it just doesn't work.
#objc func removeItem() {
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayUnion([["title" : itemToRemove.title,
"color" : itemToRemove.color,
"number" : itemToRemove.number]])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I have printed the itemToRemove to the log to check that it is correct and it is. But it just doesn't remove it from Firestore. There is no error returned, yet the "successfully deleted" is logged.
Note the above is test code as I've simplified what I actually need just for testing purposes because I can't get this working.
Any ideas on what I'm doing wrong here?
You have to use arrayRemove to remove items from arrays.
#objc func removeItem() {
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayRemove([["title" : itemToRemove.title,
"color" : itemToRemove.color,
"number" : itemToRemove.number]])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I've encountered situations where this straightforward approach didn't work because the item was a complex object, in which case I first had to query for the item from Firestore and plug that instance into arrayRemove() to remove it.
The reason your approach doesn't have any side effects is because arrays in Firestore are not like arrays in Swift, they are hybrids of arrays and sets. You can initialize an array in Firestore with duplicate items but you cannot append arrays using arrayUnion() with duplicate items. Trying to append a duplicate item using arrayUnion() will silently fail, such as in your case.

Firestore Querying a Snapshot within a Snapshot?

I am trying to listen for any notifications whenever someone has replied to a post that a user has commented on. Below is how my database structure looks.
Posts: (Collection)
Post 1: (Document)
replies: [user2, user3]
Replies: (Collection)
Reply 1: (Document)
ownerId: [user2]
Reply 2: (Document)
ownerId: [user3]
Currently my code has 2 snapshot listeners. The first one listens to the Posts collections, where a user is inside the 'replies' array. Then the second one listens to the Replies collection, where it returns all documents added that != the current user. When a new reply has been detected, it will set the Tab Bar item's badge.
This currently works right now, but I am curious if there is a better method of doing so.
func getNotifications() {
database.collection("Posts")
.whereField("replies", arrayContains: userData["userId"]!)
.order(by: "timestamp", descending: true)
.limit(to: 70)
.addSnapshotListener() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { documentd in
if (documentd.type == .added) {
let dataTemp = documentd.document.data()
let ifUser = dataTemp["ownerId"] as! String
if(ifUser == self.userData["userId"]!) {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: dataTemp["timestamp"] as! Int)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
else {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("ownerId", isEqualTo: self.userData["userId"]!)
.order(by: "timestamp", descending: false)
.limit(to: 1)
.getDocuments() { (querySnapshot2, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
var timestamp = Int()
for documentde in querySnapshot2!.documents {
let temp = documentde.data()
timestamp = temp["timestamp"] as! Int
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: timestamp)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Your code does not have only two listeners, but one listener per post for which the user you are interested in has ever replied for. This will lead to terrible performances very soon and will potentially crash you app as Firestore has a limitation of 100 listeners per client.
I would advise to redesign your data model:
Only one listener on posts that the user has ever replied to (your first listener)
On each reply increment a reply counter in the post doc, this will trigger the snapshot above.
Optimisation 1: on each action on a post you could set a field lastactiontype, which would have a specific value reply for replies only. This way the snapshot is only triggered on replies.
Optimisation 2: set a field timestamp on each action to the current time and only pull the last n (for instance 10) posts in your snapshots, this will limit the number of reads at load time. You will have to implement some special logic to handle the case when your app goes offline and back online and all n posts of the snapshot have changed. This is a must if your app is meant to scale (you dont want a snapshot with no limit on a collection with 100k docs...)
Example:
firestore.collection("Posts")
.where( "lastaction", "==" , "reply")
.where( "replies", "array-contains", uid)
.orderBy("timestamp", "desc").limit(10)

Having trouble loading custom data from firebase using swift

I followed the directions on the firebase website and came up with the code below labeled NEW CODE. The error I am getting is:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
I have tried researching about the Result object and this error but have not found anything that would help me directly. I was able to read the data using the old way (which I will post below), but am trying to follow Googles documentation
(https://firebase.google.com/docs/firestore/query-data/get-data)
Any help would be appreciated, thank you!
OLD CODE
func readData(word: String) -> CombinedModel? {
print("reading data")
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
var wordResults: CombinedModel? = nil
docRef.getDocument { (document, error) in
if let e = error {
print("Error loading data: \(e)")
return
} else {
do {
if let resultData = try document?.data(as: CombinedModel.self){
print("Definitions: \(resultData.definitionsArray)")
print("\n Synonyms: \(resultData.synonyms)")
wordResults = resultData
}
} catch {
print("Error decoding: \(error)")
}
}
}
return wordResults
}
NEW CODE
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
print("CombinedModel: \(combinedModel)")
} else {
print("Document does not exist")
}
case .failure(let error):
print("Error decoding city: \(error)")
}
}
}
Here is the codable custom class I created for reading the data.
struct CombinedModel: Codable {
var definitionsArray: [WordModel]
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case definitionsArray
case synonyms
}
}
struct WordModel: Codable {
let id: String
let partOfSpeech: String
let definitions: [String]
let example: [String]
let ipa: String
let audio: String
private enum CodingKeys: String, CodingKey {
case id
case partOfSpeech
case definitions
case example
case ipa
case audio
}
}
struct ThesaurusModel: Codable {
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case synonyms
}
}
I realized the issue with the Result object was that Xcode was reading it as a structure that I created in a different file, but once I changed the name of that structure(it was named Result previously), it recognized the Result object as the correct object which is of type: enum Result where Failure : Error
Looks like you're getting close - minus the type error. From what I can tell, your error:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
Refers to this portion of code:
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
At least, this is the portion of code that looks off to me. Your (document, error) variable as a whole represents your result. You shouldn't need a Result middleman.
The real focus should be around determining whether or not there is an error. If there isn't you should read the document.
A working approach should look something like this:
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
guard let fbDoc = document else {
// document is nil, parse the error object and handle it.
}
if let resultData = try fbDoc?.data(as: CombinedModel.self) {
return resultData
} else {
return nil
}
}
}
Let me know how that goes for you! Best of luck.