I've read several articles here on StackOverflow asking this same question and the answers are all the same but it doesn't seem to work for me and I'm not sure why. I've read Elegant Way To Dynamically Query FireStore, Conditional Where Query on Firestore, the two articles that question references, plus How to conditionally add another filter to a query in Firebase Firestore using swift?, and the article Firestore Query Options Swift.
I followed the simple guidance the all offered:
var ref = db.collection("Violations")
if locationCompoundQuery != "" {
ref = ref.whereField("location", isEqualTo: locationCompoundQuery)
}
The issue is that Xcode gives me an error: Cannot assign value of type 'Query' to type "CollectionReference" insert 'as! CollectionReference'. If I do that it will compile and run, but when the query runs it crashes. The message in the console is: Could not cast value of type 'FIRQuery' (0x108972150) to 'FIRCollectionReference'
I'm using Xcode 13.1. All of my other queries that are compound without conditional statements run just fine. Any help is greatly appreciated.
I had the same issue. This works for me on Xcode 14.0.1 and Firebase 9.6.0.
let ref = db.collection(EventsObject)
var query: Query = ref
if filter == .all {
query = ref.whereField(CloudEvent.CodingKeys.ownerID.stringValue, isNotEqualTo: "") // must have any OwnerID
} else if filter == .allPublished {
query = ref.whereField(CloudEvent.CodingKeys.ownerID.stringValue, isNotEqualTo: "") // must have any OwnerID
query = query.whereField(CloudEvent.CodingKeys.published.stringValue, isEqualTo: true) // and has been published
} else if filter == .onlyThisUser {
query = ref.whereField(CloudEvent.CodingKeys.ownerID.stringValue, isEqualTo: currentUser) // belongs to this user
} else if filter == .onlyThisUserPublished {
query = ref.whereField(CloudEvent.CodingKeys.ownerID.stringValue, isEqualTo: currentUser) // belongs to this user
query = query.whereField(CloudEvent.CodingKeys.published.stringValue, isEqualTo: true) // and has been published
} else if filter == .onlyThisUserNotPublished {
query = ref.whereField(CloudEvent.CodingKeys.ownerID.stringValue, isEqualTo: currentUser) // belongs to this user
query = query.whereField(CloudEvent.CodingKeys.published.stringValue, isEqualTo: false) // and has not been published
}
query.addSnapshotListener({ querySnapShot, error in
if let querySnapShot = querySnapShot {
self.cloudEvents = querySnapShot.documents.compactMap { document in
do {
return try document.data(as: CloudEvent.self)
}
catch {
EventLogging.logger.error("error loading cloud events: \(error.localizedDescription)")
}
return nil
}
}
})
Related
Firstly I've reviewed Firebase documentation, they only have two separate explanations on login user and CRUD operations but not really how both they can be connected with security rules. Despite that I've worked on it here is the security rules. In iOS code I fetch the list and also add the item with user id
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contacts/{creatorsID} {
allow read, write: if request.auth != null && request.auth.uid == creatorsID;
}
}
}
ERROR: Write at contacts/wv6fMMhBpw3ROEOyUMBe failed: Missing or insufficient permissions
{ creatorsID: "", name: "" }
func fetchData() {
db.collection("contacts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.contacts = documents.map { (queryDocumentSnapshot) -> Contact in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let userID = data["creatorsID"] as? String ?? ""
return Contact(name: name)
}
}
}
func addData(name: String) {
do {
_ = try db.collection("contacts").addDocument(data: ["name": name, "creatorsID": Auth.auth().currentUser?.uid])
}
catch {
print(error.localizedDescription)
}
}
The problem here is that I still receive all the items from all users instead of specific user's list(FYI, CreatorsId is as same as UID). I can't figure out if the issue is in rules or in iOS code.
You're getting all users because you read the data with:
db.collection("contacts").addSnapshotListener...
So this gives you all documents from the contacts collection. If you want to only read a single document, you'll have to identify that document either by its ID or by using a query.
Since you use addDocument to add the document, Firestore generates a unique ID for you. That means that with your current data structure, the way to get the user document is by querying on the createdID field:
db.collection("contacts")
.whereField("createdID", isEqualTo: Auth.auth().currentUser?.uid)
.addSnapshotListener { (querySnapshot, error) in
...
A more idiomatic way to store user profile documents is to use the UID of each user as the ID of the document too. That both guarantees that each user can only have one document (as document IDs are by definition unique within a collection), and it means you can look up the profile document for a user without a query:
db.collection("contacts")
.document(Auth.auth().currentUser?.uid)
.addSnapshotListener { (documentSnapshot, error) in
....
Doing this will also solve your security errors, as this code matches exactly with what you're checking here:
match /contacts/{createdID} {
if request.auth != null && request.auth.uid == createdID
In your current approach the value of createdID is the random ID that Firestore generates when you call addDocument, and not the user's UID.
I'm playing around with Firebase and I'm using their authentication framework plus Firestore database.
My database model is quite simple: I want to keep a collection "users" which will hold a document -identified by the user's uid- for each registered user. This user document will hold a collection of documents representing user expenses.
Following their documentation I've set up the rules in the firebase console as shown beneath:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId=**} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
And in my iOS app, when user logs in, I want to retrieve the user's expenses with the following code:
func retrieveUserExpenses() {
guard let dbRef = userExpensesDbRef() else {
print("Attempting to retrieve expense for unlogged user")
return
}
dbRef.getDocuments { [unowned self] (snapshot, err) in
if let err = err {
print("Error retrieving expenses \(err)")
} else {
print("Retrieved expenses!")
snapshot?.documents.forEach {
if let expense = self.createExpenseFromDocument($0) {
self.expenses.append(expense)
}
}
self.loadedExpenses = true
}
}
}
private func userExpensesDbRef() -> CollectionReference? {
guard let loggedInUser = AuthHandler.shared.loggedInUser() else {
return nil
}
return db.collection("users").document(loggedInUser.uid).collection("expenses")
}
The thing is that the call to getDocuments prints the error line:
Error retrieving expenses Error Domain=FIRFirestoreErrorDomain Code=7
"Missing or insufficient permissions."
I've checked by printing that this: loggedInUser.uid is the same one I see on the Firebase Console in the Authentication section in the Users tab.
Any guidance or tip on where I'm failing to securely retrieve the data is greatly appreciated.
Go in Database -> Rules ->
Change allow read, write: if false; to true;
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
allow write: if false;
}
}
}
I have a model pattern as depicted in the below picture.
In the UI I am trying to get all the Countries and related data. I have created respective structures and my idea was to use the custom object approach shown in the link Custom_Objects. Now the problem I have is that subcollections won't come in the querysnapshot ( I am getting only the fields) and hence I cannot do the direct object mapping (as I have to query and retrieve the subcollections to make the object complete). for example: Country structure has States as one of the properties and without states Country object cannot be created and States further has provinces. Right now I am doing recursive looping to construct the whole structure which I am not feeling quite good.
So my question would be, what is the best way to handle this kind of data (provided there is no room for normalization and we can't avoid subcollections)?
Also if I want to get notified on any changes to State, Province or Town, do I need to separately add listeners to each collection or adding to the root is enough?
Here is the current code snap
db.collection("countries").getDocuments { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
self.fetchStatesForDoc(document: document, completion: { (nodes) in
var data = document.data()
data["states"] = nodes
let country = Country(dictionary: data)
self.countryList.append(country!)
print(self.sectionList)
docCount = docCount - 1
if docCount == 0{
DispatchQueue.main.async {
self.countryCollection.reloadData()
}
}
})
}
}
}
}
func fetchStatesForDoc(document: DocumentSnapshot, completion:#escaping ([State])-> Void){
var states = [State]()
document.reference.collection("states").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
var data = document.data()
self.fetchProvincesForDoc(document: document, completion: { (provinces) in
data["Provinces"] = provinces
let state = State(dictionary: data)
states.append(state!)
docCount = docCount - 1
if docCount == 0{
completion(state)
}
})
}
}
})
}
func fetchProvincesForDoc(document: DocumentSnapshot, completion:#escaping ([Province])-> Void){
var provinces = [Province]()
document.reference.collection("provinces").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
var docCount = QuerySnapshot!.documents.count
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
var data = document.data()
self.fetchTownsForDoc(document: document, completion: { (towns) in
data["towns"] = provinces
let province = Province(dictionary: data)
provinces.append(province!)
docCount = docCount - 1
if docCount == 0{
completion(province)
}
})
}
}
})
}
func fetchTownssForDoc(document: DocumentSnapshot, completion:#escaping ([Towns])-> Void) {
var towns = [Towns]()
document.reference.collection("towns").getDocuments(completion: { (QuerySnapshot, error) in
if let error = error {
print("\(error.localizedDescription)")
}else{
for document in QuerySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
}
towns = QuerySnapshot!.documents.compactMap({Towns(dictionary: $0.data())})
completion(towns)
}
})
}
Now the problem I have is that subcollections wont come in the querysnapshot ( I am getting only the fields)
That's right, this is how Cloud Firestore queries works. The queries are named shallow, which means they only get items from the collection that the query is run against. There is no way to get documents from a top-level collection and a subcollections in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection. That's why ypu cannot see the subcollection in the querysnapshot object so you can do the direct object mapping.
what is the best way to handle these kind of data (provided there is no room for normalization and we cant avoid subcollections)?
In this case you should query the database twice, once to get the objects within the collection and second to get all the objects within the subcollection.
There is also another practice which is called denormalization and is a common practice when it comes to Firebase. This technique implies also querying the database twice. If you are new to NoQSL databases, I recommend you see this video, Denormalization is normal with the Firebase Database for a better understanding. It is for Firebase realtime database but same rules apply to Cloud Firestore.
Also, when you are duplicating data, there is one thing that need to keep in mind. In the same way you are adding data, you need to maintain it. With other words, if you want to update/detele an item, you need to do it in every place that it exists.
So in this case, you can denormalize your data by creating a top-level collection in which you should add all the objects that exist in your subcollections. It's up to you to decide which practice is better for you.
I trying to make an event app. and I add a new field in my events documents. I try to add "venue" field for my event documents
so before I run the app, I delete all the available data on my Firestore database. But when I retrieve my data back to the app, it is said that the "venue" is nil, it seems that the "venue" field is not exist, even though in fact, the "venue" field exist on my firestore database.
I suspect my app still retrieve my deleted documents. here is why
here is the code I use :
enum FirestoreCollectionReference {
case users
case events
case cities
case APIKey
private var path : String {
switch self {
case .users : return "users"
case .events : return "events"
case .cities : return "cities"
case .APIKey : return "secretAPIKeyKM"
}
}
func reference () -> CollectionReference {
return Firestore.firestore().collection(path)
}
}
FirestoreCollectionReference.events.reference()
.whereField("city", isEqualTo: selectedCity)
.whereField("eventType", isEqualTo: selectedEventType)
.whereField("coordinate", isGreaterThan: lesserGeopoint)
.whereField("coordinate", isLessThan: greaterGeopoint)
.order(by: "coordinate")
.order(by: "dateTimeStart", descending: true)
.limit(to: 20)
.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil,eventListener)
print("Error when observing events document: \(error.localizedDescription)")
} else {
print("Successfully get events data from Firestore by Listener")
guard let documentsSnapshot = snapshot else {
completion(nil, eventListener)
return
}
let eventDocuments = documentsSnapshot.documents
print("the number of documents: \(eventDocuments.count)")
var eventsArray = [EventKM]()
for document in eventDocuments {
let eventDictionary = document.data()
let theEvent = EventKM(dictionary: eventDictionary)
eventsArray.append(theEvent)
}
completion(eventsArray,eventListener)
}
}
}
I try to print the number of documents, and it shows that I have 8 documents from this query, in fact, it should be only one document available in my database.
I try to delete the composite indexes from firebase console, but usually, after I delete the composite indexes, It will give an error + a link to generate the composite indexes in my debugging area on my Xcode, but after I delete the composite indexes, I don't get the error + link to generate the indexes, and give 8 documents (it should be one document only)
it seems the data is cached on my iOS app. isn't it? or is this a bug since Firestore is still in Beta version? I need to understand why and how to solve this issue so I can understand firebase better. Thanks in advance.
FireBase has cached in device so if user stay in outside of internet,
But still can use Firebase.
So You just remove your app in your simulator.
In my case, It fixed.
I am having trouble understanding some of the CloudKit sharing concepts and the WWDC 2016 "What's new in CloudKit" video doesn't appear to explain everything that is required to allow users to share and access shared records.
I have successfully created an app that allows the user to create and edit a record in their private database.
I have also been able to create a Share record and share this using the provided sharing UIController. This can be successfully received and accepted by the participant user but I can't figure out how to query and display this shared record.
The app creates a "MainZone" in the users private database and then creates a CKRecord in this "MainZone". I then create and save a CKShare record and use this to display the UICloudSharingController.
How do I query the sharedDatabase in order to access this record ? I have tried using the same query as is used in the privateDatabase but get the following error:
"ShareDB can't be used to access local zone"
EDIT
I found the problem - I needed to process the accepted records in the AppDelegate. Now they appear in the CloudKit dashboard but I am still unable to query them. It seems I may need to fetch the sharedDatabase "MainZone" in order to query them.
Dude, I got it: First you need to get the CKRecordZone of that Shared Record. You do it by doing the following:
let sharedData = CKContainer.default().sharedCloudDatabase
sharedData.fetchAllRecordZones { (recordZone, error) in
if error != nil {
print(error?.localizedDescription)
}
if let recordZones = recordZone {
// Here you'll have an array of CKRecordZone that is in your SharedDB!
}
}
Now, with that array in hand, all you have to do is fetch normally:
func showData(id: CKRecordZoneID) {
ctUsers = [CKRecord]()
let sharedData = CKContainer.default().sharedCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Elder", predicate: predicate)
sharedData.perform(query, inZoneWith: id) { results, error in
if let error = error {
DispatchQueue.main.async {
print("Cloud Query Error - Fetch Establishments: \(error)")
}
return
}
if let users = results {
print(results)
self.ctUsers = users
print("\nHow many shares in cloud: \(self.ctUsers.count)\n")
if self.ctUsers.count != 0 {
// Here you'll your Shared CKRecords!
}
else {
print("No shares in SharedDB\n")
}
}
}
}
I didn't understand quite well when you want to get those informations. I'm with the same problem as you, but I only can get the shared data by clicking the URL... To do that you'll need two functions. First one in AppDelegate:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = {
metadata, share, error in
if error != nil {
print(error?.localizedDescription)
} else {
let viewController: ViewController = self.window?.rootViewController as! ViewController
viewController.fetchShare(cloudKitShareMetadata)
}
}
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
in ViewConroller you have the function that will fetch this MetaData:
func fetchShare(_ metadata: CKShareMetadata) {
let operation = CKFetchRecordsOperation(recordIDs: [metadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if error != nil {
print(error?.localizedDescription)
}
if record != nil {
DispatchQueue.main.async() {
self.currentRecord = record
//now you have your Shared Record
}
}
}
operation.fetchRecordsCompletionBlock = { _, error in
if error != nil {
print(error?.localizedDescription)
}
}
CKContainer.default().sharedCloudDatabase.add(operation)
}
As I said before, I'm now trying to fetch the ShareDB without accessing the URL. I don't want to depend on the link once I already accepted the share. Hope this helps you!