Dealing with Firebase observers when userID changes? - ios

I use this function inside an helper class to fetch the trips of a user. It keeps listening for new changes:
import Foundation
import Firebase
class APICardService: NSObject {
class func fetchTrips(forID userID: String, completion: #escaping ([Trip]) -> Void) {
let path = "users/\(userID)/trips"
Database.database().reference().child(path).queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
var trips = [Trip]()
for child in snapshot.children {
if let child = child as? DataSnapshot {
if let dict = child.value as? [String: Any] {
let trip = Trip(key: child.key, dbValues: dict)
trips.append(trip)
}
}
}
completion(trips)
}
}
}
class MyViewController: UIViewController {
// but what if the userID changes? I have no way to stop the current observer and start a new one
internal func fetchCards() {
guard let userID = APIAuthService.getUserID() else { return }
APICardService.fetchTrips(forID: userID) { trips in
self.trips = trips.reversed() // show most recent first
self.addAnnotations(for: trips)
self.collectionView.reloadData()
}
}
}
The problem is that whenever the user signs out and logs into a new account this observer will still be listening for new value changes in the data but it won't work because the userID changed. How do I deal with this? I could remove the observer somehow but this is complicated because we're inside a separate class and I wouldn't know how to handle that.

In your class APICardService create these global variables
static var refHandle:DatabaseHandle? = nil
static var ref:DatabaseReference? = nil
in your Method func fetchTrips(forID userID: String, completion: #escapin
assign global variables like this
let path = "users/\(userID)/trips"
ref = Database.database().reference().child(path)
refHandle = ref?.queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
Now you have the reference of your observer
just add following method in your class
class func removeObserverForFetchTrip() {
if let refHandleValue = refHandle {
ref?.removeObserver(withHandle: refHandleValue)
}
}
When user logout just call this method
Hope it is helpful

As #PrashantTukadiya says.
Try making something like this:
struct User {
var id: String
...
}
class UserHolder {
static var shared: UserHolder = UserHolder() // Singleton
var currentUser: User? {
willSet {
// Remove all the registered firebase handlers associated with the user
firebaseHandles.forEach({ $0.0.removeObserver(withHandle: $0.1) })
}
}
var firebaseHandles: [(DatabaseReference, UInt)] = []
// Note: Try to add all the firebase handles you register to "firebaseHandles"
// array. The more adequate solution is to make this class register
// the listeners, not all other classes.
}

Related

iOS private/public data management and storage in Swift

Is it possible to display as one array objects retrieved from network and same model but retrieved from core data. Purpose is to have same data possible to be public (then retrieved from network) or private and then this data is stored locally in coredata model. Attributes/Properties will be the same for both.
I plan to display this as swiftUI view (if that matters)
After some search I came with idea to have one struct that based on its privacy property will be translated to core data class model or if public directly connected to networking layer?
for example (some pseudo swift ;) )
struct Note {
let note: String
let isPrivate: Bool
func save(self) {
if self.isPrivate { save to CoreData }
else { send save request with use of networking }
}
}
class coreDataModel: NSManagedObject {
var note: String
let isPrivate = true
}
struct networkingModel {
var note: String
let isPrivate = false
}
class modelManager {
func joinData() {
let joinedModel: Note = coreDataModel + networkingModel
// and so on to display that model
}
}
I think you can try this, first load the ui with the existing local data. At the same time, make a call to your api on a background queue. Once the api results are available, filter for duplicates, then persist it locally. Then the last step is to notify the ui to reload.
This pseudo-code is a bit UIKit specific, however the same logic can be applied to when in SwiftUI. You won't need closure, you will compute directly to your publisher object, and ui will react based any new emits.
/// you can have one model for both api and core data
struct Note: Hashable { let uuid = UUID.init() }
class ModelManager {
private var _items: Array<Note> = []
var onChanged: ((Array<Note>) -> Void)? = nil
// you might also have an init, where you can have a timer running
func start() {
loadFromCoreData { [weak self] (items) in
guard let `self` = self else { return }
self._items.append(contentsOf: items)
self.onChanged?(self._items)
self.loadFromApi()
}
}
private func loadFromCoreData(completion: #escaping (Array<Note>) -> Void) {
// background queue
// logic to load from coredata.
let coreDataResults: Array<Note> = []
completion(coreDataResults)
}
private func loadFromApi() {
// background queue
let apiResults: Array<Note> = []
compute(contents: apiResults)
}
private func compute(contents: Array<Note>) {
let combined = zip(_items, contents).flatMap{[$0.0, $0.1] }
let newItems = Array(Set(combined)) // set doesn't allow duplicates
// save to db
// insert to new to _items
_items.append(contentsOf: newItems)
// sort maybe to place new items on top
self.onChanged?(self._items)
}
}
// usage of this object
let manager = ModelManager()
manager.start()
manager.onChanged = { items in
// [weak self] remember of retain cycles
// make sure you are on main queue when reloading
}

Not sure how to append array outside of async call

I'm trying to get certain child nodes named City from Firebase using observeSingleEvent but I am having issues trying to pull it into the main thread. I have used a combination of completion handlers and dispatch calls but I am not sure what I am doing wrong, in addition to not being that great in async stuff. In viewDidLoad I'm trying to append my keys from the setupSavedLocations function and return it back to savedLocations I feel like I am close. What am I missing?
Edit: Clarity on question
import UIKit
import Firebase
class SavedLocationsViewController: UIViewController {
var userID: String?
var savedLocations: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
self.savedLocations = savedData
print("inside", self.savedLocations)
})
}
print("outside",savedLocations)
}
func setupSavedLocations(completion: #escaping ([String]) -> ()) {
guard let user = userID else { return }
let databaseRef = Database.database().reference(fromURL: "https://************/City")
var dataTest : [String] = []
databaseRef.observeSingleEvent(of: .value, with: {(snapshot) in
let childString = "Users/" + user + "/City"
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
dataTest.append(key)
}
completion(dataTest)
})
}
sample output
outside []
inside ["New York City", "San Francisco"]
The call to setupSavedLocations is asynchronous and takes longer to run than it does for the cpu to finish viewDidLoad that is why your data is not being shown. You can also notice from your output that outside is called before inside demonstrating that. The proper way to handle this scenario is to show the user that they need to wait for an IO call to be made and then show them the relevant information when you have it like below.
class SavedLocationsViewController: UIViewController {
var myActivityIndicator: UIActivityIndicatorView?
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
showSavedLocations(locations: savedData)
})
}
// We don't have any data here yet from the IO call
// so we show the user an indicator that the call is
// being made and they have to wait
let myActivityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
myActivityIndicator.center = view.center
myActivityIndicator.startAnimating()
self.view.addSubview(myActivityIndicator)
self.myActivityIndicator = myActivityIndicator
}
func showSavedLocations(locations: [String]) {
// This function has now been called and the data is passed in.
// Indicate to the user that the loading has finished by
// removing the activity indicator
myActivityIndicator?.stopAnimating()
myActivityIndicator?.removeFromSuperview()
// Now that we have the data you can do whatever you want with it here
print("Show updated locations: \(locations)")
}

How to connect items to category in one to many relationship when insert data

i have 2 entities one customers and other is cases, the relationship is one to many one customer had many cases and the relation names toCustomers for cases entity and toCases for customers entity so in my code i pass selected customer name by segue to the add cases UIViewController
var CustmerNameForAddNewCaseVar: Customers?
CustomerNameLbl.text = CustmerNameForAddNewCaseVar?.name
in the save button
let newCase : Cases!
newCase = Cases(context:context)
newCase.caseName = caseNameTextField.text
newCase.toCustomers = CustmerNameForAddNewCaseVar?.name // here i got error ... Cannot assign value of type 'String?' to type 'Customers?'
do{
AppDel.saveContext()
}catch{
print(error)
}
any help
thank you
To use your example... you would alter your code to match this:
let newCase : Cases!
newCase = Cases(context:context)
newCase.caseName = caseNameTextField.text
newCase.toCustomers = CustmerNameForAddNewCaseVar
do {
// Save Context
} catch {
// Handle Error
}
Notice the line "newCase.toCustomers" has changed.
What others are suggesting in the comments to your question is to clear up the naming of your entities and variables to help clarify usage. An example would be:
import UIKit
import CoreData
class Customer: NSManagedObject {
#NSManaged var cases: Set<Case>?
}
class Case: NSManagedObject {
#NSManaged var customer: Customer?
#NSManaged var name: String?
}
class ViewController: UIViewController {
var customer: Customer?
var context: NSManagedObjectContext?
#IBOutlet weak var caseName: UITextField?
#IBAction func didTapSave(_ sender: UIButton) {
guard let context = self.context else {
return
}
guard let customer = self.customer else {
return
}
let newCase = Case(context: context)
newCase.name = caseName?.text
newCase.customer = customer
do {
try context.save()
} catch {
// Handle Error
}
}
}

Remove the observer using the handle in Firebase in Swift

I have the following case. The root controller is UITabViewController. There is a ProfileViewController, in it I make an observer that users started to be friends (and then the screen functions change). ProfileViewController can be opened with 4 tabs out of 5, and so the current user can open the screen with the same user in four places. In previous versions, when ProfileViewController opened in one place, I deleted the observer in deinit and did the deletion just by ref.removeAllObservers(), now when the user case is such, I started using handle and delete observer in viewDidDisappear. I would like to demonstrate the code to find out whether it can be improved and whether I'm doing it right in this situation.
I call this function in viewWillAppear
fileprivate func firObserve(_ isObserve: Bool) {
guard let _user = user else { return }
FIRFriendsDatabaseManager.shared.observeSpecificUserFriendshipStart(observer: self, isObserve: isObserve, userID: _user.id, success: { [weak self] (friendModel) in
}) { (error) in
}
}
This is in the FIRFriendsDatabaseManager
fileprivate var observeSpecificUserFriendshipStartDict = [AnyHashable : UInt]()
func observeSpecificUserFriendshipStart(observer: Any, isObserve: Bool, userID: String, success: ((_ friendModel: FriendModel) -> Void)?, fail: ((_ error: Error) -> Void)?) {
let realmManager = RealmManager()
guard let currentUserID = realmManager.getCurrentUser()?.id else { return }
DispatchQueue.global(qos: .background).async {
let specificUserFriendRef = Database.database().reference().child(MainGateways.friends.description).child(currentUserID).child(SubGateways.userFriends.description).queryOrdered(byChild: "friendID").queryEqual(toValue: userID)
if !isObserve {
guard let observerHashable = observer as? AnyHashable else { return }
if let handle = self.observeSpecificUserFriendshipStartDict[observerHashable] {
self.observeSpecificUserFriendshipStartDict[observerHashable] = nil
specificUserFriendRef.removeObserver(withHandle: handle)
debugPrint("removed handle", handle)
}
return
}
var handle: UInt = 0
handle = specificUserFriendRef.observe(.childAdded, with: { (snapshot) in
if snapshot.value is NSNull {
return
}
guard let dict = snapshot.value as? [String : Any] else { return }
guard let friendModel = Mapper<FriendModel>().map(JSON: dict) else { return }
if friendModel.friendID == userID {
success?(friendModel)
}
}, withCancel: { (error) in
fail?(error)
})
guard let observerHashable = observer as? AnyHashable else { return }
self.observeSpecificUserFriendshipStartDict[observerHashable] = handle
}
}
Concerning your implementation of maintaining a reference to each viewController, I would consider moving the logic to an extension of the viewController itself.
And if you'd like to avoid calling ref.removeAllObservers() like you were previously, and assuming that there is just one of these listeners per viewController. I'd make the listener ref a variable on the view controller.
This way everything is contained to just the viewController. It also is potentially a good candidate for creating a protocol if other types of viewControllers will be doing similar types of management of listeners.

Sync data between two viewcontrollers to avoid creating same observer again

In my main VC I look for changes in my FB database like this:
ref.child("posts").observe(.childChanged, with: { (snapshot) in
.......
})
From this VC I can enter VC2 which is set to be "present modally" in my segue.
Now I wonder if I can pass live FB data from VC1 to VC2? I know that I can use a segue.identifier and pass data when I segue to the next VC but this is one time send only. Or should I setup a delegate to fetch data from vc1 to vc2?
So is there any way I can send data from VC1 to VC2 once a node has been updated or must I setup a new .observe() function in VC2?
First I would like to remind you about the singleton design patter :
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.
So the first thing you need to do is to create a call that contains as a parameters the data you get from firebase, I have did the following to get the following in order to get the user data when he logged in into my app and then use these data in every part of my application (I don't have any intention to pass the data between VC this is absolutely the wrong approach )
my user class is like this :
import Foundation
class User {
static let sharedInstance = User()
var uid: String!
var username: String!
var firstname: String!
var lastname: String!
var profilePictureData: Data!
var email: String!
}
after that I have created another class FirebaseUserManager (you can do this in your view controller but it's always an appreciated idea to separate your view your controller and your model in order to make any future update easy for you or for other developer )
So my firebaseUserManager class contains something like this
import Foundation
import Firebase
import FirebaseStorage
protocol FirebaseSignInUserManagerDelegate: class {
func signInSuccessForUser(_ user: FIRUser)
func signInUserFailedWithError(_ description: String)
}
class FirebaseUserManager {
weak var firebaseSignInUserManagerDelegate: FirebaseSignInUserManagerDelegate!
func signInWith(_ mail: String, password: String) {
FIRAuth.auth()?.signIn(withEmail: mail, password: password) { (user, error) in
if let error = error {
self.firebaseSignInUserManagerDelegate.signInUserFailedWithError(error.localizedDescription)
return
}
self.fechProfileInformation(user!)
}
}
func fechProfileInformation(_ user: FIRUser) {
var ref: FIRDatabaseReference!
ref = FIRDatabase.database().reference()
let currentUid = user.uid
ref.child("users").queryOrderedByKey().queryEqual(toValue: currentUid).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
let dict = snapshot.value! as! NSDictionary
let currentUserData = dict[currentUid] as! NSDictionary
let singletonUser = User.sharedInstance
singletonUser.uid = currentUid
singletonUser.email = currentUserData["email"] as! String
singletonUser.firstname = currentUserData["firstname"] as! String
singletonUser.lastname = currentUserData["lastname"] as! String
singletonUser.username = currentUserData["username"] as! String
let storage = FIRStorage.storage()
let storageref = storage.reference(forURL: "gs://versus-a107c.appspot.com")
let imageref = storageref.child("images")
let userid : String = (user.uid)
let spaceref = imageref.child("\(userid).jpg")
spaceref.data(withMaxSize: 1 * 1024 * 1024) { data, error in
if let error = error {
// Uh-oh, an error occurred!
print(error.localizedDescription)
} else {
singletonUser.profilePictureData = data!
print(user)
self.firebaseSignInUserManagerDelegate.signInSuccessForUser(user)
}
}
}
})
}
}
so basically this class contains some protocols that we would implements and two functions that manager the firebase signIn and fechProfileInformation , that will get the user information
than in my login View controller I did the following :
1 implement the protocol
class LoginViewController: UIViewController, FirebaseSignInUserManagerDelegate
2 in the login button I did the following
#IBAction func loginAction(_ sender: UIButton) {
guard let email = emailTextField.text, let password = passwordTextField.text else { return }
let firebaseUserManager = FirebaseUserManager()
firebaseUserManager.firebaseSignInUserManagerDelegate = self
firebaseUserManager.signInWith(email, password: password)
}
3 implement the protocol method :
func signInSuccessForUser(_ user: FIRUser) {
// Do something example navigate to the Main Menu
}
func signInUserFailedWithError(_ description: String) {
// Do something : example alert the user
}
So right now when the user click on the sign in button there is an object created which contains the user data save on firebase database
now comes the funny part (the answer of your question : how to get the user data in every where in the app)
in every part of my app I could make
print(User.sharedInstance.uid) or print(User.sharedInstance. username)
and I get the value that I want to.
PS : In order to use the singleton appropriately you need to make sure that you call an object when it's instantiated.

Resources