Getting Information from Two Firebase Root Collections - ios

I am using Firebase Firestore with Swift and SwiftUI. I have two collections Users and Tweets. Each Tweet store the userId of the user who tweeted. I want to fetch all the tweets along with the user information associated with tweet. Since both are stored in different collections at the root level, this poses a problem. Here is one of the solutions.
private func populateUserInfoIntoTweets(tweets: [Tweet]) {
var results = [Tweet]()
tweets.forEach { tweet in
var tweetCopy = tweet
self.db.collection("users").document(tweetCopy.userId).getDocument(as: UserInfo.self) { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
results.append(tweetCopy)
if results.count == tweets.count {
DispatchQueue.main.async {
self.tweets = results.sorted(by: { t1, t2 in
t1.dateUpdated > t2.dateUpdated
})
}
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
Kinda messy!
Another way is to store the documentRef of the User doc instead of the userId.
If I store the User document reference then I am using the following code:
init() {
db.collection("tweets").order(by: "dateUpdated", descending: true)
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
snapshot?.documents.compactMap { document in
try? document.data(as: Tweet.self)
}.forEach { tweet in
var tweetCopy = tweet
tweetCopy.userDocRef?.getDocument(as: UserInfo.self, completion: { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
self.tweets.append(tweetCopy)
case .failure(let error):
print(error.localizedDescription)
}
})
}
}
}
The above code does not work as expected. I am wondering there must be a better way. I don't want to store all the user information with the Tweet because it will be storing a lot of extra data.
Recommendations?

The common alternative is to duplicate the necessary information of the user in their tweets, a process known as denormalization. If you come from a background in relational databases this may sounds like blasphemy, but it's actually quite common in NoSQL data modeling (and one of the reasons these databases scale to such massive numbers of readers).
If you want to learn more about such data modeling considerations, I recommend checking out NoSQL data modeling and watching Todd's excellent Get to know Cloud Firestore series.
If you're wondering how to keep the duplicated data up to date, this is probably also a good read: How to write denormalized data in Firebase

Related

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.

How to access a specific field from Cloud FireStore Firebase in Swift

Here is my data structure:
I have an ios app that is attempting to access data from Cloud Firestore. I have been successful in retrieving full documents and querying for documents. However I need to access specific fields from specific documents. How would I make a call that retrieves me just the value of one field from Firestore in swift? Any Help would be appreciated.
There is no API that fetches just a single field from a document with any of the web or mobile client SDKs. Entire documents are always fetched when you use getDocument(). This implies that there is also no way to use security rules to protect a single field in a document differently than the others.
If you are trying to minimize the amount of data that comes across the wire, you can put that lone field in its own document in a subcollection of the main doc, and you can request that one document individually.
See also this thread of discussion.
It is possible with server SDKs using methods like select(), but you would obviously need to be writing code on a backend and calling that from your client app.
This is actually quite simple and very much achievable using the built in firebase api.
let docRef = db.collection("users").document(name)
docRef.getDocument(source: .cache) { (document, error) in
if let document = document {
let property = document.get(field)
} else {
print("Document does not exist in cache")
}
}
There is actually a way, use this sample code provided by Firebase itself
let docRef = db.collection("cities").document("SF")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let property = document.get('fieldname')
print("Document data: \(dataDescription)")
} else {
print("Document does not exist")
}
}
I guess I'm late but after some extensive research, there is a way in which we can fetch specific fields from firestore. We can use the select keyword, your query would be somthing like (I'm using a collection for a generalized approach):
const result = await firebase.database().collection('Users').select('name').get();
Where we can access the result.docs to further retrieved the returned filtered result. Thanks!
//this is code for javascript
var docRef = db.collection("users").doc("ID");
docRef.get().then(function(doc) {
if (doc.exists) {
//gives full object of user
console.log("Document data:", doc.data());
//gives specific field
var name=doc.get('name');
console.log(name);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});

Delete duplicated object in core data (swift)

I'm saving objects to core data from a JSON, which I get using a for loop (let's say I called this setup function.
Because the user might stop this loop, the objects saved in core data will be partial. The user can restart this setup function, restarting the parsing and the procedure to save object to core data.
Now, I'm getting duplicated objects in core data if I restart the setup().
The object has an attribute which is id.
I've thought I could fetch first objects that could eventually already exist in core data, save them to an array (a custom type one), and test for each new object to add to core data if already exist one with the same id.
The code used is the following:
if !existingCards.isEmpty {
for existingCard in existingCards {
if id == existingCard.id {
moc.deleteObject(existingCard)
println("DELETED \(existingCard.name)")
}
}
}
...
// "existingCards is the array of object fetched previously.
// Code to save the object to core data.
Actually, the app return
EXC_BAD_ACCESS(code=1, address Ox0)
Is there an easier way to achieve my purpose or what should I fix to make my code work? I'm quite new to swift and I can't figure other solution.
The main purpose is to delete duplicated core data, BTW.
Swift 4 code to delete duplicate object:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Card")
var resultsArr:[Card] = []
do {
resultsArr = try (mainManagedObjectContext!.fetch(fetchRequest) as! [Card])
} catch {
let fetchError = error as NSError
print(fetchError)
}
if resultsArr.count > 0 {
for x in resultsArr {
if x.id == id {
print("already exist")
mainManagedObjectContext.deleteObject(x)
}
}
}
At the end, I managed to make it work.
I had to rewrite my code, because I realized moc.deleteObject() works with a fetch before, which in my previous code wasn't in the same function, but it was in viewDidLoad().
// DO: - Fetch existing cards
var error: NSError?
var fetchRequest = NSFetchRequest(entityName: "Card")
if let results = moc.executeFetchRequest(fetchRequest, error: &error) as? [Card] {
if !results.isEmpty {
for x in results {
if x.id == id {
println("already exist")
moc.deleteObject(x)
}
}
}
} else {
println(error)
}
No more existingCards, the result of the the fetch is now processed as soon as possible. Something isn't clear to me yet, but now my code works. If you have any improvements/better ways, they're welcome.
P.S.: I actually found Apple reference useful but hard to understand because I don't know Obj-C. Often I can figure what the code do, but in swift functions and properties are a bit different.

Resources