Initialize Class from Firebase Data Pull - ios

I'm trying to create a class with a failable initializer that takes a FIRUser and FIRDatabaseReference. It downloads the data from the Firebase database and sets its own variables based on what is returned. Else, the initializer is supposed to fail.
What happens is that the data is downloaded in the closure but then everything just reverts back to its default values as if the download had never happened!
I would really like to contain this server chat logic within the class initializer. Any way I can safely achieve this? I've tried a lot of things so far and can't figure it out.
init?(from user: FIRUser, withUserReference ref: FIRDatabaseReference){
let userID = user.uid
var init_succeeded = false
//These values don't matter. If the init fails
//It'll return an empty class.
//Yes this is a hack lol
self.incognito = false
self.email = "NO"
self.username = "NOPE"
self.ref = ref
self.fir_user = user
self.mute_all = false
//Getting the information from the database
ref.child(userID).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
//Unpacking user preferences
self.incognito = (value?["incognito"] as? Bool)!
self.mute_all = (value?["mute_all"] as? Bool)!
self.email = (value?["email"] as? String)!
self.username = (value?["username"] as? String)!
init_succeeded = true
}) { (error) in
print("ERROR : \(error.localizedDescription)")
}
if !init_succeeded { return nil }
}
Thanks! - Keenan

Simple answer: No
You simply shouldn't return a value from a function when it's dependent on async statements.
This method will always return nil since init_succeeded is most likely set to true after the method returns. Remember, Firebase queries are asynchronous, so once you call observeSingleEvent, the function doesn't wait for that statement to finish executing, it just runs it asynchronously and goes on with the rest of your code (which is the return in this case).
A completion closure is the closest you can get (but your code won't be exactly contained in the initializer that way):
init(from user: FIRUser, withUserReference ref: FIRDatabaseReference, completion: #escaping (Bool) -> Void){
let userID = user.uid
// default values
self.incognito = false
self.email = "NO"
self.username = "NOPE"
self.ref = ref
self.fir_user = user
self.mute_all = false
//Getting the information from the database
ref.child(userID).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
//Unpacking user preferences
self.incognito = (value?["incognito"] as? Bool)!
self.mute_all = (value?["mute_all"] as? Bool)!
self.email = (value?["email"] as? String)!
self.username = (value?["username"] as? String)!
completion(true) // true = success
}) { (error) in
completion(false) // false = failed
print("ERROR : \(error.localizedDescription)")
}
}
And now basically use it like this
let myObject = myClass(from: someUser, withUserReference: someRef, completion: { success in
if success {
// initialization succeeded
}
else {
// initialization failed
}
})
I would suggest though to not retrieve the data in the initializer generally. Perhaps write another function specifically for retrieving the data and only set the default values in init()

Related

Strange behaviour on Firebase query result SWIFT

I'm getting this error on the line let itemToAdd = snapshot.childSnapshot(forPath: "Shopa function that retrieves data from Firebase.
the output of the console in Could not cast value of type 'NSNull' (0x1118c8de0) to 'NSString' (0x10dda45d8)..
What I'm trying to do is to filter database ordering by one value
opening Timeand than get another value Shop Namefrom the returned entries in the snapshot.
here's the function:
func filterOpenShops(enterDoStuff: #escaping (Bool) -> ()) {
ref = Database.database().reference().child("Continent").child("Europe").child("Country").child("Italy").child("Region").child("Emilia-Romagna").child("City").child("Bologna").child("Shops").child("Shops Opening Times")
let query = ref?.queryOrdered(byChild: "Opening Time").queryStarting(atValue: openingTimeQueryStart).queryEnding(atValue: openingTimeQueryEnd)
query?.observe(.value, with: { (snapshot) in
for childSnapshot in snapshot.children {
// new modification
if childSnapshot is DataSnapshot {
let itemToAdd = snapshot.childSnapshot(forPath: "Shop Name").value as! String // gets the open shop from snapshot
self.availableShopsArray.append(itemToAdd)
print(snapshot.children)
print(" Open Shops are \(self.availableShopsArray)")
}
}
// still asynchronous part
enterDoStuff(true)
// call next cascade function filterClosedShops only when data
})
// Sychronous part
print("opening query start is \(openingTimeQueryStart) and opening query end is \(openingTimeQueryEnd)")
} // end of filterOpenShops()
EDIT:
I rewrote the function as:
func filterOpenShops(enterDoStuff: #escaping (Bool) -> ()) {
// get from Firebase snapshot all shops opening times into an array of tuples
//shopOpeningTimeArray:[(storeName: String, weekdayNumber: String, opening1: Sring, closing1: String, opening2:String, closing2: String)]
ref = Database.database().reference().child("Continent").child("Europe").child("Country").child("Italy").child("Region").child("Emilia-Romagna").child("City").child("Bologna").child("Shops").child("Shops Opening Times")
let query = ref?.queryOrdered(byChild: "Opening Time").queryStarting(atValue: String(describing: openingTimeQueryStart)).queryEnding(atValue: String(describing :openingTimeQueryEnd))
query?.observe(.value, with: { (snapshot) in // original is ok
// guard let data = snapshot.value as? [String:String] else { return }
for childSnapshot in snapshot.children {
print("snapshot is: \(childSnapshot)")
print("snapshot.childrend is: \(snapshot.children)")
guard let data = snapshot.value as? [String:String] else { return }
let itemToAdd = data["Shop Name"]
self.availableShopsArray.append(itemToAdd!)
print("Open Shop is: \(String(describing: itemToAdd))")
print(" Open Shops are \(self.availableShopsArray)")
}
// still asynchronous part
enterDoStuff(true)
// call next cascade function filterClosedShops only when data
print(" Open Shops are \(self.availableShopsArray)")
})
print("opening query start is \(openingTimeQueryStart) and opening query end is \(openingTimeQueryEnd)")
} // end of filterOpenShops()
but I still get a null object and not a [String:String] as expected.
The function that created the entries in Firebase is:
func postOpeningTime() {
// if shopNameTextfield.text != nil && openingTimeTextfield.text != nil && closingTimeTextfield.text != nil {
let shopName = shopNameTextfield.text!
let openingTime = openingTimeTextfield.text!
let closingTime = closingTimeTextfield.text!
// } else {return}
let post: [String:String] = [
"Shop Name" : shopName ,
"Opening Time" : openingTime ,
"Closing Time" : closingTime
]
var ref: DatabaseReference!
ref = Database.database().reference()
ref?.child("Continent").child("Europe").child("Country").child("Italy").child("Region").child("Emilia-Romagna").child("City").child("Bologna").child("Shops").child("Shops Opening Times").childByAutoId().setValue(post)
}
Now I have two behaviours:
1st: When querying for entries and finds values that are Int: completion get called but I get no snapshot print.
2nd: When querying for entries and find values that are String: completion doesn't get called but snapshot prints the right entries with values.
Can anyone please spot what's going on here?
I found the problem to bee the way I was casting query result.
Casting it as [String:String] produced to return because upshot was actually [String[String:String]] when all the values for entry's parameter were String, but as I changed Opening Time and Closing time to be Int, than I have to read the snapshot as [String[String:Any]].
So the final function is:
func filterOpenShops(setCompletion: #escaping (Bool) -> ()) {
// Empty the array for beginning of the search
self.availableShopsArray.removeAll()
var ref = Database.database().reference()
ref.child("Continent").child("Europe").child("Country").child("Italy").child("Region").child("Emilia-Romagna").child("City").child("Bologna").child("Shops").child("Shops Opening Times").queryOrdered(byChild: "Opening Time").queryStarting(atValue: openingTimeQueryStart).queryEnding(atValue: openingTimeQueryEnd).observe(.value) { (snapshot) in
print(snapshot)
if let data = snapshot.value as? [String : [String : Any]] {
for (_, value) in
data {
let shopName = value["Shop Name"] as! String
let active = value["Active"] as! String
if active == "true" {
self.availableShopsArray.append(shopName)
print("Shop_Name is :\(shopName)")
print("self.availableShopsArray is: \(self.availableShopsArray)")
}
}
} else {
print("No Shops")
}
// still asynchronous part
setCompletion(true)
// call next cascade function filterClosedShops only when data retrieving is finished
self.filterClosedShops(setCompletion: self.completionSetter)
print(" 1 Open Shops are \(self.availableShopsArray)")
}
} // end of filterOpenShops()

How to know if a Firebase has fetched a value

I have three functions getNewOrder(),storeOrderDetails(_ details:[String:String]) and getUserInfo(_ userID:String).
Function getNewOrder() is called first. It fetches new orders ( .childAdded values) and sends the dictionary to storeOrderDetails(_ details:[String:String]).
storeOrderDetails(_ details:[String:String])then segregate all the values and callsgetUserInfo(_ userID:String)` by passing it userID which was present in its details.
getUserInfo(_ userID:String) then fetches users details and returns user's
information.
However, the problem is [ userInfo = getUserInfo(_ userID:String) in storeOrderDetails(_ details:[String:String]) ] userInfo is always empty. Apparently func getUserInfo(_ userID:String) goes into a completion block after it has returned empty value.
I want these three functions to execute in sequential way.
Any advice is highly appreciated.
Please follow the below Links to review my code.
https://imgur.com/hNjvyDk
https://imgur.com/J0LMXMg
func childAdded(){
let ref = Database.database().reference().child("Orders").child(todaysDate)
ref.observe(.childAdded) { (snapshot) in
var details = [String:String]()
if let orderID = snapshot.key as? String {
ref.child(orderID).observeSingleEvent(of: .value, with: { (snap) in
self.newOrderTextView.text = ""
self.customerNameLabel.text = ""
self.customerPhoneLabel.text = ""
self.orderNumberLabel.text = ""
let enumerator = snap.children
while let rest = enumerator.nextObject() as? DataSnapshot {
details[rest.key as? String ?? ""] = rest.value as? String ?? ""
}
self.storeUserDetails(details)
})
}
}
}
func storeUserDetails(_ details:[String:String]){
if details["CustomerID"] != nil {
userInfo = getUserDetails(details["CustomerID"]!)
print(userInfo)
}
if !userInfo.isEmpty{
let order = OrderDatabase()
order.customerEmail = userInfo["Email"]!
order.customerName = userInfo["Name"]!
order.orderAcceptStatus = details["OrderStatus"]!
order.customerOrderNumber = details["orderNumber"]!
order.orderID = details["orderID"]!
order.time = details["Time"]!
order.customerFirebaseID = details["CustomerID"]!
self.orderDatabase[details["orderNumber"]!] = order
self.orderTable.reloadData()
}
}
func getUserDetails(_ userID:String) -> [String:String]{
var details = [String:String]()
let userDetailsReference = Database.database().reference().child("Users")
userDetailsReference.child(userID).observeSingleEvent(of: DataEventType.value, with: { (snapshot) in
if let dictionary = snapshot.value as? NSDictionary {
self.customerNameLabel.text = dictionary.value(forKey: "Name") as? String
self.customerPhoneLabel.text = dictionary.value(forKey: "Email") as? String
details["Name"] = dictionary.value(forKey: "Name") as? String
details["Email"] = dictionary.value(forKey: "Email") as? String
}
})
return details
}
From what I can see here, I am betting that the issue you are facing has to do with the fact that the methods are asynchronous. So one thing is not completely finished and some other method gets fired too soon. There are a few ways to deal with this issue. One is completion handlers, and the other is adding observers. Below is an example of doing both for Firebase. Here I'm asking a getLocationPhotos method to get all the photos from Firebase. Notice the observers and completion handler
func getLocationPhotos(coordinate:CLLocationCoordinate2D){
dbHandler.getImageFileNames(coordinateIn: coordinate) { (filenames) in
if filenames.isEmpty {
log.debug(String.warningGet + "filenames is empty")
return
}//if filenames.isEmpty
self.imageFiles = filenames.filter { $0 != "none" }
if self.imageFiles.isEmpty {
log.error(String.errorGet + "imageFiles array is empty")
return
}//if imageFiles.isEmpty
for file in self.imageFiles {
let reference = self.storageHandler.imageReference.child(file)
let download = self.imageView.sd_setImage(with: reference)
if let i = self.imageView.image {
self.imageArray.append(i)
self.collectionView.reloadData()
}//let i
download?.observe(.progress, handler: { (snapshot) in
guard let p = snapshot.progress else {
return
}//let p
self.progressView.progress = Float(p.fractionCompleted)
if self.progressView.progress == Float(1) {
self.progressView.isHidden = true
}
})//progress
download?.observe(.success, handler: { (snapshot) in
self.progressView.progress = 1
self.progressView.isHidden = true
self.collectionView.setNeedsLayout()
})//success
download?.observe(.failure, handler: { (snapshot) in
log.error(String.errorGet + "Error occured getting data from snapshot")
})//failure
}//for file
}//dbHandler

Swift 3 Firebase retrieving key and passing to view controller

I've spend hours looking at identical questions but none of the answers I've found are helping this issue. Simple app retrieves data from Firebase Database and passes to another view controller from the tableview. The main data will pass through but I can't edit the information without an identifying "key" which I tried to set as childByAutoID() but then changed to a timestamp. Regardless of the method, all I get is the entries info not the actual key itself.
func loadData() {
self.itemList.removeAll()
let ref = FIRDatabase.database().reference()
ref.child(userID!).child("MyStuff").observeSingleEvent(of: .value, with: { (snapshot) in
if let todoDict = snapshot.value as? [String:AnyObject] {
for (_,todoElement) in todoDict {
let todo = TheItems()
todo.itemName = todoElement["itemName"] as? String
todo.itemExpires = todoElement["itemExpires"] as? String
todo.itemType = todoElement["itemType"] as? String
self.itemList.append(todo)
print (snapshot.key);
}
}
self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
}
}
If your data looks like this:
Uid: {
MyStuff: {
AutoID: {
itemName: “Apocalypse”,
itemExpires: “December 21, 2012”,
itemType: “Catastrophic”
}
}
}
Then I would query like this:
ref.child(userID!).child("MyStuff").observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let child = child as? DataSnapshot
let key = child?.key as? String
if let todoElement = child?.value as? [String: Any] {
let todo = TheItems()
todo.itemName = todoElement["itemName"] as? String
todo.itemExpires = todoElement["itemExpires"] as? String
todo.itemType = todoElement["itemType"] as? String
self.itemList.append(todo)
self.tableView.reloadData()
}
}
})
Additionally, like I said in my comment you can just upload the key with the data if you’re using .updateChildValues(). Example:
let key = ref.child("userID!").childByAutoId().key
let feed = ["key": key,
“itemName”: itemName] as [String: Any]
let post = ["\(key)" : feed]
ref.child("userID").child("MyStuff").updateChildValues(post) // might want a completionBlock
Then you can get the key the same way you are getting the rest of the values. So your new data would look like this:
Uid: {
MyStuff: {
AutoID: {
itemName: “Apocalypse”,
itemExpires: “December 21, 2012”,
itemType: “Catastrophic”,
key: “autoID”
}
}
}
The key you are trying to look for is located in the iterator of your for loop
Inside your if-let, try to do this:
for (key,todoElement) in todoDict {
print(key) // this is your childByAutoId key
}
This should solve the problem. Otherwise show us a screen of your database structure

queryEqualTo is being skipped over

i'm trying to set up a viewCount for my app, when I set the breakpoints up and go through the code it always skips past the queryOrdered and i'm not exactly sure why
func increaseViewCount(username: String, time: NSNumber){
guard let uid = Auth.auth().currentUser?.uid else{
return
}
let refOfUserName = Database.database().reference().child("Users").child(uid)
refOfUserName.observeSingleEvent(of: .value, with: {(snapshot) in
let dictionaryOfUser = snapshot.value as? [String: AnyObject]
// let currentUsersName = dictionaryOfUser?["username"] as? String
let currentUsersName = "hello"
if username == currentUsersName {
print("this is the same user")
}else{
let postRef = Database.database().reference().child("HistoryOfPosts").child("post")
postRef.queryOrdered(byChild: "post").queryEqual(toValue: time).observeSingleEvent(of: .childAdded, with: {(snapshotPost) in
print(snapshotPost.exists())
print(snapshotPost)
let valString = snapshotPost.value
let number = valString as! NSNumber
var value = number.intValue
value = value + 1
let values = ["viewCount": value] as [String:Any]
postRef.updateChildValues(values)
})
}
})
}
The data is loaded from the Firebase Database asynchronously. Instead of waiting for that loading to complete, the program continues with the statement after you attach the observer.
In this case that means that the code pretty much exits increaseViewCount() straight after it attaches the observer. Then once the data comes back from the Firebase servers, the code in your callback block is executed.
To get into the callback block, place a breakpoint on the first statement in that block.

How to ensure that the data is not retrieved and appended as a whole each time a new entry is added?

func generateDataForRecents() {
if URLArrayStringThisSeason.count == 0 {
self.activityIndicator2.isHidden = false
self.activityIndicator2.startAnimating()
}
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("palettes").queryLimited(toFirst: 100).observe(.value, with: { (snapshot) in
if let snapDict = snapshot.value as? [String:AnyObject]{
for each in snapDict as [String:AnyObject]{
let URL = each.value["URL"] as! String
self.URLArrayStringRecents.append(URL)
//print(self.URLArrayString.count)
//print(snapshot)
//let pictureTitle = each.value["title"] as! String
print(self.URLArrayStringRecents.count)
}
}
self.whatsNewCollectionView?.reloadData() //Reloads data after the number and all the URLs are fetched
self.activityIndicator2.stopAnimating()
self.activityIndicator2.isHidden = true
})
}
The following code does a retrieval of data each time the function is called, or when a new data is added.
This is extremely useful when the app is first started up or closed and then restarted. However, when the app is running, whenever a new entry is added, the code seemed to run again and thus appending twice the amount of new data.
For example, when there are already 15 entries identified and then suddenly a new entry is added, the array of the URL would contain 15+16 thus amounting to a total of 31.
How do I make it such that the new data is added to the array instead of adding the entire snapshot in?
You do that by listening for .childAdded events, instead of listening for .value:
var query = databaseRef.child("palettes").queryLimited(toFirst: 100)
query.observe(.childAdded, with: { (snapshot) in
let URL = snapshot.childSnapshot(forPath/: "URL").value as! String
self.URLArrayStringRecents.append(URL)
}
Since you have a limit-query, adding a 101st item means that one item will be removed from the view. So you'll want to handle .childRemoved too:
query.observe(.childRemoved, with: { (snapshot) in
// TODO: remove the item from snapshot.key from the araay
})
I recommend that you spend some time in the relevant documentation on handling child events before continuing.
Please check below method. I have use this method not getting any duplicate entry.
func getallNotes()
{
let firebaseNotesString: String = Firebase_notes.URL
let firebaseNotes = FIRDatabase.database().referenceFromURL(firebaseNotesString).child(email)
firebaseNotes.observeEventType(.Value, withBlock: { snapshot in
if snapshot.childSnapshotForPath("Category").hasChildren()
{
let child = snapshot.children
self.arrNotes = NSMutableArray()
self.arrDictKeys = NSMutableArray()
for itemsz in child
{
let childz = itemsz as! FIRDataSnapshot
let AcqChildKey : String = childz.key
if AcqChildKey == AcqIdGlobal
{
if (childz.hasChildren() == true)
{
let dictChild = childz.value as! NSMutableDictionary
self.arrDictKeys = NSMutableArray(array: dictChild.allKeys)
for i in 0..<self.arrDictKeys.count
{
let _key = self.arrDictKeys.objectAtIndex(i).description()
print(_key)
let dictData : NSMutableDictionary = NSMutableDictionary(dictionary: (dictChild.valueForKey(_key)?.mutableCopy())! as! [NSObject : AnyObject])
dictData.setObject(_key, forKey: "notesId")
self.arrNotes.addObject(dictData)
}
}
}
}
self.tableviewNote.reloadData()
}
})
}
As for the query for removed child,
query.observe(.childRemoved, with: { (snapshot) in
print(snapshot)
let URL = snapshot.childSnapshot(forPath: "URL").value as! String
self.URLArrayStringThisSeason = self.URLArrayStringThisSeason.filter() {$0 != URL}
self.thisSeasonCollectionView.reloadData()
})
it will obtain the URL of the removed child and then update the array accordingly.

Resources