I have a "users" node in my Firebase database. Each user in it has a root with a uid for it. One of the properties of each user is "coordinates". I want to observe any change in any of those coordinates for all users. I almost need something like:
usersDatabaseReference.child("*").child("coordinates").observe(.value, with: { (snapshot) in
My structure looks like this:
users
abcdefghijklmnop
coordinates: "12.345,12.345"
other props
qrstuvwxyz
coordinates: "34.567,34.567"
other props
So I'm looking for a global notification for any user whose coordinates value changes.
Do I need to loop through all children in my userDatabaseReference and set observers for each? And then any time a user is added or removed, set it all up again?
You can see why I'd like to use a wildcard. How should I approach this?
You could just observe the users structure as a whole, then use the snapshot to determine if the coordinate is the element that has changed
usersDatabaseReference.child("users").observe(.childChanged, with: { (snapshot) in
//Determine if coordinate has changed
})
What you could also do is :
func observeChildAdded(){
usersDatabaseReference.child("users").observe(.childAdded, with: { (snapshot) in
//Get the id of the new user added
let id = "123"
self.usersDatabaseReference.child("users").child(id).observe(.childChanged, with: { (snapshot) in
self.foundSnapshot(snapshot)
})
})
}
func foundSnapshot(_ snapshot: DataSnapshot){
let idChanged = snapshot.key
//Process new coordinates
}
So that whenever you add a new child it automatically sets up an observer for it, but they all get pushed to the same function foundSnapshot
Related
In all iOS classes that use Firebase you will have code like this,
private func clearObservations() {
// your method for clearing observations, probably something like
blah blah. removeAllObservers()
}
In view controllers, it's essential that you call this in viewWillDisappear (or viewDidDisappear)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
clearObservations()
}
That's fine.
Assume that you have created an observation in a UITableViewCell.
What is the best place in a cell to "clear observations" ?
Note that prepareForReuse is useless, try it.
The only approach we've found is
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
clearObservations()
}
super.willMove(toSuperview: newSuperview)
}
Seems flakey/bizarre though.
What's the deal on this?
Update
Note while "XY Answers" are interesting and informative, if anyone knows the answer to the question that would be great also!
Preface
This was an attempt to answer the question but the question was misunderstood. I'll leave it here as it does have some relevance regarding observers, handles and tableView cell interaction.
While you can go through those gyrations, it's not really needed in most use cases.
For example, if you add and observer to a node, there wouldn't necessarily be a someRef? variable hanging around. So here we are watching the Posts node for new posts
let postsRef = self.ref.child("Posts")
postsRef.observe(.childAdded, with: { snapshot in
print(snapshot) //add the post to the dataSource and reloadTableview/cell
})
Here's another example of watching for any posts that are changed by uid_2
let postsRef = self.ref.child("Posts")
let queryRef = postsRef.queryOrdered(byChild: "poster_id").queryEqual(toValue: "uid_2")
queryRef.observe(.childChanged) { (snapshot) in
print(snapshot) //change the post in the dataSource and reloadTableview/cell
}
No class vars are needed for this functionality and nothing needs be nil'd. The point here being that you do not have to have class vars to get observing functionality and you do not need to keep a handle for every observer (keep reading)
In view controllers, it's essential that you call this
(someRef?.removeAllObservers()) in viewWillDisappear (or Did)..
will use Firebase in the cells of tables.
To clarify; I wouldn't want to put Firebase observers in the cells of tables. The observers should be in whichever viewController controls the tableView that has cells. Cells should pull data from the dataSource array (which is backed by Firebase)
There are some circumstances where you may want to remove all observers, again no need to have a class var or a need to nil a var.
let postsRef = self.ref.child("Posts")
postsRef.removeAllObservers()
There are times when a specific observer needs to be removed (in the case where a node has observers on it's child nodes for example), and in those cases, we store a handle to that observer as say, a class var (keeping them in an array is a tidy way to do it)
class ViewController: UIViewController {
var myPostHandle : DatabaseHandle?
func addObserver() {
let postsRef = self.ref.child("Posts")
self.myPostHandle = postsRef.observe(.childAdded, with: { snapshot in
print(snapshot)
})
func stopObserving() {
if self.myPostHandle != nil {
let postsRef = self.ref.child("Posts")
postsRef.removeObserver(withHandle: self.myPostHandle) //remove only the .childAdded observer
}
}
}
Again though, once the observer is removed, the handle would go out of scope once the class closes.
Tableviews that contain cells are backed by a dataSource and that dataSource get's it's data from firebase. When something is added, changed or removed from Firebase, your app is notified and the array is updated and then the cell refreshed. No need for an observer in the cell itself.
There's no need to add dozens of observers (in the cells) - add one central observer and let it keep the array current. Refresh tableView only when something changes.
EDIT
To Address a comment regarding the use of removeAllObservers: code is worth 1000 words:
Create a new Firebase project with two button actions. Here's the code for button0 which adds an observer to a node:
func button0() {
let testRef = self.ref.child("test_node")
testRef.observe( .value) { snapshot in
print(snapshot)
}
}
when this button0 is clicked, from there on, any adds, changes, or deletes to the test node will print it's contents to the log.
func button1() {
let testRef = self.ref.child("test_node")
testRef.removeAllObservers()
}
This will remove all observers for the node specified. Once clicked, no events will print to the console.
Try it!
It is not right to clear observations in cell and therefore there is not a best place to do it in cell, because, firstly, this approach contradicts MVC pattern. Views only responsible for displaying content and they should only contain code that describes how they must be draw. And in the view controller you give the content for showing by views. Usually content has provided by your model. So controller connects views and model. In your case, when you place clearObservations() in cell class, you also have someRef as a class property, so you have a model in your view class and this is incorrect.
Secondly, if you try to clear observations in table cell you definitely make logic of showing some content in table in wrong way. Cell only show data that has to be generated by some object that conforms to UITableViewDataSource protocol and implements protocol methods. For instance, in cellForRow method you generate cell and setup it with some content from array. This array is generated from model (Firebase service). Your view controller may be this data source object. You have to include array property to controller class and someRef, than you fill array and reload table data. If controller's view disappeared you clear observations, but you do it only inside view controller (in viewWillDisappear()).
Overall, all manipulations with someRef you should do in view controller and therefore "clear observations" also inside controller.
Need help on how to delete a specific child within a child. I am able to access the correct child through a query call, but I am not sure how to remove just that specific child because right now I am removing everything relating to that user. The picture beneath is my firebase son structure. For "posts" I want to delete "-KzV-uwXL9lbP5wbsQ7W". Maybe when I am creating the "posts" I need to add this "id" into the structure for the query call? Not sure, just thinking... thanks.
Database.database().reference().child("posts").child(uid!).queryOrdered(byChild: "download_url").queryEqual(toValue: pictureImage).observe(.value, with: { snapshot in
if snapshot.exists() {
snapshot.ref.removeValue()
print("snapshot exists")
} else {
print("snapshot doesn't exist")
}
})
In my app I fetch live data like this:
//Firebase
var ref: FIRDatabaseReference?
var handle: FIRDatabaseHandle?
override func viewDidLoad() {
ref = FIRDatabase.database().reference()
handle = ref?.child("posts").child(String(itemId)).observe(.childChanged, with: { (snapShot) in
if let item = snapShot.value as? String {
print(item)
}
})
.....
Now reading the firebase docs I see this:
Observers don't automatically stop syncing data when you leave a ViewController. If an observer isn't properly removed, it continues to sync data to local memory.
So I added this function that gets fired when I exit the VC:
#IBAction func backButtonDidTouch(_ sender: AnyObject) {
if let handle = handle {
ref?.removeObserver(withHandle: handle)
}
showNavBar = true
_ = navigationController?.popViewController(animated: true)
}
But I can also call removeAllObservers() insetad of removeObserver() and the docs also says:
Calling removeObserverWithHandle or removeAllObservers on a listener does not automatically remove listeners registered on its child nodes; you must also keep track of those references or handles to remove them.
So looking at my code am I doing it right? I dont want to keep data syncing between my app and firebase when I exit my VC
You seem to be calling an observer on the specific post, but you are removing the observer from the parent reference. As the documentation states, removing a listener from a reference does not clear the observers from the children, hence I believe you have not removed the observer as you intended.
I have run into this issue myself. Particularly when I log a user out, for a brief moment, the presenting view controller is trying to read from firebase and crashes. What I have done is define a set of type DatabaseReference among the singleton I am using. And where I call
ref.observe(.value) {(snapshot) in
singleton.refsUsed.insert(snapshot.ref)
...
}
Then upon logging out, before I dismiss the current view controller, I am iterating over all the items in the reference set and removing all observers.
I am trying to make a search bar, where you can search for users names. The cell is showing two things - and image and a label.
The label is showing the users name and the image is showing the profile picture. The image needs the user userID to display his/her picture.
The countryAndCode contains the users userID called country, and the users name called code.
My problem is that it is adding the first user twice and I do not know why or how to change it.
Here is the relevant code, let me know if you would like to see more:
func startObersvingDB() {
FIRDatabase.database().reference().child("UserInformation").observeEventType(.ChildAdded, withBlock: { snapshot in
let title = snapshot.value!["usersUID"] as? String
let name = snapshot.value!["userName"] as? String
self.tableData.append(title!)
self.tableDataNames.append(name!)
for i in 0..<self.tableData.count {
print(self.tableDataNames.count)
print(self.tableData.count)
self.countryAndCode.append((self.tableData[i], code: self.tableDataNames[i]))//this shows error in XCode some times
//if the above code doesn't compile then use the below code
print("DONE")
print(self.countryAndCode)
}
self.tableView.reloadData()
})
}
I'm able to successfully update my entries in Firebase, for it shows up on the console, but not the table view. If I restart my app the changes will then show, but I want them to show immediately.
My approach is to edit the array that populates the tableview as soon possible under the "ChildChanged" notification.
ref.observeEventType(.ChildChanged, withBlock: { (snapshot) in
print("One of the entries were changed so we're reloading the table view")
if let firstname = snapshot.value?.objectForKey("firstname"), lastname = snapshot.value?.objectForKey("lastname")
{
let fullname = "\(firstname) \(lastname)"
names[I_Need_This_Index] = fullname
dispatch_async(dispatch_get_main_queue())
{
self.tableView.reloadData()
}
}
}, withCancelBlock: nil)
As you can see, I just need to locate index of what I need, for I already have the "fullname" value which is the edited value that I wish to update the table view with.
How would I go about doing that?
You need to lookup the index using the key of the changed child. Therefore a dictionary could be set up initially to map the keys for all children to the index of your tableview cell when receiving all values.
Edit:
If you need the old value itself you have to obtain the initial change snapshot yourself.