Extract Value from Block Swift - 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
}

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

Access array outside of Firebase call

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

Firebase one of two observers not working

I have two observers, the second observer is dependent on the first observers value. I can't seem to get the first observer to work, I am not getting any errors on Xcode. The first function has to check the Users profile for information and then use that information to search for different information in the database. Here is my code:
func loadposts() {
ref = Database.database().reference()
let trace = Performance.startTrace(name: "test trace")
trace?.incrementCounter(named:"retry")
let userID = Auth.auth().currentUser?.uid
print(userID!)
ref.child("Users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let one1 = value?["Coupon Book"] as? String ?? ""
print("one1: \(one1)")
self.bogus.set(one1, forKey: "bogus")
}) { (error) in
print(error.localizedDescription)
}
delay(0.1) {
print("bogus: \(self.bogus.string(forKey: "bogus"))")
Database.database().reference().child("Coupons").child(self.bogus.string(forKey: "bogus")!).observe(.childAdded) { (Snapshot : DataSnapshot) in
if let dict = Snapshot.value as? [String: Any] {
let captiontext = dict["company name"] as! String
let offerx = dict["offer count"] as! String
let logocomp = dict["logo"] as! String
let actchild = dict["childx"] as! String
let post = Post(captiontext: captiontext, PhotUrlString: actchild, offertext: offerx, actualphoto: logocomp)
self.posts.append(post)
self.tableview.reloadData()
print(self.posts)
}
}
}
trace?.stop()
}
Any help is appreciated.
self.bogus.string(forKey: "bogus"))" is nil because observeSingleEvent is an async method, so to get the required results you need to call the second observer inside the first observer or you can use the completion handler
You can use the completionHandler like this:
guard let uid = Auth.auth().currentUser?.uid else {
return
}
func firstObserverMethod(completionCallback: #escaping () -> Void) {
ref.child("Users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
if let value = snapshot.value as? [String: Any] {
let one1 = value["Coupon Book"] as? String
print("one1: \(one1)")
self.bogus.set(one1, forKey: "bogus")
completionCallback()
}
}) { (error) in
print(error.localizedDescription)
}
}
Now using the above method:
firstObserverMethod {
print("bogus: \(self.bogus.string(forKey: "bogus"))")
guard let bogusString = self.bogus.string(forKey: "bogus") else {
print("bogus is not set properly")
return
}
Database.database().reference().child("Coupons").child(bogusString).observe(.childAdded) { (Snapshot : DataSnapshot) in
if let dict = Snapshot.value as? [String: Any] {
let captiontext = dict["company name"] ?? ""
let offerx = dict["offer count"] ?? ""
let logocomp = dict["logo"] ?? ""
let actchild = dict["childx"] ?? ""
let post = Post(captiontext: captiontext, PhotUrlString: actchild, offertext: offerx, actualphoto: logocomp)
self.posts.append(post)
DispatchQueue.main.async {
self.tableview.reloadData()
}
print(self.posts)
}
}
}
Note: You should use optional binding to get the values from optional
Since you are using the result of the 1st observer in the reference of your 2nd observer, it's a very bad idea to add the 2nd observer right below the first observer. And adding a delay won't be a viable solution : these two calls are asynchronous, which means that the reason why you are not getting might very likely be because the 2nd observer is triggered even before the 1st has returned any data.
The solution here, would be using a completion handler, or you could just incorporate your 2nd observer inside the completion block of the 1st, to be make sure that the proper order (1st observer -> 2nd observer) will always be respected.
It would look somehow like this:
func loadposts() {
// ...
// 1st Observer here
ref.child("Users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
// Get your value here
guard let one1 = snapshot.childSnapshot(forPath: "Coupon Book").value as? String else { return }
// 2nd Observer here. Now you can use one1 safely:
Database.database().reference().child("Coupons").child(one1).observe(.childAdded) { (Snapshot : DataSnapshot) in
// ...
}
})
}
Now, a couple of things that you could also improve in your code, while not directly related to the question:
I would suggest you to make use of guard statements instead force-unwrapping, which may end up in crashing your app at some point.
For example, you could check whether your current user exist or not like so:
guard let currentUserID = Auth.auth().currentUser?.uid else {
return
}
// Now you can use safely currentUserID
Also, when you try to get the data out of the snapshot, it's not a good idea either, to use force-casting. You would better write it in this way:
yourRef.observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
guard let text = child.childSnapshot(forPath: "text").value as? String, let somethingElse = child.childSnapshot(forPath: "otherValue").value as? NSNumber else {
return
}
// And so on, depending of course on what you have in your database.
}

Return two arrays from observeSingleEvent

I have questions and answers in Firebase Realtime Database, which I need to be downloaded and put in to arrays. I have created a function which takes a parameter and should return two arrays
let data = fetchQuestions(category: "Animals")
qAni = data.ques
aAni = data.ans
fetchQuestions method:
func fetchQuestions(category: String) -> (ques: [String], ans: [String]) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
return (q, a) ** Unexpected non-void return value in void function
}
}
})
}
I have tried to use dispatch group, but then I receive a different error.
You are currently returning from the completion block of observeSingleEvent and its return type is void, What you need is you need to create completionHandler with your fetchQuestions. Now with Swift instead of using NSArray use Swift native array. Also instead of maintaining two array maintain one array with custom objects of struct or class.
So first create one struct like this.
struct Category {
let question: String
let answer: String
}
Now make completionHandler with your fetchQuestions like this way.
func fetchQuestions(category: String, completion: #escaping([Category]) -> Void) {
var array = [Category]
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! [[String:String]]
for i in QA {
let que = i["question"]!
let ans = i["answer"]!
array.append(Category(question: que, answer: ans))
}
}
}
completion(array)
})
}
Now call fetchQuestions this way.
self.fetchQuestions(category: "Animals") { categories in
//access categories here
print(categories)
}
Note: Instead of comparing value in for loop you can also use queryEqual(toValue:) with your request to get specific result check this SO thread for more details on it.
Its because you are returning a value from a closure instead you should use different closure for that. Try it:
func fetchQuestions(category: StringcompletionBlock:#escaping (_ ques:[String], _ ans :[String]) -> ()) {
var q = [String]()
var a = [String]()
ref.child("QA/Category").observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? [String: Any]
for (k, v) in value! {
if k == category {
let QA = v as! NSArray
for i in QA {
let question = i as! [String: String]
q.append(question["question"]!)
a.append(question["answer"]!)
}
}
return completionBlock(q,a)
}
})
}
#escaping is required : when the closure is passed as an argument to
the function, but is called after the function returns.

access a variable outside a function - swift

I'm trying yo access a variable outside a function, I tried to declare the variable outside the class but it keeps displaying the initial value in the declaration not the value inside the function, here is my code, I need to access databaseScore
func getDatabaseScore()-> Int{
let ref2 = FIRDatabase.database().reference().child("users").child("user").child((user?.uid)!)
ref2.observeSingleEvent(of: .childAdded, with: { (snapshot) in
if var userDict = snapshot.value as? [String:Int] {
//Do not cast print it directly may be score is Int not string
var databaseScore = userDict["score"]
}
})
return databaseScore
}
As mentioned in the comment it's impossible to return something form a method containing an asynchronous task.
You need a completion block for example
func getDatabaseScore(completion: (Int?)->()) {
let ref2 = FIRDatabase.database().reference().child("users").child("user").child((user?.uid)!)
ref2.observeSingleEvent(of: .childAdded, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Int] {
//Do not cast print it directly may be score is Int not string
completion(userDict["score"])
}
completion(nil)
})
}
getDatabaseScore() { score in
guard let score = score else { return }
// do something with unwrapped "score"
}
You're doing an async operation so getDatabaseScore returns before observeSingleEvent completes. You could look at something like this…
class MyClass {
var databaseScore: Int = 0
func getDatabaseScore() {
let ref2 = FIRDatabase.database().reference().child("users").child("user").child((user?.uid)!)
ref2.observeSingleEvent(of: .childAdded, with: { (snapshot) in
if let userDict = snapshot.value as? [String:Int] {
print(userDict["score"]) // Confirm you have the a value
self.databaseScore = userDict["score"]
}
}
}