Querying users in Firebase with unique UID's as keys using Swift - database

I'm trying to query users by lowercaseString name which I auto save when a user signs up. The thing is I'm creating keys for users using their uid's. Now I'm having trouble letting other users find their friends because all I'm pulling back is the very first key "user". Here's my firebase info...
Here is my code...
let userRef = self.ref.childByAppendingPath("users")
userRef.queryOrderedByChild("caseUsername")
.queryStartingAtValue(self.searchTXTFLD.text?.lowercaseString, childKey: "caseUsername")
.observeEventType(.Value, withBlock: { (snapshot) -> Void in
print(snapshot.key)
Please help me find users using self.searchTXTFLD.text?.lowercaseString. It should equal user child caseUsername.

Thanks to #Frank van Puffelen. The answer illuminated more about how Firebase data should be structured. I didn't completely use your answer, but here is what I have and what works.
#IBAction func searchFriendsACTN(sender: AnyObject)
{
let queryRef = self.ref.childByAppendingPath("users")
queryRef.queryOrderedByChild("caseUsername")
.queryEqualToValue(self.searchTXTFLD.text?.lowercaseString)
.queryLimitedToFirst(1)
.observeEventType(.Value, withBlock: { snapshot in
if snapshot.exists()
{
var newIds = [String]()
var newNames = [String]()
var newCsnames = [String]()
var newPics = [String]()
for items in snapshot.value as! NSDictionary
{
let ids = items.key
let names = items.value["displayName"] as! String
let csnames = items.value["caseUsername"] as! String
let pics = items.value["image"] as! NSString
newIds.append(ids as! String)
newNames.append(names)
newCsnames.append(csnames)
newPics.append(pics as String)
}
self.caseUserNameArray = newCsnames
self.usersID = newIds
self.usernameArray = newNames
self.photos = newPics
self.friendsTBLVW.reloadData()
}
})
}

When you have a query, there may be multiple children that match the condition. When you then listen for the .Value event, Firebase will return a snapshot that contains a list of the matching children. Even if there is only one matching child, it will still be in a list.
So the solution is to iterate over the matching children:
ref.queryOrderedByChild("caseUsername")
.queryStartingAtValue(self.searchTXTFLD.text?.lowercaseString, childKey: "caseUsername")
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
print(child.key);
}
});

Related

Firebase to array - wait for all Data before do next function

Okay, after some comments for more information to this question, let's try it:
In a viewController I'm calling a function loadPosts().
In this function I'm calling api.shared.function observePosts().
observePosts() downloads all posts at a specified database reference. with the first downloaded post back in the loadPosts() I'm calling self.fetchUser(uid: post.uid) in VC. This call api.shared.function observeUser() which gives me the user from the current post downloaded.
In observeUser() I'm filling users-array with user-object from type UserModel and in completion-block of loadPosts() I'm filling posts-array with post-object from type PostModel. After insert the post to posts-array at:0 I reload the tableView and get a view of all downloaded posts with matching info about the user that created it. (code comes after description)
Means: If there are 5 posts from type PostModel - there must be 5 userModel's, too. Every function is called 5 times - in this example with 5. Every function is running 5 times. (Tested with a print("call loadPosts")).
What I finally want is:
Get the posts and the users - fill the arrays (when all posts at this time) are loaded. Make this step again - with posts2 and users2. If all four arrays are filled - an algorithm get them together in one posts-array and one users-array, like:
post[0], post1, post[2], post2[0], post[3], post[4], post[5], post21 ...
Of course the users will be putting together in one as well.
THIS PART IS NO PROBLEM! I HAVE A WORKING CODE ... with hardcoded strings in the arrays
My problem is:
When I'm calling a function at the end of a function it can be fact that the asynchronous task of loading the posts ( users ) is not done. So, how can I make the code "wait to complete"? I have tried this from StackOverflow and also tried to work with DispatchQueue but that don't work, too.
Before the code is coming - here is my workaround:
xCode 11.3.1 with Swift 5. ( async / await - solutions from the comments are not possible )
Code(s):
In ViewController:
viewDidLoad => loadPosts()
loadPosts(){
Api.shared.observePosts { (post) in
self.fetchUser(uid: UserUid, completed:{
self.posts.insert(post, at:0)
self.tableView.reloadData()
})
}
}
func fetchUser(uid: String, completed: #escaping () -> Void) {
Api.shared.observeUser(uid: uid) { (user) in
self.users.insert(user, at: 0)
completed()
}
}
In Api
// load posts
func observePosts(completion: #escaping (PostModel) -> Void){
REF_POSTS.observe(.childAdded) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = PostModel(dictionary: dic, key: snapshot.key)
completion(newPost)
}
}
// load users
func observeUser(uid: String, completion: #escaping(UserModel) -> Void){
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
I hope that now is clear what I'm doing and what I want to to after modifying the code. Any help or info is welcome.
The question is a bit vague but what I think is being asked is:
I have two Firebase nodes (collections in Firestore), a post node and a user node. The post node contains child nodes that have a node that stores the uid of the user which are found in the users node. I want to load all the posts and the user associated with each post
If that's the question, here's a simplified solution:
First a structure to store the post and users name
struct PostAndUserStruct {
var post = ""
var userName = ""
}
and then a class var to use as a tableView dataSource
var postsAndUsersArray = [PostAndUserStruct]()
and then the code to read it in and populate that datasource for use with the tableView
func readPostsAndUser( completion: #escaping () -> Void ) {
let postsRef = self.ref.child("posts")
let usersRef = self.ref.child("users")
postsRef.observeSingleEvent(of: .value, with: { snapshot in
let allPosts = snapshot.children.allObjects as! [DataSnapshot]
let lastPostIndex = allPosts.count - 1
for (index, post) in allPosts.enumerated() {
let postText = post.childSnapshot(forPath: "post_msg").value as? String ?? "No Msg"
let uid = post.childSnapshot(forPath: "post_uid").value as? String ?? "No User"
let thisUser = usersRef.child(uid)
thisUser.observeSingleEvent(of: .value, with: { userSnapshot in
let name = userSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
let postAndUser = PostAndUserStruct(post: postText, userName: name)
self.postsAndUsersArray.append(postAndUser)
if index == lastPostIndex {
completion()
}
})
}
})
}
The flow code flow is
The postsAndUsersArray would be a tableView datasource.
Load all of the posts and cast the snapshot to an array to preserve ordering. Keep track of how many there are
Iterate over each post, capturing the post msg as well as the uid of the user that made the post.
Read each user, instantiate a PostAndUserStruct and add it to the class array (the tableViewDatasource)
When the loop gets to the last index, call the completion handler and output the results to console (good place to reload the tableView)
and is called like this
func handleButton0Action() {
self.readPostsAndUser(completion: {
for post in self.postsAndUsersArray {
print(post.userName, post.post)
}
//this is a good place to reload the tableview
})
}
The "note to self" on this is it's not super scaleable with large datasets but gets the job done for small dataset. If you have a millions posts you will need to implement pagination to read a chunk of posts at a time, otherwise you'll overload the device's memory and have random crashes.
NOTE: BELOW IS FIRESTORE CODE
The code to read the posts, read in the user for each post and return a populated array of PostAndUserStruct objects, noting self.db points to MY Firestore.
func readPostsAndUsersAsync() async -> [PostAndUserStruct] {
let postsCollection = self.db.collection("posts")
let usersCollection = self.db.collection("users")
var postDataArray = [PostAndUserStruct]()
let postSnapshot = try! await postsCollection.getDocuments()
let allPosts = postSnapshot.documents
for post in allPosts {
let postText = post.get("post_msg") as? String ?? "No Post Message"
if let uid = post.get("post_uid") as? String {
let userDoc = usersCollection.document(uid)
let userSnap = try! await userDoc.getDocument()
let name = userSnap.get("name") as? String ?? "No Name"
let postData = PostAndUserStruct(post: postText, userName: name)
postDataArray.append(postData)
}
}
return postDataArray
}
The code flow:
All of the documents are read from Firestore with getDocuments. Then we cast those documents to the allPosts array.
Iterate over the array, capturing the post_msg field from each post as well as the uid of the use that posted it.
Then, using the uid, read the users document from the users collection and get the users name.
Instantiate a PostAndUserStruct object with the post and users name and add it to an array.
Then return the populated array
That function is called like this and outputs the users name and post text to console.
func fetchPostsAndUsers() {
Task {
let postResults = await self.readPostsAndUsersAsync()
for postAndUser in postResults {
print(postAndUser.userName, postAndUser.post)
}
}
}

TableView is not populating after populated from loaded array

I have read through the similar questions, but am still stuck.
I have an array in a separate swift file called getSkills. It connects to a Firebase database and returns the skills into a variable/array called mySkills.
func getSkills(owner: String) {
mySkills = []
let db = Firestore.firestore()
db.collection("Skills").order(by: "SkillName").getDocuments { (querySnapshot, error) in
if let e = error {
print(e)
//TO DO: show error on screen
} else {
if let safeDocs = querySnapshot?.documents {
for doc in safeDocs {
let data = doc.data()
if let sklName = data["SkillName"] as? String,
let sklType = data["SkillType"] as? String,
let sklLevel = data["SkillLevel"] as? String,
let sklIsTimed = data["isTimed"] as? Bool,
let sklSecs : Int = data["Seconds"] as? Int,
let sklOwner = data["Owner"] as? String,
let sklID = doc.documentID as? String {
let newSkill = Skill(id: sklID, name: sklName, type: sklType, isTimed: sklIsTimed, seconds: sklSecs, level: sklLevel, owner: sklOwner)
mySkills.append(newSkill)
}
}
}
}
}
}
In a separate swift view controller, I am calling the getSkills function in ViewDidLoad.
override func viewDidLoad() {
super.viewDidLoad()
skillList.dataSource = self
skillList.delegate = self
DispatchQueue.main.async {
getSkills(owner: getUserID())
print(mySkills.count)
self.skillList.reloadData()
}
However, when the View Controller displays in the simulator the tableview is not populated with the list of Skills.
I can get the issue to work with a "LoadData()" function in the View Controller. However, I'm trying to build a reusable getSkills function that I can call in multiple locations as opposed to writing the same code over and over.
I've tried the following things:
Dispatch Main Queue in the getSkills function
Dispatch main queue in the ViewController itself
Moving the Dispatch main queue lines to the ViewWillAppear function on the ViewController.
wrapping the mySkills.append in the getSkills function within Dispatch Main Queue
Any ideas?

Pick only "name" from every entity from Core Data as Array Swift 4

I have User model and Car model in CoreData. I saved 3 cars for 1 user.
How can I pick only name variable of every car and set to an array?
let users = try! managedContext.fetch(userFetch)
let currentUser: User = users.first as! User
let cars = currentUser.cars?.allObjects as! [Car]
print(currentUser.email)
let carsList = [String]
cars.forEach { sample in
carsList.append(sample.name)
}
Is there a solution better than mine?
There is a better solution
let carList = cars.map { $0.name }
And if you declare (and design) cars as non-optional Set<Car> rather than unspecified NSSet? there is a still better one
let currentUser = users.first as! User
let carsList = currentUser.cars.map { $0.name }
No allObjects, no type cast

Firestore - appending data to a single value inside a struct

How can I update a single value inside of a struct. Currently I'm fetching all of the data inside multiple documents of a collection with the below function. the data structure is as follows:
People - collection
DocumentID
Name: "Joe"
Friends (Object)
1 (Object)
Name: "Sally"
2 (Object)
Name: "Sam"
DocumentID
Name: "Emily"
Friends (Object)
1 (Object)
Name: "Peter".
If I run the below code it jut creates a new array, whereas I would like to merge the two together. Any help is greatly appreciated. Many thanks!!
func loadData() {
let userRef = db.collection("Users").document(user!)
let docRef = userRef.collection("People")
docRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshot = querySnapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let newPeople = People(name: name, friends: [:])
self.peopleArray.append(newPeople)
if let friends = data["Friends"] as? [String: Any] {
for (key, _) in friends {
let number = friends[key] as? [String: Any] ?? [:]
let friendsName = number["name"] as? String ?? ""
\\ The code in which I want to update the value of friendsName into the previous set array
let newFriends = People(name: name, friendsName: friendsName)
self.peopleArray.append(newFriends)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
What I would like the array to look like:
[ name: Joe, friends: ["name": Sally, "name": Sam]], [name: Emily, friends: [name: Peter]]
Updated Code
var friendsName: [String: [String: Int]] = [:]
var friendsArray = [String: Int]()
func loadData() {
let userRef = db.collection("Users").document(user!)
let docRef = userRef.collection("People")
docRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshot = querySnapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
if let friends = data["Friends"] as? [String: Any] {
for friend in friends {
let key = friend.key
let values = friend.value
let friendsDict = friends[key] as? [String: Any] ?? [:]
let friendsNameString = friendsDict["name"] as? String ?? ""
self.friendsArray.updateValue(friendsNameString, forKey: key)
}
self.friendsName.updateValue(self.friendsArray, forKey: "name")
let newPeople = People(name: name, friends: self.friendsName)
self.peopleArray.append(newPeople)
self.friendsArray.removeAll()
self.friendsName.removeAll()
}
}
}
}
}
}
The way you are doing it seems a little too complex to what you require. Firestore has quite fast queries so you can have different collections for people and friends.
Lets say for example that in your "people" collection you have the names of all the users and other properties that you may need... Email, age, gender, etc etc.
Then in a second collection you can add the relations for each user. For example, "Friends" contains documents with fields friendA: AAA, FriendB: BBB. Then you won't have any duplicates in your data.
The way you are doing it creates too many duplicated that you don't need. For example, in your Joe document you have set that Sally is a friend. But then in your Sally document you will have to set that Joe is a friend. This makes the database more difficult to maintain and with a lot of duplicates.
If you make two different collections for People and Friends the code for fetching also becomes simpler.

How can you combine two firebase observation results into the same array

I have two nodes in my firebase DB that tracks like and comment notifications. Each value is parsed as a Notification class and I am looking to be able to get the snapshots from each node and create a merged notification array. The two functions I have work to pull the respective data but I dont know how to merge the two arrays. I tried to use sets and then form a union but I cant do it with non hashable values. The only other solution I could think of was to combine the two functions into one, however, this seemed and looked very wrong. Here is what Im working with...
var commentNotifications = [Notifications]()
func commentNotificationObserver(_ update: #escaping () -> Void) {
commentHandle = CURRENT_USER_COMMENT_NOTIFICATION.observe(DataEventType.value, with: { (snapshot) in
self.commentNotifications.removeAll()
for child in snapshot.children.allObjects as! [DataSnapshot] {
let id = child.key
let dict = child.value as! Dictionary<String, Any>
let notif: Notifications = Notifications(notifID: id, data: dict)
self.commentNotifications.insert(notif, at: 0)
update()
}
if snapshot.childrenCount == 0 {
update()
}
})
}
// Detects New Likes
var notifications = [Notifications]()
func likeNotificationObserver(_ update: #escaping () -> Void) {
likesHandle = CURRENT_USER_LIKE_NOTIFICATION.observe(DataEventType.value, with: { (snapshot) in
self.notifications.removeAll()
for child in snapshot.children.allObjects as! [DataSnapshot] {
for dictionary in child.value as! Dictionary<String, Any> {
let id = dictionary.key
let dict = dictionary.value as! Dictionary<String, Any>
let notif: Notifications = Notifications(notifID: id, data: dict)
self.notifications.insert(notif, at: 0)
update()
}
}
if snapshot.childrenCount == 0 {
update()
}
})
}
So as you can see I have a notifications array and a commentNotification array. How can I get these into the same array without duplicates. Thanks!

Resources