Access array outside of Firebase call - swift

From the documentation I see that I can get some user data (which I'm already getting correctly), however, the way it's structured, it doesn't allow me to access the array outside of it, this is what I mean, I have a function:
func observe() {
let postsRef = Database.database().reference(withPath: "post")
struct test {
static var tempPosts = [Post]()
}
postsRef.observe(.value, with: { snapshot in
for child in snapshot.children {
if let childSnapshot = child as? DataSnapshot,
let data = childSnapshot.value as? [String:Any],
// let timestamp = data["timestamp"] as? Double,
let first_name = data["Author"] as? String,
let postTitle = data["title"] as? String,
let postDescription = data["description"] as? String,
let postUrl = data["postUrl"] as? String,
let postAddress = data["Address"] as? String,
let url = URL(string:postUrl)
{
// Convert timestamp to date
// let newDate = self.getDateFromTimeStamp(timestamp:timestamp)
// Store variables from DB into post
let post = Post(author: first_name, postTitle: postTitle, postDescription: postDescription, postUrl: url, postAddress: postAddress)
test.tempPosts.append(post)
}
}
self.posts = test.tempPosts
// HERE IT WORKS
print(test.tempPosts[0].postTitle , " 0")
self.tableView.reloadData()
})
// HERE IT DOESN'T WORK
print(test.tempPosts[0].postTitle , " 0")
}
and I'm trying to access the data where it says: // HERE IT DOESN'T WORK, how can I access that array outside of it? I need to call it later

The observe() method is asynchronous, so after you call postsRef.observe the code executed within that closure is run ONLY AFTER the application receives a response from Firebase, so there's a delay. All code after this call that's NOT stored within the closure will be executed immediately though.
So the .observe asynchronous function call is executed, and then the next line under // HERE IT DOESN'T WORK is executed immediately after. This is why this doesn't work because test.tempPosts doesn't contain any values until after the server response is received, and by that time, your print statement outside the closure has already run.
Check out this StackOverflow answer to get some more information about async vs sync.
Asynchronous vs synchronous execution, what does it really mean?
Also too, you may want to look into closures on Swift here.
If you want to access the value outside of the closure, you'll need to look into using a completion handler or a class property.
Edit:
Here's an example
func observe (finished: #escaping ([Post]) -> Void) {
// ALL YOUR CODE...
finished(test.tempPosts)
}
func getTempPosts () {
observe( (tempPosts) in
print(tempPosts)
}
}

Related

How to read data from firebase and append it to an array

I have a userImages collection and I am trying to read data from it and appending it to my imgUrls array.
This is my code where I read the data from the database and try appending it to my array. Unfortunately, I keep getting an error because the array is apparently empty.
override func viewDidLoad() {
var ref: DatabaseReference!
let userID = Auth.auth().currentUser?.uid //holds the current user uid
ref = Database.database().reference()
var imgUrls = [String]() //array to hold the image urls from the userImages collection
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { (snapshot) in //read from userImages collection only from the subcollection where the
guard let dict = snapshot.value as? [String: Any] else { return } //document Id equals the current user uid. Create a dictionary from the
//snapshot values
let values = dict.values //holds the values from the dictionary
for value in values { //for loop to go through each value from the dictionary
imgUrls.append((value as? String)!) //and append to the imgUrls array
}
}
testLabel.text = imgUrls[0] //I used this to test, but i get an error saying the array is empty
}
I posted a question before, but it was so convoluted I decided to delete it and repost it simpler.
Any help is much appreciated!
the reason you are not getting anything in your testLabel.text is because:
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { (snapshot) in //read from userImages collection only from the subcollection where the
...
}
is an asynchronous function called. That is it will be done sometimes in the future.
but your:
testLabel.text = imgUrls[0] //I used this to test, but i get an error saying the array is empty
is outside of this call. So the results in "imgUrls" are not available yet.
Put this line inside the function or wait until it has finished before using the results.
You try to use the array before the observeSingleEvent closure is executed. All observation calls to Firebase are asynchronous. This means that you test code is executed before the closure and the array is still empty.
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
// this closure is executed later
guard let dict = snapshot.value as? [String: Any] else { return }
let values = dict.values
for value in values {
imgUrls.append((value as? String)!)
}
// your test code should bee here
}
// this is executed before closure and the array is empty
testLabel.text = imgUrls[0]
For this reason you get the error. You need to add your test code into the closure.
However, there is another catch. Asynchronous calls (their closures) are executed on the background thread.
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
// this is executed on the background thread
}
However, all user interface calls must be executed on the main thread. Also wrap your test code by calling the main thread, otherwise you won't see the result in the user interface.
DispatchQueue.main.async {
// this code is executed on the main thread
// all UI code must be executed on the main thread
}
After editing, your code might look like this:
override func viewDidLoad() {
var ref: DatabaseReference!
let userID = Auth.auth().currentUser?.lid
ref = Database.database().reference()
var imgUrls = [String]()
ref.child("userImages").child(userID!).observeSingleEvent(of: .value) { snapshot in
guard let dict = snapshot.value as? [String: Any] else { return }
let values = dict.values
for value in values {
imgUrls.append((value as? String)!)
}
DispatchQueue.main.async {
testLabel.text = imgUrls[0]
}
}
}

How to instantiate a class with asynchronous Firebase methods in Swift?

I am instantiating a User class via a Firebase DataSnapshot. Upon calling the initializer init(snapshot: DataSnapshot), it should asynchronously retrieve values from two distinct database references, namely pictureRef and nameRef, via the getFirebasePictureURL and getFirebaseNameString methods' #escaping completion handlers (using Firebase's observeSingleEvent method). To avoid the 'self' captured by a closure before all members were initialized error, I had to initialize fullName and pictureURL with temporary values of "" and URL(string: "initial"). However, when instantiating the class via User(snapshot: DataSnapshot), these values are never actually updated with the retrieved Firebase values.
import Firebase
class User {
var uid: String
var fullName: String? = ""
var pictureURL: URL? = URL(string: "initial")
//DataSnapshot Initializer
init(snapshot: DataSnapshot) {
self.uid = snapshot.key
getFirebasePictureURL(userId: uid) { (url) in
self.getFirebaseNameString(userId: self.uid) { (fullName) in
self.fullName = fullName
self.profilePictureURL = url
}
}
func getFirebasePictureURL(userId: String, completion: #escaping (_ url: URL) -> Void) {
let currentUserId = userId
//Firebase database picture reference
let pictureRef = Database.database().reference(withPath: "pictureChildPath")
pictureRef.observeSingleEvent(of: .value, with: { snapshot in
//Picture url string
let pictureString = snapshot.value as! String
//Completion handler (escaping)
completion(URL(string: pictureString)!)
})
}
func getFirebaseNameString(userId: String, completion: #escaping (_ fullName: String) -> Void) {
let currentUserId = userId
//Firebase database name reference
let nameRef = Database.database().reference(withPath: "nameChildPath")
nameRef.observeSingleEvent(of: .value, with: { snapshot in
let fullName = snapshot.value as? String
//Completion handler (escaping)
completion(fullName!)
})
}
}
Is there a reason this is happening, and how would I fix this so it does initialize to the retrieved values instead of just remaining with the temporary values? Is it because init isn't asynchronous?
Edit: I am reading data from one node of the Firebase database and, using that data, creating a new node child. The method that initializes the User class will create this new node in the database as:
As you can see, the children are updated with the temporary values so it seems the program execution does not wait for the callback.
Any help would be much appreciated!
By the comments, it seems we could reduce the code considerably which will also make it more manageable
(SEE EDIT)
Start with a simpler User class. Note that it is initialized by passing the snapshot and then reading the child nodes and populating the class vars
class UserClass {
var uid = ""
var username = ""
var url = ""
init?(snapshot: DataSnapshot) {
self.uid = snapshot.key
self.username = snapshot.childSnapshot(forPath: "fullName").value as? String ?? "No Name"
self.url = snapshot.childSnapshot(forPath: "url").value as? String ?? "No Url"
}
}
then the code to read a user from Firebase and create a single user
func fetchUser(uidToFetch: String) {
let usersRef = self.ref.child("users")
let thisUserRef = usersRef.child(uidToFetch)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
let user = UserClass(snapshot: snapshot)
//do something with user...
} else {
print("user not found")
}
})
}
I don't know how the user is being used but you could add a completion handler if you need to do something else with the user outside the Firebase closure
func fetchUser(uidToFetch: String completion: #escaping (UserClass?) -> Void) {
//create user
completion(user)
EDIT:
Based on additional info, I'll update the answer. Starting with restating the objective.
The OP has two nodes, a node that stores user information such as name and another separate node that stores urls for pictures. They want to get the name from the first node, the picture url from the second node and create a new third node that has both of those pieces of data, along with the uid. Here's a possible structure for pictures
pictureUrls
uid_0: "some_url/uid_0"
uid_1: "some_url/uid_1"
and then we'll use the same /users node from above.
Here's the code that reads the name from /users, the picture url from /pictureUrls combines them together and writes out a new node with an /author child that contains that data and the uid.
func createNode(uidToFetch: String) {
let usersRef = self.ref.child("users")
let thisUserRef = usersRef.child(uidToFetch)
let imageUrlRef = self.ref.child("pictureUrls")
let thisUsersImageRef = imageUrlRef.child(uidToFetch)
let allAuthorsRef = self.ref.child("allAuthors")
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let userName = snapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
thisUsersImageRef.observeSingleEvent(of: .value, with: { imageSnap in
let imageUrl = imageSnap.value as? String ?? "No Image Url"
let dataToWrite = [
"full_name": userName,
"profile_picture": imageUrl,
"uid": uidToFetch
]
let thisAuthorRef = allAuthorsRef.childByAutoId()
let authorRef = thisAuthorRef.child("author")
authorRef.setValue(dataToWrite)
})
})
}
The output to firebase is this
allAuthors
-LooqJlo_Oc-voUHai3k //created with .childByAutoId
author
full_name: "Leroy"
profile_picture: "some_uid/uid_0_pic"
uid: "uid_0"
which exactly matches the output shown in the question.
I removed the error checking to shorten the answer so please add that back in and I also omitted the callback since it's unclear why one it needed.
This is very hacky.
You should add completionHandler in init method. So, when your asynchronous call completed you will get actual value of object.
init(snapshot: DataSnapshot, completionHandler: #escaping (User) -> Void) {
self.uid = snapshot.key
getFirebasePictureURL(userId: uid) { (url) in
self.getFirebaseNameString(userId: self.uid) { (fullName) in
self.fullName = fullName
self.profilePictureURL = url
completionHandler(self)
}
}
}
I hope this will help you.

How to wait for asynchronous task to end - Swift 4

I need to read data from a Firebase database and insert the data inside a struct, on which I'll later do some operations. The issue that I'm facing right now is that, since Firebase manages these things in an async way, when I check inside the struct, it's often nil and not filled with data yet.
I know that using the value outside the async block as I'm actually doing is wrong and that callbacks can be used to solve my problem. I've read a bunch of examples online, but I can't figure out how to implement them in my code.
For the sake of simplicity, in the example below I download just one object from the database.
func fetchJSON(key: String) -> Void {
var meal = Meal()
let ref = rootRef.child(key)
ref.observe(.value) { (snap: DataSnapshot) in
meal.firstMeal = snap.childSnapshot(forPath: "first").value as! String
meal.secondMeal = snap.childSnapshot(forPath: "second").value as! String
meal.thirdMeal = snap.childSnapshot(forPath: "third").value as! String
}
self.meals.append(meal)
}
And here's what the function call looks like:
fetchJSON(key: currentDate)
Could someone please help me?
For Firebase you may also find handy promises. I use the PromiseKit library and it's working great.
You could do something like this:
fetchJSON(key: currentDate).done { meal in
print(meal)
}
func fetchJSON(key: String) -> Promise<Meal> {
let ref = rootRef.child(key)
return Promise { seal in
ref.observe(.value) { (snap: DataSnapshot) in
guard let firstMeal = snap.childSnapshot(forPath: "first").value as! String,
let secondMeal = snap.childSnapshot(forPath: "second").value as! String,
let thirdMeal = snap.childSnapshot(forPath: "third").value as! String {
let meal = Meal(firstMeal: firstMeal,
secondMeal: secondMeal,
thirdMeal: thirdMeal)
seal.fulfill(meal)
}
}
}
}
You can use completionHandler, first declare typealias in your class
typealias completion = (_ isFinished:Bool) -> Void
then add completionHandler to your function:
func fetchJSON(key: String, completionHandler: #escaping completion){
var meal = Meal()
let ref = rootRef.child(key)
ref.observe(.value) { (snap: DataSnapshot) in
meal.firstMeal = snap.childSnapshot(forPath: "first").value as! String
meal.secondMeal = snap.childSnapshot(forPath: "second").value as! String
meal.thirdMeal = snap.childSnapshot(forPath: "third").value as! String
}
meals.append(meal)
completionHandler(true)
}
then call your function like this way:
fetchJSON(key: currentDate, completionHandler: { (isFinished) in
if isFinished {
// do Somthing
}
})

Calling method with error: Expected argument (Variable) ->()

I am trying to call a method with a completion handler, but I can't seem to call it without this error. I am confused on what its asking for.
Here is the method I can calling:
func fillFromFile(completionBlock: #escaping ([Asset_Content]) -> ()) {
let url = "URLSTRING"
LoadJSONFile(from: url) { (result) in
// The code inside this block would be called when LoadJSONFile is completed. this could happen very quickly, or could take a long time
//.map is an easier way to transform/iterate over an array
var newContentArray = [Asset_Content]()
for json in result{
let category = json["BIGCATEGORY"] as? String
let diagnosis = json["DIAGNOSIS"] as? String
let perspective = json["PERSPECTIVE"] as? String
let name = json["NAME"] as? String
let title = json["Title"] as? String
let UnparsedTags = json["TAGS"] as? String
let filename = json["FILENAME"] as? String
let tagArray = UnparsedTags?.characters.split(separator: ",")
for tag in tagArray!{
if(!self.ListOfTags.contains(String(tag))){
self.ListOfTags.append(String(tag))
}
}
let asset = Asset_Content(category!, diagnosis!, perspective!, name!, title!, filename!)
// This is a return to the map closure. We are still in the LoadJSONFile completion block
newContentArray.append(asset)
}
print("return count ", newContentArray.count)
// This is the point at which the passed completion block is called.
completionBlock(newContentArray)
}
}

Extract Value from Block Swift

I want to be able to use the the currentVote Int outside of the block, where currentVote is a variable defined at the top of the class.
databaseRef.child("Jokes").child(currentKey).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let currentNumber = snapshot.value! as? [String: AnyObject] {
let currentValue = (currentNumber["votes"] as? Int)!
self.currentVote = currentValue
}
})
//*Location* where i want to use currentVote with it's new value
Location is where I want to use the value of currentVote. When i print the value here, i am returned nil, and thereafter I will be returned the expected value. When I print the value inside the block, I get the expected value. I understand why this is, it's because the block is being executed off the main thread and therefore when i print outside of the block, the print is being executed before the block and so it has a value of nil. I know that to get it onto the main thread you have to use
dispatch_async(dispatch_get_main_queue(), {
code
})
However, I have nested my code in numerous different ways with this dispatch call, but haven't been able to obtain the new value of currentVote. I have searched through stack overflow and users have suggested making a function outside of the block, then calling this inside. However, their function involves
func printValue(value: Int) {
print(value)
}
Which, as you can see would be useless to me as I want to use the value outside of the block, not print it!
***Code altered according to Cod3rite suggestion****
func get(completion: (value: Int) -> Void) {
databaseRef.child("Jokes").child(currentKey).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let currentNumber = snapshot.value! as? [String: AnyObject] {
let currentValue = (currentNumber["votes"] as? Int)!
self.currentVote = currentValue
completion(value: self.currentVote!)
}
})
}
//where I want to use currentVote
I have put it into a completion as suggested, but I still don't know how to obtain the variable!
You almost got it right. But here are a little modifications you need to do:
func get(completion: (value: Int) -> Void) {
databaseRef.child("Jokes").child(currentKey).observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if let currentNumber = snapshot.value! as? [String: AnyObject] {
let currentValue = (currentNumber["votes"] as? Int)!
completion(value: currentValue)
}
})
}
Now when you want to call this get, do this:
get { value in
//Now you have your value retrieved from firebase and you can use it however you want.
print(value)
self.currentVote = value
//your other code
}