How to get user specific data from firebase in iOS app? - ios

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.

Related

How to retrieve an authenticated user data with Firestore in iOS?

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

Swift: Firestore sub collections, custom objects and listeners

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.

Cloud Firestore Swift: How to delete a query of documents

I would like to delete all documents in my collection Usernames that has the field UID as the current user's ID. This is my code so far:
let uid = Auth.auth().currentUser!.uid
db.collection("Usernames").whereField("UID", isEqualTo: uid).delete
but here comes the error:
Value of type 'Query' has no member 'delete'.
Any special technique to this? Thanks!
The guide shows you how to delete data. It also notes that deleting entire collections shouldn't be done from the client. There's no way to delete from a query -- you will have to get all the documents and delete them individually. If there is the potential for lots of documents, then you should do this server-side. If you know for sure there will only be a few, you can query them, get the doc references, and then delete them, like this:
let uid = Auth.auth().currentUser!.uid
db.collection("Usernames").whereField("UID", isEqualTo: uid).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
document.reference.delete()
}
}
}
If this is something that will need to be done frequently, then this is a good opportunity to examine your data structure. Maybe make a collection where user's Usernames are listed under their uid. Then you could just query that single doc and use it to reference the other ones to delete.
Usernames: {
byUID: {
uid1: {
funky_monkey: true,
goodTimes: true
}
}
byUsername: {
funky_monkey: {
UID: uid1
}
}
}
This is just one suggestion. There are definitely other options that may be better for your app.

why I still get deleted documents when getting data using listener from Firestore in Swift?

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.

CloudKit Sharing

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!

Resources