Get list of users who liked a post Swift and Firestore - ios

I am attempting to build a tableview that displays the users who have liked the current user's posts. For example, "John Smith, Tom Jones, and Sally Hughes liked your post (display image of post in cell". I think I'm on the right track but I'm wondering if my data structure is going to make this unnecessarily difficult or if there is an easier way.
My Firestore data structure is just below. The "BmV..." is the userId, "-M0e..." is the postId, and the "Ff0..." and "nIh..." are the users who have liked the post.
"likeActivity" : {
"BmvRlWWuGRWApqFvtT8mXQlDWzz2" : {
"-M0efUXcZy43fDVXjTvT" : {
"Ff0CxZhQzYVHuqbnsiOKwRAB01D2" : true,
"nIhx1SnChjapy4cbrD5sC1WIZXM2" : true
}
},
}
My first question is if this is the best way to structure this data? Then, In the ActivityViewController using the following code to retrieve of the current user's posts with activity.
var activityDict = [String: [Any]]()
let newActivity = DataService.ds.REF_LIKE_ACTIVITY.child("\(uid)")
//print("NEW POST - \(newPost)")
newActivity.observe(.value, with: { (snapshot) in
self.posts = []
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
print("ACTIVITY -- \(snap.key)")
let userLikeData = DataService.ds.REF_LIKE_ACTIVITY.child("\(uid)").child(snap.key)
userLikeData.observe(.value, with: { (snapshot) in
self.posts.append(snap.key)
print("SNAPSHOT VALUE -- \(snapshot)")
if self.activityDict["\(snap.key)"] != nil {
self.activityDict["\(snap.key)"]!.append(snapshot.value!)
} else {
self.activityDict["\(snap.key)"] = [snapshot.value!]
}
self.activityTableView.reloadData()
})
}
}
})
My next question is if the activity should be accumulated in an array of dictionaries like I have it? Is there a better way to organize this data?

My first question is if this is the best way to structure this data?
When using NoSQL databases there is no singular "best" way to store data. You'll instead store the data in a way that best supports your use-cases. And since you'll typically uncover more (details about your) use-cases as you implement and evolve your app, you data model evolves (adapting and expanding) with it.
I recommend reading NoSQL data modeling, and watching Firebase for SQL developers and Getting to know Cloud Firestore. The last one is for Cloud Firestore, but many of the techniques Todd discusses apply equally to other NoSQL databases.
should [the activies] be accumulated in an array of dictionaries like I have it?"
If that works for your use-cases, then it sounds fine.
The only thing of note is that you're using observe, which attaches a permanent listener, and then append the updated snapshot data to the array. This means that if an activity gets changed in the database, your closure will get called again with a snapshot for that activity.
This is great, because it allows you to show the updated activity in the UI. But since you're appending it to the array, you'll end up displaying the activity twice: once as it was stored in the database when you first loaded it, and then again as it exists after the update. If that is what you're aiming for then šŸ‘, but it is more common to update the existing data in the array, instead of adding the updated data as a new item.

Related

ios - Repeating Keys when paginating with Firebase

I am learning pagination with Firebase. I am using a method in which I store the key of the last added item in the previous page, so the next page can continue from there.
The problem is that when using ref.queryStarting(at value: lastItemKey) to keep retrieving items from the last added key, the last item gets repeated twice (since queryStarting is inclusive).
And so if I limit to 5 the query I would end up with only 4 new items as 1 would be a duplicate.
The only solution I came up is requesting one more item and remove the repeated one, but I wonder if itĀ“s efficient at all doing it this way. (since we are wasting one item in each query)
If itĀ“s any help, my code looks like this:
// rest of the pages
if let lastItemID = lastItemKey {
itemPageRef = self.itemsRef.queryOrderedByKey().queryStarting(atValue: lastItemID)
.queryLimited(toFirst: UInt(amount))
} else {
// First page of data: we retrieve the first (amount) items
print("We are in the first page of DATA")
itemPageRef = self.itemsRef.queryOrderedByKey().queryLimited(toFirst: UInt(amount))
}
itemPageRef.observeSingleEvent(of: .value, with: { [weak self] (snapshot) in
Requesting an overlapping child node between the pages is the only way the Firebase API supports. Since there is no other way to do this, there isn't a more efficient way.
That said, it's typically quite efficient, especially if you use a page size of 25+ child nodes, which is also more reasonable on most use-cases I've seen.

Firebase: how to retrieve only changed items next time app launches?

I've got an app that uses a Firebase db containing 100,000 items. My app has to process through each of these items which takes several seconds.
What is happening is that every time the app is launched (from a terminated state) those 100,000 items are being processed each time (even if the contents of the db on the Firebase server have not changed). Obviously, I don't want the app to do this if not necessary. Here's some code:
if dbRef == nil {
FirebaseApp.configure();
Database.database().isPersistenceEnabled = true
...
let dbRef = Database.database().reference(withPath: kFirebaseDBName)
_ = spamRef.observe(DataEventType.value, with: { (theSnapshot) in
if let content = theSnapshot.value as? [String : AnyObject]
{
self.processContent(content: content)
}
Each time the app is started then the content snapshot contains the entire database reference contents.
Is there a way of, for example, getting the last date the database was updated (on the server), or only obtaining the delta of changed items between each app launch - can a query return just changed since last queried for example, or something similar?
I don't know how many items have changed so cannot call something like:
queryLimited(toLast: N))
As I don't know what value N is.
I've tried adding keepSynced as follows in the hope it might change things, but no.
if dbRef == nil {
FirebaseApp.configure();
Database.database().isPersistenceEnabled = true
...
let dbRef = Database.database().reference(withPath: kFirebaseDBName)
dbRef.keepSynced(true)
_ = dbRef.observe(DataEventType.value, with: { (theSnapshot) in
if let content = theSnapshot.value as? [String : AnyObject]
{
self.processContent(content: content)
}
I have no idea how much data might have changed so don't know what value to supply to something like toLast or similar to modify the observation parameters.
The database (which was not created nor updated with new content by me) has 100,000 items in a flat structure (i.e. one parent with 100,000 children) and any number of these children in any order might have been deleted and replaced since last time my app ran, but the total will still be 100,000. None of the children have an explicit timestamp or anything like that.
I was under the impression if Firebase kept a local cache of the data (due to isPersistenceEnabled) then next time it connects with the server it would only sync what had changed on the server. Therefore in order to do this Firebase itself must internally have some delta information somewhere, so I was hoping that delta information may available in some form to my app.
Note: My app does not need persistence to be enabled, the above code is doing so just as variations to see if anything will result in the behavior I desire with the observer.
UPDATE
So looking at the documentation more you can set a timestamp for the last time a user was connected to the server using:
lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
Take a look at this question Frank explains some issues with persistence and listeners. The question is for Android but the principles are the same.
I still think the problem is your query. Since you already have the data persisted .value is not what you want since this returns all of the data.
I think you want to attach a .childChanged listener to your query. In this case the query will only return the data that has been changed. If you haven't heard of .childChanged before you can read about it here.
I didn't realize this problem is specifically related to persistence. I think you are looking for keepSynced(). Take a look at this.
ORIGINAL ANSWER
The problem is your query. You are asking for all of the data that's why you're getting all of the data. You want to look into limiting your queries using toFirst or toLast. Additionally, I don't think you can query for the last time the database was updated. You could check the last node in your data structure if you have the timestamp saved, but you might as well just get the newest data.
You want something like this:
ref.child("yourChild").queryLimited(toLast: 7).observeSingleEvent(of: .value, with: { snap in
// do something
})
Depending on how you're writing your data you'll want toLast or toFirst. Assuming the newest data is written last toLast is what you want. Also note that the numbers I am limiting to are arbitrary you can use any number that fits your project.
If you already have a key and you want to start querying above that key you can do something like this:
ref.child("YourChild").queryOrderedByKey().queryEnding(atValue: lastVisiblePostKey).queryLimited(toLast: 8).observeSingleEvent(of: .value, with: { snap in
// do something with more posts
})
You may also want to look into this question, this question and pagination.

Firebase observeEventType .childAdded doesn't work as promised

When I call this observe function from in my viewcontroller, the .childadded immediately returns a object that was already stored instead of has just bin added like .childadded would suspect.
func observe(callback: RiderVC){
let ref = DBProvider.Instance.dbRef.child("rideRequests")
ref.observe(DataEventType.childAdded) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
let drive = cabRide(ritID: ritID, bestemming: bestemming,
vanafLocatie: vanaf, taxiID: taxiID, status: status)
print(drive)
callback.alertForARide(title: "Wilt u deze rit krijgen?", message: "Van: \(vanaf), Naar: \(bestemming)", ritID: ritID)
}
}
}
When I try this function with .childchanged, I only get a alert when it is changed like it suppose to do, but when doing .chiladded, it just gets all the requests out of the database and those requests were already there.
When I add a new request, it also gives an alert. So it works, but how can I get rid of the not added and already there requests?
Does anybody know this flaw?
This is working exactly as promised. From the documentation:
Retrieve lists of items or listen for additions to a list of items.
This event is triggered once for each existing child and then again
every time a new child is added to the specified path. The listener is
passed a snapshot containing the new child's data.
That might seem weird at first, but this is generally what most developers want, as it's basically a way of asking for all data from a particular branch in the database, even if new items get added to it in the future.
If you want it to work the way you're describing, where you're only getting new items in the database after your app has started up, you'll need to do a little bit of work yourself. First, you'll want to add timestamps to the objects you're adding to the database. Then you'll want to do some kind of call where you're asking to query your database by those timestamps. It'll probably look something like this:
myDatabaseRef.queryOrdered(byChild: "myTimestamp").queryStarting(atValue: <currentTimestamp>)
Good luck!

How to create a database using Parse that will work offline?

My app is a directory with basic things like phone numbers and locations. There's going to be many entries and I will have to update the database on a weekly basis and I don't want to rely on my users to update their app using the App store, so I want the data to be updated whenever they connect to the internet, but I want all of the data stored locally so they can use it even if they don't have internet access. For my first app, it's proving to be quite tricky :)
I think I'll have to use Parse so I can update the database whenever I need to, along with something like Realm (https://realm.io/) or Core Data (hopefully not Core Data :( ). I read about Parse's Local DataStore, and if there's a way to make it work for my needs I'd definitely use it. I like the simplicity of Realm though and if I there's a way to make it work with Parse, that would be the route I'd want to take.
Can somebody show me an example on how they might go about doing this? If the PFObject or RLMObject is called person and has two strings (phone number and name, how would you get that from Parse.com to the device (local storage)?
This is an extra and I don't even know if it's possible*
While the app is downloading from the App Store, could it download the data from Parse? As in, the second the user opens the app, even if they no longer have internet access, the data is locally stored and usable to them.
(I only know Swift but may be able to understand obj-c if anybody cares to show me some code snippets)
Any help at all would be greatly appreciated :)
Joe from Realm here.
First to retreive an object from Parse you will need to do this:
var query = PFQuery(className:"GameScore")
query.whereKey("playerName", equalTo:"Sean Plott")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(objects!.count) scores.")
// Do something with the found objects
if let objects = objects as? [PFObject] {
for object in objects {
println(object.objectId)
}
}
} else {
// Log details of the failure
println("Error: \(error!) \(error!.userInfo!)")
}
}
You can read more about this here
Once you retrieve your objects, I would loop through them as you see here
for object in objects {
println(object.objectId)
}
And then I would use Realm's Realm().create(_:value:update:)(shown in code below). You need to make sure you have a primary key to use this though (I would use parse's objectId as the primary key in Realm). Here is how you set a primary key in Realm.
An example of how I would import them into Realm would be something like this (Where Venue is the class of the way the object is stored in Realm):
let realm = Realm()
realm.write {
for object in objects {
realm.create(Venue.self, value: object, update: true)
}
}
You can read more about importing here
Lastly the logic of when you do this is up to you. You could do it each time the person opens the app. One thing with syncing local databases with the ones on the server is that it's good to just check another Table maybe called LastUpdated. This table will let you know if you need to update your local database. Overall it's a very manual process but it depends on the situation and how you want to structure your app.

Query users by name or email address using Firebase (Swift)

I'm quite new to Firebase and Swift and I'm having some trouble when it comes to querying.
So there are basically two things I'd like to do:
Query my users and find only those that contain a certain String in their name (or email address) and add them to an array.
Get all of my users and add them to an array.
The relevant part of my data for this question looks like this:
As you can see, I'm using the simplelogin of Firebase (later I'd like too add Facebook login) and I'm storing my users by their uid.
A part of my rules file looks like this:
"registered_users": {
".read": true,
".write": true,
".indexOn": ["name"]
}
So everybody should have read and write access to this part of my data.
I also read the "Retrieving Data" part of the Firebase iOS Guide on their website and according to that guide, my code on getting all the users names and email addresses should work, at least I think so. But it doesn't. Here is my code:
func getUsersFromFirebase() {
let registeredUserRef = firebaseRef.childByAppendingPath("registered_users")
registeredUserRef.queryOrderedByChild("name").observeSingleEventOfType(.Value, withBlock: { snapshot in
if let email = snapshot.value["email"] as? String {
println("\(snapshot.key) has Email: \(email)")
}
if let name = snapshot.value["name"] as? String {
println("\(snapshot.key) has Name: \(name)")
}
})
}
I noticed, that in the firebase guide, they always used the type ChildAdded and not Value, but for me Value makes more sense. The output with Value is nothing and the output with ChildAdded is only one user, namely the one that is logged in right now.
So my questions are:
Can I do this query with my current data structure or do I have to get rid of storying the users by their uid?
If yes, how would I have to change my code, to make it work?
If no, what would be the best way to store my users and make querying them by name possible?
How can I query for e.g. "muster" and get only the user simplelogin:1 (Max Mustermann)?
I hope my description is detailed enough. Thx in advance for your help.
Supplement:
The weird thing is, that the "Retrieving Data" guide says, that querying and sorting the following data by height is possible.
Data:
Querying code:
And isn't that exactly the same that I intent to do?
I have run into similar situations where I wanted to pull out data from child nodes.
https://groups.google.com/d/msg/firebase-talk/Wgaf-OIc79o/avhmN97UgP4J
The first thing I can recommend is to not think of Firebase query's as SQL queries as they are not. They are like a light duty query.
Secondly, you need to flatten your data if you want to query, as a query only goes one level deep (can't really query data in child notes)
Lastly - if you don't want to flatten your data, one conceptual option to answer your question;
If possible, ObserveSingleEventOfType:FEventTypeValue on the
registered users node. This will read all of the registered users into a snapshot.
Iterate over the snapshot and read each user into an array (as dictionary objects)
Then use NSPredicate to extract an array of users that you want.
I've run numerous tests and performance wise, it's negligible unless you have thousands of users.
Hope that helps!
To answer your questions
1) Yes, you can query with your current structure. A query can go 1 child deep, but not within a child's children.
2) If yes, how would I have to change my code, to make it work?
Here's a quickie that queries by a users last name:
Firebase *usersNodeRef = your users node 'registered_users'
FQuery *allUsersRef = [usersNodeRef queryOrderedByChild:#"lastName"];
FQuery *specificUserRef = [allUsers queryEqualToValue:#"aLastName"];
[specificUser observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSDictionary *dict = snapshot.value;
NSString *key = snapshot.key;
NSLog(#"key = %# for child %#", key, dict);
}];
How can I query for e.g. "muster" and get only the user simplelogin:1 (Max Mustermann)?
In your uses node structure, the users are store in nodes with a key.. the key is the simplelogin:1 etc. snapshot.key will reveal that. So it's key/value pair deal...
value = snapshot.value
key = snapshot.key

Resources