Detach from Firestore listener - ios

I'm using Firestore together with Swift.
I have a singleton data class UserManager. I call this from my different ViewControllers to get data to populate my tableviews. I want the tableviews to automatically update when the collections are updated so I need to use a SnapshotListener. Everything works fine but I'm not sure how to detach from the listener when the Viewcontroller is closed.
In the singleton class I have methods like this below. The method gives a list of users and will be called from several different places around my app.
I also want to give back a reference to the listener so that I can detach from it when the Viewcontroller is closed. But I can't get it working. The below solution gives compiler error.
I've been trying to look at the reference, for example here
https://firebase.google.com/docs/firestore/query-data/listen but I need to get it working when the data is loaded in a singleton class instead of directly in the Viewcontroller. What is the way to go here?
In UserManager:
func allUsers(completion:#escaping ([User], ListenerRegistration?)->Void) {
let listener = db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users, listener)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
UserManager.shared.allUsers(completion: { (users, listener) in
self.users = users
self.listener = listener
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}

I guess the compiler error that you see is referring to the fact that you are using listener into it's own defining context.
Try this for a change:
In UserManager:
func allUsers(completion:#escaping ([User])->Void) -> ListenerRegistration? {
return db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.listener = UserManager.shared.allUsers(completion: { (users) in
self.users = users
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}

I think that getDocument instead of addSnapshotListener is what you are looking for.
Using this method the listener is automatically detached at the end of the request...
It will be something similar to
func allUsers(completion:#escaping ([User])->Void) {
db.collection("users").getDocument { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
} }

Related

How do refresh my UITableView after reading data from FirebaseFirestore with a SnapShotListener?

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()
}
}

How to structure code to deal with asynchronous Firebase snapshot? [duplicate]

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!)"
}

Getting a picture from firebase storage

I'm creating an iOS application which has many pictures on it. I therefore decided to use a database to store the pictures on and retrieve from the database. I have inputted the pictures manually through the Firebase site.
Here is the code I currently have:
import UIKit
import Firebase
class F_A_1: UIViewController {
#IBOutlet weak var imageViewer: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
let database = FIRDatabase.database().reference()
let storage = FIRStorage.storage().reference()
let animalref = FIRStorage.storage().reference().child("animal/bird.png")
animalref.dataWidthMaxSize(1*1000*1000){ (date, error) in
if error = nill {
print(data)
self.imageViewer.image = UIImage(data: data!)
} else {
print(error?.localizedDescription)
}
}
}
This is giving me an error which I cannot fix.
Thanks in advance :)
import UIKit
import Firebase
class F_A_1: UIViewController {
#IBOutlet weak var imageViewer: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
let database = FIRDatabase.database().reference()
let storage = FIRStorage.storage().reference()
let animalref = FIRStorage.storage().reference().child("animal/bird.png")
func nameThisWhatYouWant() {
animalref.data(withMaxSize: 1*1000*1000) { (data, error) in
if error == nil {
print(data)
} else {
print(error?.localizedDescription)
}
}
}
}
i have tried this but it gives a sigbart error
Well I have never worked with Firebase and don't know how to go about connecting it, but you have an issue where you are doing logic in your app.
class F_A_1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
let database = FIRDatabase.database().reference()
let storage = FIRStorage.storage().reference()
let animalref = FIRStorage.storage().reference().child("animal/bird.png")
func nameThisWhatYouWant() {
animalref.dataWidthMaxSize(1*1000*1000){ (date, error) in
if error = nill {
print(data)
self.imageViewer.image = UIImage(data: data!)
} else{
print(error?.localizedDescription)
}
}
}
}
I added a function called nameThisWhatYouWant where you should be handling your data, and sending it off to Firebase. As far as working with Firebase, go here to see how to set up Firebase by working with Cocoa Pods, then allowing you to import Firebase. As far as actually being able to send your data off, they have tons of documentation and examples to follow to read through.
You are comparing the error to nill instead of nil.
Where are you getting your error?

REST API, Swift, Automatic Update

I'm currently struggling to find an easy-to-use programming approach/design pattern, which solves the following problem:
I've got an REST API where the iOS app can request the required data. The data is needed in different ViewControllers. But the problem is, that the data should "always" be up to date. So I need to set up a timer which triggers a request every 5-20 seconds, or sth like that. Everytime the data changes, the view needs to be updated (at the current viewcontroller, which is displayed).
I tried some stuff with delegation and MVC Pattern, but it's kind a messy. How is it done the right way?
In my current implementation I only can update the whole UICollectionView, not some specific cells, because I don't know how the data changed. My controller keeps track of the data from the api and updates only if the hash has changed (if data changed on the server). My models always holds the last fetched data.
It's not the perfect solution, in my opinion..
I also thought about models, that keep themselves up to date, to abstract or virtualise my Rest-API. In this case, my controller doesn't even know, that it isn't directly accessible data.
Maybe someone can help me out with some kind of programming model, designpattern or anything else. I'm happy about anything!
UPDATE: current implementation
The Controller, which handles all the data
import Foundation
import SwiftyJSON
import SwiftyTimer
class OverviewController {
static let sharedInstance = OverviewController()
let interval = 5.seconds
var delegate : OverviewControllerUpdateable?
var model : OverviewModel?
var timer : NSTimer!
func startFetching() -> Void {
self.fetchData()
timer = NSTimer.new(every: interval) {
self.fetchData()
}
timer.start(modes: NSRunLoopCommonModes)
}
func stopFetching() -> Void {
timer.invalidate()
}
func getConnections() -> [Connection]? {
return model?.getConnections()
}
func getConnectionsSlave() -> [Connection]? {
return model?.getConnectionsSlave()
}
func getUser() -> User? {
return model?.getUser()
}
func countConnections() -> Int {
if let count = model?.getConnections().count {
return count
}
return 0
}
func countConnectionsSlave() -> Int {
if let count = model?.getConnectionsSlave().count {
return count
}
return 0
}
func fetchData() {
ApiCaller.doCall(OverviewRoute(), completionHandler: { (data, hash) in
if let actModel = self.model {
if (actModel.getHash() == hash) {
//no update required
return
}
}
var connections : [Connection] = []
var connectionsSlave : [Connection] = []
for (_,connection):(String, JSON) in data["connections"] {
let connectionObj = Connection(json: connection)
if (connectionObj.isMaster == true) {
connections.append(connectionObj)
} else {
connectionsSlave.append(connectionObj)
}
}
let user = User(json: data["user"])
//model needs update
let model = OverviewModel()
model.setUser(user)
model.setConnections(connections)
model.setConnectionsSlave(connectionsSlave)
model.setHash(hash)
self.model = model
//prevent unexpectedly found nil exception
if (self.delegate != nil) {
self.delegate!.reloadView()
}
}, errorHandler: { (errors) in
}) { (progress) in
}
}
}
protocol OverviewControllerUpdateable {
func reloadView()
}
The model, which holds the data:
class OverviewModel {
var user : User!
var connections : [Connection]!
var connectionsSlave : [Connection]!
var connectionRequests : [ConnectionRequest]!
var hash : String!
...
}
And in the ViewController, I use it like this:
class OverviewVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, OverviewControllerUpdateable {
let controller = OverviewController.sharedInstance
override func viewDidLoad() {
super.viewDidLoad()
self.controller.delegate = self
self.controller.startFetching()
}
//INSIDE THE UICOLLECTIONVIEW DELEGATE METHODS
...
if let user : User = controller.getUser() {
cell.intervalTime = interval
cell.nameLabel.text = "Ihr Profil"
}
...
func reloadView() {
self.userCollectionView.reloadData()
}
}
You could use a Singleton object to fetch your data periodically, then post notifications (using NSNotificationCenter) when the data is updated. Each view controller dependent on the data would listen for these notifications, then reload UI based on the updated data.

Firebase removing observers

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() })
}

Resources