Compare values with an array of values with Firebase. Swift - ios

I have a value which needs to be compared with an array of values. Basically a user needs to check if it has the same item as another user. But I am struggling to solve this as how do I get the item of an array of users? Normally when you observe a value you do something like this:
Database.database().reference(withPath: "users/\(userID)/itemList").observeSingleEvent...
However, when it comes to an array of users itemList how can this be achieved if there are multiple ID's? I'd like compare a user item or items with other users item and check if they match in order to sort an array of users that have a match.
If there is already an example for this please help direct me there.
Update
This is how my data structure looks:
{
"users": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": {
"firstName": "Friederike",
"itemList": [
2: true,
3: true,
0: true
]
},
"C076OYmVTzOiHgnq4wPQtY2XpED2": {
"firstName": "Ian",
"itemList": [
0: true,
1: true,
3: true
]
},
"Juoiuf0N6qNmkm32jrtu6X6UK62": {
"itemList": [
0: true
],
"firstName": "Jack"
}
}
}
Update 2.0
With the answer below I am able to query through the items table but still unable to query the keys that match as there can be multiple arrays and therefore I cannot use it to filter or anything.

Ok so with your current data structure you'd have to query all nodes under users and then compare the arrays, which is very inefficient. There isn't a straight forward or easy way to do that without modifying your structure. So I suggest you modify your data structure so that each item has a list of all users that have it. Something like this:
{
"items": {
"0": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": "Friederike",
"C076OYmVTzOiHgnq4wPQtY2XpED2": "Ian",
"Juoiuf0N6qNmkm32jrtu6X6UK62": "Jack"
},
"1": {
"C076OYmVTzOiHgnq4wPQtY2XpED2": "Ian"
},
"2": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": "Friederike"
}
//....
}
}
Depending on what you want to display you might want to store more information than just the users UID and username. You can query all the users that have the same items as you using a query like this:
let ref = Database.database().reference()
// assuming you already have the current users items stored in an array
for item in items {
ref.child("items").child(String(item)).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let key = child?.key, let name = child?.value as? String {
// do something with this data
}
}
})
}
Firebase database is noSQL, so data is meant to be denormalized or duplicated so that queries can be optimized. Firebase actually recommends that you avoid nesting data. Take a look at this question for more information.
Hope that helps
Code related to question asked in comments
Assuming you are storing the UID's or names of users with the same items in a string array you can prevent duplicates using .contains()
var namesWithMatchingItems = [String]()
if !namesWithMatchingItems.contains(nameYouJustFetched) {
namesWithMatchingItems.append(nameYouJustFetched)
}

Related

Query Firebase nested data to check specific value - iOS Swift

I have an array of User ID Strings and I want to query data in Firebase Realtime Database to see if each User ID has a specific gender.
var nearbyUserArray = [String]()
Below is an example of the data I have in Firebase. The second level deep is the User ID (0UFhIgGAwIY1btnBeNaNkJruYKJ3). What I am trying to do is loop through my User ID array and for each User ID check if that specific user is Male. If the user is Female, remove that User ID from the array.
"profiles" : {
"0UFhIgGAwIY1btnBeNaNkJruYKJ3" : {
"dateOfBirth" : "May 11, 1987",
"gender" : "Female",
"higherAgeRange" : 36,
"interestedIn" : "Male",
"lowerAgeRange" : 29,
"name" : "Sally"
},
"6vNjngZqoTbd4oGudip3NX78pu32" : {
"dateOfBirth" : "December 23, 1990",
"gender" : "Male",
"higherAgeRange" : 39,
"interestedIn" : "Female",
"lowerAgeRange" : 25,
"name" : "Bob"
}
How would you recommend I accomplish this? I want to minimize the amount of data I have to download and manipulate on the client side. Below is what I am attempting to do but I cannot get it to work. Any ideas why this always returns Null?
func getUsersWithinGenderCriteria() {
for user in nearbyUserArray {
DatabaseService.shared.profilesRef.child(user).queryOrdered(byChild: "gender").queryEqual(toValue: "Female").observeSingleEvent(of: .value) { [weak self] (snapshot) in
print("snap: \(snapshot)")
// The idea is if snapshot is Female, remove the user ID from the array. If the snapshot is Null, keep it in the array. However, this is not working as all snapshots come back as null.
}
}
}
Also, here are the security rules I set up.
"profiles": {
"$userId": {
".indexOn": ["gender"]
}
}
When you perform a query, Firebase compares a property of each child node under the location you query to the value you pass in.
I assume that profilesRef points to /profiles in your database. In that case profilesRef.child(user) in your code points to the profile for a specific user already. When you then run a query, you are querying each of the properties of that specific user.
What you like want to do is to get all users with gender equal to Female. In that case you should query the user list:
DatabaseService.shared.profilesRef.queryOrdered(byChild: "gender").queryEqual(toValue: "Female")...
Note that you also should define the index on the level where the query is run, so on /users:
"profiles": {
".indexOn": ["gender"]
}
Another alternative (the might scale better) is to not use a query altogether, but instead load the gender property of each individual user in your array. In (approximate) code:
for user in nearbyUserArray {
DatabaseService.shared.profilesRef.child(user).child( "gender").observeSingleEvent(of: .value) { [weak self] (snapshot) in
if snapshot.value == "Female" {
print("snap: \(snapshot)")
}
}
}

Firebase load related data iOS

What's the best way to load "related" data in swift?
Common setup, if I have a list of users all stored under uid node and contains a list of follows which stores uids, something like:
"users" : {
"abc123" : {
"email" : "test#test.com",
"follows" : {
"xyz789" : true
}
},
"xyz789" : { ... }
}
What's the most efficient way of loading in the data for all the users one user follows? Is it best to loop through each of the uid's with observeSingleEvent(of: .value)?
This is the solution I've come up with, but feels somewhat cumbersome:
func loadRelated(user: User, completion: #escaping (Bool, [UserObject]) -> ()) {
let ref = Database.database().reference(withPath: "users/" + user.uid + "/follows")
ref.observeSingleEvent(of: .value) { snapshot in
var uids = [String]()
for child in snapshot.children {
let userData = child as! DataSnapshot
uids.append(userData.key)
}
let userRef = Database.database().reference(withPath: "users")
var users = [UserObject]()
var count = 0
uids.forEach { uid in
userRef.child(uid).observeSingleEvent(of: .value) { snapshot in
let user: UserObject(from: snapshot)
users.append(user)
count += 1
if count == uids.count {
completion(true, users)
}
}
}
}
}
I don't really want to go down the denormalization path and store each users data under the top level user.
If you are decided on using Realtime Database, it is best practice to create another root node in your case called user-follows. You can create a follow at the path user-follows/$uid/$fid by setting the value to true, then on your app you would have to observeSingleEvent for each snapshot key ($fid) at user-follows/$uid.
To avoid having to observe each follow separately, instead of setting the value to true, you can just store the data you need about a user in user-follows/$uid. However, a user may change their username for example and so you would need to keep the data inside each user-follows up to date. You can utilise Firebase Cloud Functions to maintain the user-follows when a user changes their information.
Otherwise, I would suggest looking at Firebase Firestore, where some nesting is allowed.
If you know that your node at /users will always contain few users, you could try to get all the users at once with a observeSingleEvent(of:) at path /users. Then filter the users with the ones who are in ../follows.
This may pull more data but it might be faster (not sure) and will need less code to handle.
In fact your initial implementation is quite performant already. Just make sure to handle correctly failing of observeSingleEvent(of:) or the condition count == uids.count will never be fulfilled.
By the way storing each user under ../follows will just duplicate your data and will be hard to maintain updated. So yes avoid it.

Get a value from a dictionary given the key of a different field

I'm using the third-party library SwiftAddressBook to work with Contacts in my app. I want to iterate through contacts in my device and extract a few specific values. Namely the twitter and facebook usernames if they exist.
SwiftAddressBook has an object called SwiftAddressBookPerson which represents a single contact. And SwiftAddressBookPerson object has a property called socialProfiles which contains an array of dictionaries. Each dictionary contains info about available social media account such as the username, url etc. Printing the value of socialProfiles looks like this.
[
SwiftAddressBook.MultivalueEntry<Swift.Dictionary<SwiftAddressBook.SwiftAddressBookSocialProfileProperty, Swift.String>>(value: [
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://twitter.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "twitter"],
label: nil, id: 0),
SwiftAddressBook.MultivalueEntry<Swift.Dictionary<SwiftAddressBook.SwiftAddressBookSocialProfileProperty, Swift.String>>(value: [
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://www.facebook.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "facebook"],
label: Optional("facebook"), id: 1)
]
I cleaned it up a little by doing this socialProfiles.map { $0.map { $0.value } } which outputs the following.
[
[
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://twitter.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "twitter"
],
[
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.url: "http://www.facebook.com/isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username: "isuru",
SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service: "facebook"
]
]
What I want now is given the service's name (twitter, facebook), retrieve the username used for that service.
How do I parse through the dictionary and get the value of a different field?
Given that this is an array, I'd use a first { $0.service == serviceName }.
Alternatively, you could loop over the entire array ahead of time to create a dictionary with key as serviceName, and value as the profile property. The advantage of this approach is subsequent lookups would be much faster.
I have named your array of swift address book dictionaries, profiles. The forced unwrapping in the flatMap call is safe because both key and value are tested for non-nil in the previous filter call.
The result is a single dictionary containing username values for the services listed in your interestingServices array.
let service = SwiftAddressBook.SwiftAddressBookSocialProfileProperty.service
let username = SwiftAddressBook.SwiftAddressBookSocialProfileProperty.username
let interestingServices = ["facebook", "twitter"]
let usernames = profiles
.filter {
guard
let service = $0[service],
$0[username] != nil
else {
return false
}
return interestingServices.contains(service)
}
.flatMap { profile in
return [profile[service]!, profile[username]!]
}

Getting a specific child value from nodes only in which user's UID is present

I am working on a group chat application, and I'm running into trouble trying to make a specific query to Firebase DB.
I need to get all of the profile pictures of users in a room. But only the rooms that the current user is a part of.
private func observeRooms() {
let databaseRef = FIRDatabase.database().reference()
let groupRef = databaseRef.child("groups")
guard let uid = FIRAuth.auth()?.currentUser?.uid else {return}
let queryRef = groupRef.queryOrdered(byChild: "participants/\(uid)").queryEqual(toValue: nil)
queryRef.observe(.childAdded, with: { snapshot in
let roomDict = snapshot.value as! [String: AnyObject]
print("roomDict: \(roomDict)")
let id = snapshot.key
let avatar = roomDict["profilePicture"] as! String
})
}
Obviously setting queryEqual(toValue: nil) returns all the rooms that the current user is NOT a part of - the opposite of what I need. I know the inverse will be to check queryEqual(toValue: true), but the : true is not showing up in the database.
This is how I add a node to "groups" in Firebase:
// Start group node for new room
let groupRef: FIRDatabaseReference = FIRDatabase.database().reference().child("groups")
let roomID = newRoomRef.key
let groupRoomRef = groupRef.child(roomID)
let groupDict = ["roomName" : roomName, "participants": [uid : true]] as [String : Any]
groupRoomRef.setValue(groupDict)
And this is how the data structure ends up:
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"gender" : "male",
"handle" : "Testing",
"name" : "Test User",
"profilePicture" : "https://graph.facebook.com/*removed*/picture?type=large&return_ssl_resources=1",
"status" : "F8B016"
}
},
"roomName" : "Test Room"
},
How can I properly the profilePicture values for all users in a room, only for the groups that the current user's UID is a participant of?
You're on the right track. When using queryEqual your query needs to match the value actually present in the database. Unfortunately that gets complicated when the values you're querying against are complex objects. In fact I'm not sure the function will work even if you did match the entire object value. The issue here is that you're trying to look up objects by the presence of a child key, instead of actually sorting by or matching their values. That doesn't quite fit the use case of any of the orderBy methods.
There are a few ways to work around this, ordered by effort.
First, you could take advantage of the sort order to get only those who have some object with the name of the UID. Since Firebase sorts nulls first, then primitives, and finally objects, you should be able to order by child and start at true (or any primitive) to get all object instances with that key (assuming, of course, you're not storing UID: true anywhere).
groupRef.queryOrdered(byChild: "participants/\(uid)").queryStartingAtValue(true)
Second, duplicate the UID as a key inside the object with the value true so that you can sort on it directly. Notice the extra step in the query.
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : true,
...
groupRef.queryOrdered(byChild: "participants/\(uid)/\(uid)").queryEqual(toValue: true)
Finally, and this is what I recommend, reconsider your data structure. You're nesting a lot of data inside this collection of collections that is going to make your queries and updates unwieldy. Firebase tends to work better when you denormalize your data, preferring flat/shallow structures with some duplication of simple data. In your case, instead of storing the user's profile data inside of their group membership, you might have a collection of groups and another collection of users, with each group containing only a list of UIDs to its members and vice versa. The example in the documentation is a very similar case to yours involving membership in chat conversations. At that point your UIDs (and group IDs, if you're querying on users instead) will have a value of true so you can again use a simple value query.
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : true
...
"users" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : true
...
groupRef.queryOrdered(byChild: "participants/\(uid)").queryEqual(toValue: true)

Best practice to retrieve list of friends information. How to detect multiple queries are finished?

I have users structure lke this:
{
"users": {
"uniqueID1": {
"name": "Anon",
"friends": {
"uniqueID2": true,
"uniqueID3": true
}
}
"uniqueID2": { },
"uniqueID3": { },
}
}
I want to show a user's friends' names. I have to access $user/friends/ to get list of unique IDs, and iterate the list to get friend's information. But iterating the unique ID is making multiple queries, and I have to always check if all of my queries are finished. According to the doc, it seems multiple queries will not impact the performance too much, but if I want to update my view only when all of the queries are finished, I have to check how many queries are finished.
Is there no way of 'execute a completion block when all queries are finished'?
Pseudocode
var totalNumOfFriends = 0
var tempArray = NewArray()
ref(/users/uniqueID1/friends).observeEventType{ snapshot
var uIDList = snapshot.children's keys
totalNumOfFriends = uIDList .count
for uID in uIDList {
var nameRef = ref(/users/uID/name) i.e. /users/uniqueID3/name
nameRef.observeSingleEventOfType { snapshot
var username = snapshot.value
tempArray.append(username)
if tempArray.count == totalNumOfFriends {
// If counts are the same, tempArray has all of my friends' names
// Now update view using tempArray
}
}
}
}
Pseudocode explanation:
Get list of unique IDs from /users/uniqueID1/friends
'Save' number of unique IDs. (Explained in step 4)
For each unique IDs from the list, get user's name by using ref like this /users/uniquedID2/name
For each name retrieved, add it to temporary array. Once the count of the temporary array equals to the count from step 2, update my view as I have retrieved all the names.
Firebase has no built-in way to signal when a number of queries has finished. But you can easily implement this in your own code. Your approach with a counter that checks how many items have already been loaded is the most common approach for that.

Resources