I'm trying to get certain child nodes named City from Firebase using observeSingleEvent but I am having issues trying to pull it into the main thread. I have used a combination of completion handlers and dispatch calls but I am not sure what I am doing wrong, in addition to not being that great in async stuff. In viewDidLoad I'm trying to append my keys from the setupSavedLocations function and return it back to savedLocations I feel like I am close. What am I missing?
Edit: Clarity on question
import UIKit
import Firebase
class SavedLocationsViewController: UIViewController {
var userID: String?
var savedLocations: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
self.savedLocations = savedData
print("inside", self.savedLocations)
})
}
print("outside",savedLocations)
}
func setupSavedLocations(completion: #escaping ([String]) -> ()) {
guard let user = userID else { return }
let databaseRef = Database.database().reference(fromURL: "https://************/City")
var dataTest : [String] = []
databaseRef.observeSingleEvent(of: .value, with: {(snapshot) in
let childString = "Users/" + user + "/City"
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
dataTest.append(key)
}
completion(dataTest)
})
}
sample output
outside []
inside ["New York City", "San Francisco"]
The call to setupSavedLocations is asynchronous and takes longer to run than it does for the cpu to finish viewDidLoad that is why your data is not being shown. You can also notice from your output that outside is called before inside demonstrating that. The proper way to handle this scenario is to show the user that they need to wait for an IO call to be made and then show them the relevant information when you have it like below.
class SavedLocationsViewController: UIViewController {
var myActivityIndicator: UIActivityIndicatorView?
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
showSavedLocations(locations: savedData)
})
}
// We don't have any data here yet from the IO call
// so we show the user an indicator that the call is
// being made and they have to wait
let myActivityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
myActivityIndicator.center = view.center
myActivityIndicator.startAnimating()
self.view.addSubview(myActivityIndicator)
self.myActivityIndicator = myActivityIndicator
}
func showSavedLocations(locations: [String]) {
// This function has now been called and the data is passed in.
// Indicate to the user that the loading has finished by
// removing the activity indicator
myActivityIndicator?.stopAnimating()
myActivityIndicator?.removeFromSuperview()
// Now that we have the data you can do whatever you want with it here
print("Show updated locations: \(locations)")
}
Related
Say I get below code, and it works fine.
override func viewDidLoad() {
super.viewDidLoad()
// 1. put loadLevel() in background queue
DispatchQueue.global().async { [weak self] in
self?.loadLevel()
}
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code here
DispatchQueue.main.async { [weak self] in
// 3. push some UI code back to main thread
}
However, when I move the background queue to inside loadLevel() and cover the heavy code and UI code, I get an issue that UI is updated with empty values when launching the app. So what is the different of this two ways?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
Update the 2nd code with the heavy code inside.
I found the issue, it is not related with GCD actually. This issue is in line Bundle.main.url(forResource: "level\(self?.level)", which produces a String interpolation warning. And result resource load get nil I guess.
As I used weak reference [weak self] as capture list here, I need to put self? before the global variable level in case to use it in closure. If I give it a default value like \(self?.level ?? 0), then this issue is fixed.
But is it that the property way to deal with this String interpolation here? Or some better approach should be involved here?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
if let levelFileURL = Bundle.main.url(forResource: "level\(self?.level)", withExtension: "txt") {
if let levelContents = try? String(contentsOf: levelFileURL) {
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()
self?.correctGuess = 0
print("AAA")
for (index, line) in lines.enumerated() {
let parts = line.components(separatedBy: ": ")
let answer = parts[0]
let clue = parts[1]
clueString += "\(index + 1). \(clue)\n"
let solutionWord = answer.replacingOccurrences(of: "|", with: "")
solutionString += "\(solutionWord.count) letters\n"
self?.solutions.append(solutionWord)
let bits = answer.components(separatedBy: "|")
letterBits += bits
print("ABC")
}
}
}
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
You have a reference to:
let resource = Bundle.main.url(forResource: "level\(self?.level)" withExtension: ...)
The warning is
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
The compiler is warning you that you are performing string interpolation of an optional value.
Let's consider a simpler example, to show what happens when you do string interpolation with optionals:
print("\(self?.level)")
If level was xxx, it would print
Optional("xxx")
And obviously if self or level were optional, it would just say:
nil
Clearly, neither of these are quite what you want. So, unwrap the optional. E.g.
guard let level = self?.level else { return }
let resource = Bundle.main.url(forResource: "level\(level)" withExtension: ...)
Let me start off by saying, I have no idea, but I have an idea for you to test. Move DispatchQueue.global().async to the first line of loadLevel().
func loadLevel() {
DispatchQueue.global().async { [weak self] in
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
This isolates the change to just calling loadLevel(). If this works as expected, then keep moving the DispatchQueue.global().async call down until it does break.
func loadLevel() {
var clueString = ""
DispatchQueue.global().async { [weak self] in
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
I use this function inside an helper class to fetch the trips of a user. It keeps listening for new changes:
import Foundation
import Firebase
class APICardService: NSObject {
class func fetchTrips(forID userID: String, completion: #escaping ([Trip]) -> Void) {
let path = "users/\(userID)/trips"
Database.database().reference().child(path).queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
var trips = [Trip]()
for child in snapshot.children {
if let child = child as? DataSnapshot {
if let dict = child.value as? [String: Any] {
let trip = Trip(key: child.key, dbValues: dict)
trips.append(trip)
}
}
}
completion(trips)
}
}
}
class MyViewController: UIViewController {
// but what if the userID changes? I have no way to stop the current observer and start a new one
internal func fetchCards() {
guard let userID = APIAuthService.getUserID() else { return }
APICardService.fetchTrips(forID: userID) { trips in
self.trips = trips.reversed() // show most recent first
self.addAnnotations(for: trips)
self.collectionView.reloadData()
}
}
}
The problem is that whenever the user signs out and logs into a new account this observer will still be listening for new value changes in the data but it won't work because the userID changed. How do I deal with this? I could remove the observer somehow but this is complicated because we're inside a separate class and I wouldn't know how to handle that.
In your class APICardService create these global variables
static var refHandle:DatabaseHandle? = nil
static var ref:DatabaseReference? = nil
in your Method func fetchTrips(forID userID: String, completion: #escapin
assign global variables like this
let path = "users/\(userID)/trips"
ref = Database.database().reference().child(path)
refHandle = ref?.queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
Now you have the reference of your observer
just add following method in your class
class func removeObserverForFetchTrip() {
if let refHandleValue = refHandle {
ref?.removeObserver(withHandle: refHandleValue)
}
}
When user logout just call this method
Hope it is helpful
As #PrashantTukadiya says.
Try making something like this:
struct User {
var id: String
...
}
class UserHolder {
static var shared: UserHolder = UserHolder() // Singleton
var currentUser: User? {
willSet {
// Remove all the registered firebase handlers associated with the user
firebaseHandles.forEach({ $0.0.removeObserver(withHandle: $0.1) })
}
}
var firebaseHandles: [(DatabaseReference, UInt)] = []
// Note: Try to add all the firebase handles you register to "firebaseHandles"
// array. The more adequate solution is to make this class register
// the listeners, not all other classes.
}
Does anyone know how do you automatically update the child values to the tableView every time it changes? I have to terminate the app and when restarting it I'm able to to see the updated values. How can I update these without terminating the app each time?
var ref: DatabaseReference?
var databaseHandle: DatabaseHandle?
var postData = [String]()
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref?.child("Posts").observe(.childAdded, with: { (snapshot) in
let post = (snapshot.value as AnyObject).description
if let actualPost = post {
postData.append(actualPost)
self.tableView.reloadData()
}
})
}
You should change
self.tableView.reloadData()
to
DispatchQueue.main.async {
self.tableView.reloadData()
}
that may work. Because all the UI-related works should happen in main queue
This is the class where the value is
class CurrentWeather{
var _date:String!
var _cityName:String!
var _temp:Double!
var _weatherType:String!
var cityName:String{
if _cityName==nil{
_cityName = ""
}
return _cityName
}
var currentTemprature:Double{
if _temp==nil{
_temp = 0.0
}
return self._temp
}
var weathertype:String{
if _weatherType==nil{
_weatherType = ""
}
return _weatherType
}
var date:String{
if _date==nil{
_date = ""
}
let dateFormater=DateFormatter()
dateFormater.dateStyle = .long
dateFormater.timeStyle = .none
let currentDate = dateFormater.string(from: Date())
self._date="\(currentDate)"
return _date
}
func weatherDataDownload(completed : downloadComplete){
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON{response in
if let dict=response.result.value as? Dictionary<String,AnyObject>{
if let name=dict["name"] as? String{
self._cityName = name.capitalized
print(name.capitalized)
}
if let weather=dict["weather"] as? [Dictionary<String,AnyObject>]{
if let main=weather[0]["main"] as? String{
self._weatherType=main.capitalized
print(main.capitalized)
}
}
if let tempr=dict["main"] as? Dictionary<String,AnyObject>{
if let temp=tempr["temp"] as? Double{
let convertedTemp=Double(round(temp-273.15))
self._temp=convertedTemp
print(convertedTemp)
}
}
}
}
completed()
}}
This is the ViewController class
var currentWeatherOj = CurrentWeather()
override func viewDidLoad() {
super.viewDidLoad()
table.delegate=self
table.dataSource=self
currentWeatherOj.weatherDataDownload {
self.updateUIweather()
}
}
func updateUIweather () {
weatherType.text=currentWeatherOj.weathertype
presentDate.text=currentWeatherOj.date
presentLocation.text=currentWeatherOj.cityName
presentTemp.text="\(currentWeatherOj.currentTemprature)"
}
when I try to call in ViewController its showing the default value which I set inside of computed variable other than _date but I am able print values inside the func of weatherDataDownload.I am confused how variables in swift 3 works because of this.
See the comments in the following code sample. You need to move the call to "completed()"
func weatherDataDownload(#escaping completed : downloadComplete) {
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON { response in
// ... leave your code here alone
// put the call to completed() here
completed()
}
// not here
}
When you make the all to Alamofire, it executes it's request on a background thread. When that request completes, it calls the closure that you've defined (the one that starts "response in..."). You don't want to call updateUIweather until that has been done so you put the call to "completed()" inside of the same completion handler.
When the call to completed was outside of that completion handler, it would be called right away... immediately after the Alamofire request was sent (but before it had finished on that background thread). None of the code in the completion handler has run yet so your variables aren't updated yet.
Finally because your completed closure was passed to a block that was then sent off to a background thread, that closure "escapes" the current function. You add the #escaping so that folks reading your code will know that the closure will live on beyond the life of that function.
In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.