I have a problem removing a Firebase observer in my code. Here's a breakdown of the structure:
var ref = Firebase(url:"https://MY-APP.firebaseio.com/")
var handle = UInt?
override func viewDidLoad() {
handle = ref.observeEventType(.ChildChanged, withBlock: {
snapshot in
//Do something with the data
}
}
override func viewWillDisappear(animated: Bool) {
if handle != nil {
println("Removed the handle")
ref.removeObserverWithHandle(handle!)
}
}
Now when I leave the viewcontroller, I see that "Removed the handle" is printed, but when I return to the viewcontroller, my observer is called twice for each event. When I leave and return again, it's called three times. Etc. Why is the observer not being removed?
I do also call ref.setValue("some value") later in the code, could this have anything to do with it?
Thought I was having this bug but in reality I was trying to remove observers on the wrong reference.
ORIGINAL CODE:
let ref: FIRDatabaseReference = FIRDatabase.database().reference()
var childAddedHandles: [String:FIRDatabaseHandle] = [:]
func observeFeedbackForUser(userId: String) {
if childAddedHandles[userId] == nil { // Check if observer already exists
// NOTE: - Error is caused because I add .child(userId) to my reference and
// do not when I call to remove the observer.
childAddedHandles[userId] = ref.child(userId).observeEventType(.ChildAdded) {
[weak self] (snapshot: FIRDataSnapshot) in
if let post = snapshot.value as? [String:AnyObject],
let likes = post["likes"] as? Int where likes > 0 {
self?.receivedFeedback(snapshot.key, forUserId: userId)
}
}
}
}
func stopObservingUser(userId: String) {
// THIS DOES NOT WORK
guard let cah = childAddedHandles.removeValueForKey(userId) else {
print("Not observing user")
return
}
// Error! I did not add .child(userId) to my reference
ref.removeObserverWithHandle(cah)
}
FIXED CODE:
func stopObservingUser(userId: String) {
// THIS WORKS
guard let cah = childAddedHandles.removeValueForKey(userId) else {
print("Not observing user")
return
}
// Add .child(userId) here
ref.child(userId).removeObserverWithHandle(cah)
}
Given it's April 2015 and the bug is still around I'd propose a workaround for the issue:
keep a reference of the handles (let's say in a dictionary and before initiating a new observer for the same event type check if the observer is already there.
Having the handles around has very low footprint (based on some official comments :) ) so it will not hurt that much.
Observers must be removed on the same reference path they were put upon. And for the same number of times they were issued, or use ref.removeAllObservers() for each path.
Here's a trick I use, to keep it tidy:
var fbObserverRefs = [FIRDatabaseReference]() // keep track of where observers defined.
...then, put observers in viewDidLoad():
fbObserverRefs.append(ref.child("user/\(uid)"))
fbObserverRefs.last!.observe(.value, with: { snap in
// do the work...
})
...then, in viewWillDisappear(), take care of removing any issued observers:
// Only true when popped from the Nav Controller stack, ignoring pushes of
// controllers on top.
if isBeingDismissed || isMovingFromParentViewController {
fbObserverRefs.forEach({ $0.removeAllObservers() })
}
Related
UPDATE at the bottom.
I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.
But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.
This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.
It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.
Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.
But I can't figure out how to get the loadData() to make the table reload later.
ReminderRepository.swift
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
init() {
loadData()
}
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
}
The only difference from the Apple code is that in the ListDataSource.swift file, I added:
var remindersRepository: ReminderRepository
override init() {
remindersRepository = ReminderRepository()
}
and all reminders references in that file have been changed to
remindersRepository.reminders.
Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.
UPDATE: Not a full credit solution, but getting closer.
I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)
#objc
private func refreshTournaments(_ sender: Any) {
tableView.reloadData()
refreshControl?.endRefreshing()
}
Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?
Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:
creating delegate protocol.
protocol ReminderRepositoryDelegate: AnyObject {
func reloadYourData()
}
Conform ReminderListDataSource to delegate protocol:
class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
func reloadYourData() {
self.tableView.reloadData()
}
}
Add delegate weak variable to ReminderRepository that will weakly hold your controller.
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
weak var delegate: ReminderRepositoryDelegate?
init() {
loadData()
}
}
set ReminderListDataSource as a delegate when creating ReminderRepository
override init() {
remindersRepository = ReminderRepository()
remindersRepository.delegate = self
}
load data after reminder is set
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
delegate?.reloadYourData()
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
Please try changing var reminders = [Reminder]() to
var reminders : [Reminder] = []{
didSet {
self.tableview.reloadData()
}
}
I add an observer in viewWillAppear and remove it in viewWillDisappear. I switch tabs than manually add something to the posts ref and the observer still runs (I have a break point inside the block).
Where am I going wrong at when removing the observer?
var postRefHandle: DatabaseHandle!
var query = DatabaseQuery()
var postsRefObserver = Database.database().reference().child("posts")
// called in viewWillAppear
func listen() {
self.query = postsRefObserver
.queryOrderedByKey()
.queryLimited(toLast: 1)
self.postRefHandle = self.query.observe(.childAdded) { (snapshot) in
// do something
}
}
// called in viewWillDisappear
func remove() {
if let postRefHandle = postRefHandle {
self.postsRefObserver.removeObserver(withHandle: postRefHandle)
}
self.postsRefObserver.removeAllObservers()
}
You need to remove the observer on the exact query object that you registered it on.
So:
self.query.removeObserver(withHandle: postRefHandle)
or
self.query.removeAllObservers()
For some reason my segue is not being performed. Here is my storyboard setup.
This code is being called in viewDidAppear of my root view controller. In the XCode debugger it says that the fetch request returned 0 results before failing on the last line due to unexpectedly finding nil. If it returned 0 results, why wouldn't my segue be performed?
var fetchResult: [User] = []
do {
// Look for User entities in Core Data
fetchResult = try context.fetch(User.fetchRequest()) as! [User]
} catch {
fatalError("Fetch error")
}
if fetchResult.count == 0 {
// New user, calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
}
if fetchResult.count > 1 {
// ERROR: too many users
fatalError("fetch count > 1")
}
let user = fetchResult.first! as User
The call to performSegue(withIdentifier:,sender:) doesn't block the control flow. It's an asynchronous call that will make sure that UIKit eventually presents the new view controller. The code in your method will continue to execute, however.
As a result, it will encounter the last line let user = fetchResult.first! as User immediately afterwards. This will crash as fetchResult.first results is an optional and you force-unwrap it even though it is nil (the fetchResult is empty, after all). And this crash will happen even before UIKit has even started to present the new view controller.
As a general rule of thumb you should always use optional binding instead of force-unwrapping optionals.
There's excellent reading about this topic in this answer: https://stackoverflow.com/a/32170457/10165733. I recommend you have a look at it.
here is your updated RootViewController code:
//Changed back to this method
override func viewDidAppear(_ animated: Bool) {
setup()
}
func setup() {
var fetchResult: [User] = []
do {
// Look for User entities in Core Data
fetchResult = try context.fetch(User.fetchRequest()) as! [User]
} catch {
fatalError("Fetch error")
}
//Modified this with single if conditions
if fetchResult.count == 0 {
// New user, calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
} else if fetchResult.count > 1 {
// ERROR: Too many users
fatalError("fetch count > 1")
} else {
//if fetchResult.count == 1
if let user = fetchResult.first { // New Edit Here
if !user.didFinishSetup {
// Didn't finish setup, re-calculate TDEE
performSegue(withIdentifier: "toTDEE", sender: self)
}
}
}
}
And HERE is your updated project for more info.
I honestly I have tried to figure out when to call ref.removeAllObservers or ref.removeObservers, but I'm confused. I feed I'm doing something wrong here.
var noMoreDuplicators = [String]()
func pull () {
if let myIdi = FIRAuth.auth()?.currentUser?.uid {
let ref = FIRDatabase.database().reference()
ref.child("users").queryOrderedByKey().observe(.value, with: { snapshot in
if let userers = snapshot.value as? [String : AnyObject] {
for (_, velt) in userers {
let newUser = usera()
if let thierId = velt["uid"] as? String {
if thierId != myIdi {
if let userName = velt["Username"] as? String, let name = velt["Full Name"] as? String, let userIdent = velt["uid"] as? String {
newUser.name = name
newUser.username = userName
newUser.uid = userIdent
if self.noMoreDuplicators.contains(userIdent) {
print("user already added")
} else {
self.users.append(newUser)
self.noMoreDuplicators.append(userIdent)
}
}
}
}
}
self.tableViewSearchUser.reloadData()
}
})
ref.removeAllObservers()
}
}
Am I only supposed to call removeAllObservers when observing a single event, or...? And when should I call it, if call it at all?
From official documentation for observe(_:with:) :
This method is used to listen for data changes at a particular location. This is
the primary way to read data from the Firebase Database. Your block
will be triggered for the initial data and again whenever the data
changes.
Now since this method will be triggered everytime the data changes, so it depends on your usecase , if you want to observe the changes in the database as well, if not then again from the official documentation:
Use removeObserver(withHandle:) to stop receiving updates.
Now if you only want to observe the database once use observeSingleEvent(of:with:) , again from official documentation:
This is equivalent to observe:with:, except the block is
immediately canceled after the initial data is returned
Means that you wont need to call removeObserver(withHandle:) for this as it will be immediately canceled after the initial data is returned.
Now lastly , if you want to remove all observers , you can use this removeAllObserver but note that:
This method removes all observers at the current reference, but does
not remove any observers at child references. removeAllObservers must
be called again for each child reference where a listener was
established to remove the observers
Actually, you don't need to call removeAllObservers when you're observing a single event, because this observer get only called once and then immediately removed.
If you're using observe(.value) or observe(.childAdded), and others though, you would definitely need to remove all your observers before leaving the view to preserve your battery life and memory usage.
You would do that inside the viewDidDisappear or viewWillDisappear method, like so:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Remove your observers here:
yourRef.removeAllObservers()
}
Note: you could also use removeObserver(withHandle:) method.
This question already has answers here:
Returning method object from inside block
(3 answers)
Closed 5 years ago.
I have an problem that I can not solve. A lot of questions are in JS and I don't really understand them.
I'm using Firebase as my database for my IOS app and Swift. Here is my situation:
I have a class file that contains functions that can retrieve values in my database. I'm calling these functions in some viewControllers.
The values retrieved by these functions are immediately used in these viewControllers.
My problem is that my app crash because of nil values returned by the class file functions. This happen because of the asynchronous Firebase snapshot.
The variables assumed to contain the values are used before their value is assigned => My app crash, or printed values are nil.
Then my question is simple: How can I structure my code to avoid this issue? I already tried completions, but that's not working for me: functions are still asynchronous.
Here is one of my function in the class file:
func initAverageMark(completionHandler: #escaping (_ mark: Double) -> ()) {
let userRef = ref.child("users").child((user?.uid)!).child("mark")
userRef.observeSingleEvent(of: .value, with: { (snapshot) -> Void in
if let mark: Double = snapshot.value as? Double {
completionHandler(mark)
}
}) { (error) in
print(error.localizedDescription)
}
}
One of my viewController code:
private var totalAsks: Double!
override func viewDidLoad() {
super.viewDidLoad()
initInfos()
}
func initInfos() {
mainUser().initTotalAsks{ total in
self.totalAsks = total
}
initLabels()
}
func initLabels() {
totalAsksLabel.text = " \(totalAsks!)" // it crashs here
}
Assuming you'd want to set some label or something in your viewController to the value of mark you'd do it like this.
mainUser().initTotalAsks { mark in
self.totalAsksLabel.text = " \(mark)"
}
Edit
Or if you absolutely want to use that Double.
private var totalAsks: Double? = nil {
didSet {
initLabels()
}
}
override func viewDidLoad() {
super.viewDidLoad()
initInfos()
}
func initInfos() {
mainUser().initTotalAsks{ total in
self.totalAsks = total
}
}
func initLabels() {
guard totalAsks != nil else {
return
}
totalAsksLabel.text = " \(totalAsks!)"
}