How to properly add data from Firebase into Array? - arrays

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.

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?

I want to add the results of a Fetch request from a Core Data entity into an Array

I am using the following code to retrieve rows from an entity in my Core Data DB. I am able to successfully fetch the data. I can also access the rows of the entity and add it to an array. However when I try using the array 'outside' the do { } enclosure, I am only able to read the last array item value. Please assist me.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let fetchRequest = NSFetchRequest<T01_test_results>(entityName: "T01_test_results")
//3
do {
let tests = try managedContext.fetch(fetchRequest)
for test in tests {
testItem.max_number = test.c01_max_number!
testItem.results = test.c01_results!
testItem.test_date = test.c01_test_date!
testItem.timesTable = test.c01_timesTable!
print("In Loop -- \(testItem.timesTable)")
testItem.total_correct = test.c01_total_correct!
testItem.total_questions = test.c01_total_questions!
testArray.append(testItem)
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
print("Single element -> \(testArray[3].timesTable)")
}
Problem seems to be solved. I was using a Class based object to store the retrieved values. Changed this to a struct based type and seems to be working fine!

Why have a Array Empty and Error in swift4?

I've spent a few hours trying to get a fetch to work in my film sheet. I need to open film's View of my colectionview Items. I could follow different guide and post but it always give me an empty array. I'm newbie, sorry for my question but I need your help.
Here's my code:
var taskArrayScheda : NewFilm?
override func viewDidLoad() {
super.viewDidLoad()
self.fetchData()
if(self.taskArrayScheda != nil) {
let schedaOk = taskArrayScheda
mostraDatiNellaScheda(schedaOk!)
} else { print("errore array vuoto") }
}
func mostraDatiNellaScheda(_ sched:NewFilm) {
// get title
titoloScheda.text = sched.titolo
}
func fetchData() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NewFilm")
do {
taskArrayScheda = try context.fetch(NewAnime.fetchRequest())
đŸ’¥ERROR ::::: Cannot assign value of type '[Any]' to type 'NewFilm?'
} catch {
print(error)
}
}
The fetch() returns an array. But currently you assign the fetch() result to the single object var taskArrayScheda.
You'll need something like:
var taskArrayScheda: [NewFilm]?
Then you should do:
taskArrayScheda = try context.fetch(NewAnime.fetchRequest()) as? [NewFilm]
I assume here that NewAnime is a subclass of NewFilm, which seems to make sense looking at these two class names.

How to query multiple keys with Firebase GeoFire using Swift?

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

Resources