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.
Related
this Main Menu VC will be opened when the app launched for the first time or after the user back to the app (the app become active after enter the background state).
every time this main menu VC is opened, ideally I need to update the time that the date time data comes from the server. in this main menu vc class I call getDateTimeFromServer() after that I updateUI().
but to update the data after the app enter the background and back to the foreground, the getDateTimeFromServer() and updateUI() shall be activated from Appdelegate using function.
func applicationWillEnterForeground(application: UIApplication) {
}
so how do I activate a method that are exist in Main Menu VC from AppDelegate
You don’t need to call the view controller method in app delegate. Observe foreground event in your controller and call your method from there itself.
Observe for the UIApplicationWillEnterForeground notification in your viewController viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.yourMethod), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
Implement this to receive callback when user enters foreground
#objc func yourMethod() {
// Call getDateTimeFromServer()
}
These types of messaging are in most cases done with static context. As it was already mentioned you could alternatively use notification center within the within the view controller to be notified of your application entering foreground. I discourage you creating custom notifications for this though (but is a possible solution as well).
Anyway for your specific case I suggest you have a model that contains your data. Then create a shared instance of it.
class MyDataModel {
static var shared: MyDataModel = {
let model = MyDataModel()
model.reloadData()
return model
}()
var myObjects: [MyObject]?
func reloadData() {
// load data asynchronously
}
}
Now when your view controller needs to reload it simply uses MyDataModel.shared.myObjects as data source.
In app delegate all you do is reload it when app comes back to foreground using MyDataModel.shared.reloadData().
So now a delegate is still missing so we add
protocol MyDataModelDelegate: class {
func myDataModel(_ sender: MyDataModel, updatedObjects objects: [MyObject]?)
}
class MyDataModel {
weak var delegate: MyDataModelDelegate?
static var shared: MyDataModel = {
Now when your view controller appears it needs to assign itself as a delegate MyDataModel.shared.delegate = self. And implement the protocol in which a reload on the view must be made.
A callout to the delegate can simply be done in a model setter:
}()
var myObjects: [MyObject]? {
didSet {
delegate.myDataModel(self, updatedObjects: myObjects)
}
}
func reloadData() {
You can do something like that, using a technique called Key-Value Observation:
class CommonObservableData: NSObject {
// Use #objc and dynamic to ensure enabling Key-Value Observation
#objc dynamic var dateTime: Date?
static let shared = CommonObservableData()
func updateFromWeb() {
// callWebThen is a function you will define that calls your Web API, then
// calls a completion handler you define, passing new value to your handler
callWeb(then: { self.dateTime = $0 })
}
}
Then you observe on it using Swift 4 's new NSKeyValueObservation.
class SomeViewController: UIViewController {
var kvo: NSKeyValueObservation?
func viewDidLoad() {
...
kvo = CommonObservableData.shared.observe(
\CommonObservableData.dateTime, { model, change in
self.label.text = "\(model.dateTime)"
})
}
}
Key-Value Observation is originally an Objective-C technique that is "somewhat revived" by Swift 4, this technique allows you to observe changes on a property (called a Key in Objective-C) of any object.
So, in the previous code snippets, we made a class, and made it a singleton, this singleton has an observable property called dateTime, where we could observe on change of this property, and make any change in this property automatically calls a method where we could update the UI.
Read about KVO here:
Key-Value Observation Apple Programming Guide
Key-Value Observation using Swift 4
Also, if you like Rx and RFP (Reactive Functional Programming), you can use RxSwift and do the observation in a cleaner way using it.
In swift 4 and 5, the notification name is changed the below code working for both.
notifyCenter.addObserver(self, selector: #selector(new), name:UIApplication.willEnterForegroundNotification, object: nil)
#objc func new(){}
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.
I feel like I'm missing something and this should not be too hard.
I'm reading in some data in the initial scene in my app.
I've got a singleton and I make the call in viewDidLoad to singleton.getData().
This initial scene is part of a tab controller. And while I thought viewDidLoad would only get called once for each scene I'm pretty sure it's being called a few times during the lifecycle of my app.
So just wondering if there is a way to ensure a function call to retrieve some data only happens once.
viewDidLoad will be called when selected tab is changed, you can change the place you call getData.
If you want to call getData in viewDidLoad and be sure it won't be called multiple times you can create a flag and check, if it is previously called or not.
class Singleton {
static let sharedInstance = Singleton()
private static var getDataCalled = false
func getData() {
if Singleton.getDataCalled {
return
}
Singleton.getDataCalled = true
// request data
print("data requested")
}
}
Singleton.sharedInstance.getData()
Singleton.sharedInstance.getData()
Calling getData multiple times print data requested only once.
I'm trying to load data from firebase into an array of Company objects, but it's not working. By using print statements, I noticed that viewDidLoad method and the observe method are not being called. Nothing is printing to the console, except for a bunch of firebase text (i.e. "Firebase automatic screen reporting is enabled...").
import UIKit
import FirebaseDatabase
class Information: UIViewController {
var ref:FIRDatabaseReference?
var databaseHandle:FIRDatabaseHandle?
var companiesInformation = [Company]() //holds name, booth, image
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad was called")
//Set firebase database reference
ref = FIRDatabase.database().reference()
// Retrieve posts and listen for changes
databaseHandle = ref?.child("companies").observe(.childAdded, with: { (snapshot) in
// Code that executes when child is added
let company: Company = Company()
company.name = snapshot.value(forKeyPath: "name") as! String
self.companiesInformation.append(company)
print("databaseHandle was called")
})
}
}
After downloading Firebase and Firebase real time database using cocoa pods, I added the 2 necessary pieces of Firebase code in my App Delegate and my app runs without errors. I only have 2 yellow warnings as shown below:
So why isn't the database working and why is nothing printing to the console?
It was a silly mistake. I realized that I was not calling the Information View Controller when I built my app, so the viewDidLoad method was not being called, so nothing was printing.
Currently, I am using firebase. I populate my table using firebase async call observeEventType. However now I am also using BTNavigationDropdownMenu and I would need to populate my title from the database. As firebase query is async, how can I make it such that only when it is completed then BTNavigationDropdownMenu will be fired?
This is the call I make at viewDidAppear to retrieve data from firebase
_ = dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
self.titleList.insert("\(snapshotOne.key)", atIndex: 0)
})
This is supposed to fire after data is retrieved
let menuView = BTNavigationDropdownMenu(navigationController: self.navigationController, title: titleList.first!, items: titleList)
self.navigationItem.titleView = menuView
The error that I am getting is unwrapping a nil in titleList, which of course is the case since titleList has not been populated by firebase.
I tried 2 things which failed. Firstly, I placed the BTNavigationDropdownMenu codes in my firebase call. But that gave me a memory warning as the codes kept being called.
Secondly I tried using dispatch_async and serial queue but it didnt work since I was queueing a async code within an async code, which means my queue fired the firebase async codes and moved to my BTNavigationDropdownMenu before firebase responded.
Simply set the titleView inside the async completion closure of value event
dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
self.titleList.insert("\(snapshotOne.key)", atIndex: 0)
let menuView = BTNavigationDropdownMenu(navigationController: self.navigationController, title: titleList.first!, items: titleList)
self.navigationItem.titleView = menuView
})
I think your best bet is to subclass UINavigationController to solve your memory problems. The menuView is getting initialized within UIViewController but then added as a subview of navigationController. So when the ViewController should dealloc, The menuView hasn't dealloc'd because its still on the navigationController, while still having a reference to UIViewController so your UIViewController cannot dealloc either. Its not on the navigationStack, but still exists in memory creating your issue. Here is a quick subclass.
class MyNavController : UINavigationController {
var menuView : BTNavigationDropdownMenu = BTNavigationDropdownMenu()
override func viewDidLoad() {
super.viewDidLoad()
//Do additional setup like adding to superView or whatever else you need.
titleView = menuView
}
}
In firebase completion block
_ = dataRef.observeSingleEventOfType(.Value, withBlock: { (snapshotOne) in
//Individually set all of the values of your menuView here.
if let navVC = self.navigationController as? MyNavController {
navVC.menuView.title = titleList.first
}
})
The key here is that menuView now only has a reference to the navBar so will deallocate itself with the navBar and your VC can now dealloc on its since menuView no longer has a reference to it. So while creating weak reference could solve your memory issues, It makes more sense for the life-cycle of menuView to follow the life cycle of your navController if it lives on the navController