How to query multiple keys with Firebase GeoFire using Swift? - arrays

I've got a GeoFire query that is bringing back user id's or keys. I'm getting back keys in sequence, but I'm getting several sequences. How can I get the last updated sequence?
#IBAction func friendsNearMeACTN(sender: AnyObject)
{
let geofireRef = self.ref.child("UserLocations")
let geoFire = GeoFire(firebaseRef: geofireRef)
let circleQuery = geoFire.queryAtLocation(self.location, withRadius: 20.6)
circleQuery.observeEventType(.KeyEntered, withBlock: { (key: String!, location: CLLocation!) in
self.localUsers.append(key)
self.getLocalUsers()
})
}
func getLocalUsers()
{
print(self.localUsers)
}
This is what I'm getting back from func getLocalUsers()....
["WGueYzDjH4NW2vneHOyGmjf6PYB3"]
["WGueYzDjH4NW2vneHOyGmjf6PYB3", "Cg4pQj36ttNUuWNqtc16tIFmI0A2"]
["WGueYzDjH4NW2vneHOyGmjf6PYB3", "Cg4pQj36ttNUuWNqtc16tIFmI0A2", "N5pgqGEhW2f7PGGVmB3AQ8v1uPk2"]
How can I simply get the last array?

The answer is that the GeoFire query is a continuous asynchronous call and needs the final code observeReadyWithBlock to feed the self.localUsers() the collected information only. Here is the example...
let regionQuery = geoFire.queryWithRegion(self.region)
regionQuery.observeEventType(.KeyEntered, withBlock: { (key: String!, location: CLLocation!) in
var users = [String]()
users.append(key)
for keys in users
{
let user = keys
allKeys.append(user)
}
self.localUsers = allKeys
self.getLocalUsers()
})
regionQuery.observeReadyWithBlock({ () -> Void in
self.getLocalUsers()
})

The problem here is that you are calling getLocalUsers func every time the observer block is fired. You are calling it for every single result. You need to count the results and add 1 to the count every time the observer block is executed. When your count reaches the result count, call the getLocalUsers function once instead of 3 times. Try the code below. I have not tested it.
#IBAction func friendsNearMeACTN(sender: AnyObject){
var i = 0//The counter
let geofireRef = self.ref.child("UserLocations")
let geoFire = GeoFire(firebaseRef: geofireRef)
let circleQuery = geoFire.queryAtLocation(self.location, withRadius: 20.6)
circleQuery.observeEventType(.KeyEntered, withBlock: { (key: String!, location: CLLocation!) in
self.localUsers.append(key)
i += 1//Add one to i every time observer fires
if i == self.key.count {//if counter (i) is equal to the keys returned call getLocalUsers func once
self.getLocalUsers()
}
})
}
func getLocalUsers(){
print(self.localUsers)
}

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?

swift - Function that fetches and appends Firebase values returns an empty string

I'm trying to create a function that grabs the category children's keys of a given user UID in Firebase, appends them to an array and then finally joins them together into one long string. Everything works well until the observeSingleEvent function completes and the return value is empty. Here's my code:
let referenceDatabase = FIRDatabase.database().reference()
func fetchBuddyInfo(category: String, buddyId: String) -> String {
var buddyInterestsArray = [String]()
var buddyInterests = String()
referenceDatabase.child("Users").child(buddyId).child(category).observeSingleEvent(of: .value, with: { (categorySnap) in
for categoryItems in categorySnap.children.allObjects as! [FIRDataSnapshot] {
buddyInterestsArray.append(categoryItems.key)
}
buddyInterests = buddyInterestsArray.joined(separator: ",")
})
return buddyInterests
}
I think it has something to do with the nested scope of the observeSingleEvent function since buddyInterests seems to lose its value after running the function, but I can't figure out how to pull the value out.
Problem
observeSingleEvent(of:with:) is an asynchronous operation, which means that it can return later after executing the function. That's why in your case you get always an empty String as return value
Solution
So in your case you can create a completion handler like this:
func fetchBuddyInfo(category: String, buddyId: String, completion: #escaping (String) -> ()) {
var buddyInterestsArray = [String]()
var buddyInterests = String()
referenceDatabase.child("Users").child(buddyId).child(category).observeSingleEvent(of: .value, with: { (categorySnap) in
for categoryItems in categorySnap.children.allObjects as! [FIRDataSnapshot] {
buddyInterestsArray.append(categoryItems.key)
}
buddyInterests = buddyInterestsArray.joined(separator: ",")
completion(buddyInterests)
})
}
And call your function like this:
fetchBuddyInfo(category: categoryString, buddyId: buddyId) { (buddyInfoString) in
// do whatever you want with your buddyInfoString
}

How to properly add data from Firebase into Array?

I'm trying to get results from Firebase and put them into Array, but it seems I miss something. What I want is to get 'Time' and 'Blood Glucose" values from Firebase and to put them into an arrays which I will use for Charts. I'm able to put the data into 'BG' and 'TIME' arrays, but when I 'append' them into 'FetchedDate' and 'FetchedBG' I see empty arrays (FetchedBG and FetchedDate)
var FetchedDate:[String]! = []
var FetchedBG: [Double]! = []
//GET DATA FROM FB
func GetDetails(){
let posts = rootRef.child("Diary/\(userID!)/\(passedDATE!)")
//let posts = rootRef.queryOrderedByChild(passedDATE!)
posts.observeEventType(FIRDataEventType.Value , withBlock: { (snapshot) in
for list in snapshot.children {
if let BG = list.value.objectForKey("Blood Glucose")!.doubleValue {
self.FetchedBG.append(BG)
print(BG) // SHOWS RESULTS AS EXPECTED
}
if let TIME = list.value.objectForKey("Time") {
self.FetchedDate.append(TIME as! String)
print(TIME) // SHOWS RESULTS AS EXPECTED
}
}
}) { (error) in
print(error.localizedDescription)
}
}
override func viewDidLoad() {
super.viewDidLoad()
GetDetails()
print(FetchedDate) // EMPTY ARRAY
print(FetchedBG) // EMPTY ARRAY
Firebase loads (and synchronizes) the data from your database asynchronously. Since that may take some time, your code continues executing and you print the arrays while they're still empty.
Once a value is available (either for the first time or when the data has changed), your block is invoked. It adds the data to the arrays. But by that time your print statements have long finished.
The solution is to move the code that needs to run when the value is available (or when it has changed) into the block. E.g.
var FetchedDate:[String]! = []
var FetchedBG: [Double]! = []
//GET DATA FROM FB
func StartSynchronizingDetails(){
let posts = rootRef.child("Diary/\(userID!)/\(passedDATE!)")
//let posts = rootRef.queryOrderedByChild(passedDATE!)
posts.observeEventType(FIRDataEventType.Value , withBlock: { (snapshot) in
for list in snapshot.children {
if let BG = list.value.objectForKey("Blood Glucose")!.doubleValue {
self.FetchedBG.append(BG)
print(BG) // SHOWS RESULTS AS EXPECTED
}
if let TIME = list.value.objectForKey("Time") {
self.FetchedDate.append(TIME as! String)
print(TIME) // SHOWS RESULTS AS EXPECTED
}
}
print(FetchedDate)
print(FetchedBG)
}) { (error) in
print(error.localizedDescription)
}
}
override func viewDidLoad() {
super.viewDidLoad()
StartSynchronizingDetails()
This is a very common pattern when your app interacts with (potentially time-consuming) network resources. It is also precisely the reason Firebase's observeEventType takes a withBlock: argument: to isolate the code that starts synchronizing from the code that responds to value updates.

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

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

Resources