Memory leak using Firebase - ios

I execute an API call in Firebase for retrieving the user profile information and storing it in a ViewController member variable.
The API is declared as a static function inside a class MyApi:
// Get User Profile
static func getUserProfile(byID userId:String,response:#escaping (_ result:[User]?,_ error:Error?)->()) {
// check ID is valid
guard userId.length > 0 else {
print("Error retrieving Creator data: invalid user id provided")
response(nil,ApiErrors.invalidParameters)
return
}
// retrieve profile
let profilesNode = Database.database().reference().child(MyAPI.profilesNodeKey)
profilesNode.child(userId).observe(.value, with: { (snapshot) in
// check if a valid data structure is returned
guard var dictionary = snapshot.value as? [String:AnyObject] else {
print("Get User Profile API: cannot find request")
response([],nil)
return
}
// data mapping
dictionary["key"] = userId as AnyObject
guard let user = User(data:dictionary) else {
print("Get User Profile API: error mapping User profile data")
response(nil,ApiErrors.mappingError)
return
}
response([user], nil)
}) { (error) in
response(nil,ApiErrors.FirebaseError(description: error.localizedDescription))
}
}
and I call it like that:
MyAPI.getUserProfile(byID: creatorId) { (profiles, error) in
guard let profiles = profiles, profiles.count > 0 else {
Utility.showErrorBanner(message: "Error retrieving Creator profile")
print("Error retrieving creator profile ID:[\(creatorId)] \(String(describing: error?.localizedDescription))")
return
}
self.currentProfile = profiles.first!
}
The ViewController is called in Modal mode so it should be deallocated every time I exit the screen.
Problem: a huge chunk of memory get allocated when I enter the screen, but it doesn't get freed up when I leave it. I'm sure about this because the problem doesn't appear if I remove the line self.currentProfile = profiles.first! (obviously)
How can I avoid this from happening?
NOTE: currentProfile is of type User, which was used to be a struct. I made it a class so I could use a weak reference for storing the information:
weak var currentCreator: User? {
didSet {
updateView()
}
}
but the problem still persists.

You are adding an observer:
profilesNode.child(userId).observe(...)
But you never remove it. As long as that observe is still added, it will hold on to memory from the entire set of results, and continually retrieve new updates. It's a really bad practice not to remove your observers.
If you want to read data just a single time, there is a different API for that using observeSingleEvent.

Related

Listener event not triggered when document is updated (Google Firestore)

I am struggling to understand why my event listener that I initialize on a document is not being triggered whenever I update the document within the app in a different UIViewController. If I update it manually in Google firebase console, the listener event gets triggered successfully. I am 100% updating the correct document too because I see it get updated when I update it in the app. What I am trying to accomplish is have a running listener on the current user that is logged in and all of their fields so i can just use 1 global singleton variable throughout my app and it will always be up to date with their most current fields (name, last name, profile pic, bio, etc.). One thing I noticed is when i use setData instead of updateData, the listener event gets triggered. For some reason it doesn't with updateData. But i don't want to use setData because it will wipe all the other fields as if it is a new doc. Is there something else I should be doing?
Below is the code that initializes the Listener at the very beginning of the app after the user logs in.
static func InitalizeWhistleListener() {
let currentUser = Auth.auth().currentUser?.uid
let userDocRef = Firestore.firestore().collection("users").document(currentUser!)
WhistleListener.shared.listener = userDocRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("INSIDE LISTENER")
}
}
Below is the code that update's this same document in a different view controller whenever the user updates their profile pic
func uploadProfilePicture(_ image: UIImage) {
guard let uid = currentUser!.UID else { return }
let filePath = "user/\(uid).jpg"
let storageRef = Storage.storage().reference().child(filePath)
guard let imageData = image.jpegData(compressionQuality: 0.75) else { return }
storageRef.putData(imageData) { metadata, error in
if error == nil && metadata != nil {
self.userProfileDoc!.updateData([
"profilePicURL": filePath
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
}
You can use set data with merge true it doesn't wipe any other property only merge to specific one that you declared as like I am only update the name of the user without wiping the age or address
db.collection("User")
.document(id)
.setData(["name":"Zeeshan"],merge: true)
The answer is pretty obvious (and sad at the same time). I was constantly updating the filepath to be the user's UID therefore, it would always be the same and the snapshot wouldn't recognize a difference in the update. It had been some time since I had looked at this code so i forgot this is what it was doing. I was looking past this and simply thinking an update (no matter if it was different from the last or not) would trigger an event. That is not the case! So what I did was append an additional UUID to the user's UID so that it changed.

I can't get array and put in the another array swift from Firebase storage

I want get folders and images from Firebase storage. On this code work all except one moment. I cant append array self.collectionImages in array self.collectionImagesArray. I don't have error but array self.collectionImagesArray is empty
class CollectionViewModel: ObservableObject {
#Published var collectionImagesArray: [[String]] = [[]]
#Published var collectionImages = [""]
init() {
var db = Firestore.firestore()
let storageRef = Storage.storage().reference().child("img")
storageRef.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for prefixName in result.prefixes {
let storageLocation = String(describing: prefixName)
let storageRefImg = Storage.storage().reference(forURL: storageLocation)
storageRefImg.listAll { (result, error) in
if error != nil {
print((error?.localizedDescription)!)
}
for item in result.items {
// List storage reference
let storageLocation = String(describing: item)
let gsReference = Storage.storage().reference(forURL: storageLocation)
// Fetch the download URL
gsReference.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for each item storage location
let img = "\(url?.absoluteString ?? "placeholder")"
self.collectionImages.append(img)
print("\(self.collectionImages)")
}
}
}
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
//
self.collectionImagesArray.append(self.collectionImages)
}
}
}
If i put self.collectionImagesArray.append(self.collectionImages) in closure its works but its not what i want
The problem is caused by the fact that calling downloadURL is an asynchronous operation, since it requires a call to the server. While that call is happening, your main code continues so that the user can continue to use the app. Then when the server returns a value, your closure/completion handler is invoked, which adds the URL to the array. So your print("\(self.collectionImagesArray)") happens before the self.collectionImages.append(img) has ever been called.
You can also see this in the order that the print statements occur in your output. You'll see the full, empty array first, and only then see the print("\(self.collectionImages)") outputs.
The solution for this problem is always the same: you need to make sure you only use the array after all the URLs have been added to it. There are many ways to do this, but a simple one is to check whether your array of URLs is the same length as result.items inside the callback:
...
self.collectionImages.append(img)
if self.collectionImages.count == result.items.count {
self.collectionImagesArray.append(self.collectionImages)
print("\(self.collectionImagesArray)")
}
Also see:
How to wait till download from Firebase Storage is completed before executing a completion swift
Closure returning data before async work is done
Return image from asynchronous call
SwiftUI: View does not update after image changed asynchronous

Why are previous views that have been popped from the stack still active

I have this truck driving app in swiftUI where I use fire base to log users in and out. The problem is that when I sign in with one user, and all it’s fire base functionalities are triggered, After I log out from the current user and into a new user, the functionality of the old user is still playing out. I think it might have something to do with the firebase functions being in an onAppear method. I am not sure though.
This is the firebase code. I dont think what Im querying has any relation to the solution so I wont explain it but if you think it does than please let me know.
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (_,_) in
}
myDrivers = []
getEmails()
db.collectionGroup("resources").whereField("control", isEqualTo: true).addSnapshotListener { (snapshot, err) in
if myDrivers.count == 0{}
else{
if err != nil{print("Error fetching motion status: \(err)")}
if ((snapshot?.documents.isEmpty) != nil){}
// Gets all the driverLocation documents
for doc in snapshot!.documents{
if myDrivers.contains(doc.reference.parent.parent!.documentID){
let isDriving = doc.get("isDriving") as! Bool
let isStopped = doc.get("isStopped") as! Bool
let notified = doc.get("notified") as! Bool
if (isDriving == false && isStopped == false) || notified{}
else{
// Gets the name of the drivers
doc.reference.parent.parent?.getDocument(completion: { (snapshot, err) in
if err != nil{print("Error parsing driver name: \(err)")}
let firstName = snapshot?.get("firstName") as! String
let lastName = snapshot?.get("lastName") as! String
self.notifiedDriver = "\(firstName) \(lastName)"
})
// Logic
if isDriving{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) is back on the road.")
showDrivingToast.toggle()
doc.reference.updateData(["notified" : true])
}else if isStopped{
send(notiTitle: "MyFleet", notiBody: "\(notifiedDriver) has stopped driving.")
showStoppedToast.toggle()
doc.reference.updateData(["notified" : true])
}
}
}
else{}
}
}
}
})
Listeners don't die when a view controller is left. They remain active.
It's important to manage them through handlers for specific listeners as a view closes or the user navigates away. Here's how to remove a specific listener
let listener = db.collection("cities").addSnapshotListener { querySnapshot, error in
// ...
}
and then later
// Stop listening to changes
listener.remove()
Or, if your user is logging out, you can use removeAllObservers (for the RealtimeDatabase) to remove them all at one time, noting that
removeAllObservers must be called again for each child reference where
a listener was established
For Firestore, store the listeners in a listener array class var and when you want to remove them all, just iterate over the array elements calling .remove() on each.
There's additional info in the Firebase Getting Started Guide Detach Listener

How to refer to currently logged in users in swift using Firestore?

This code shows the app's main view controller. We would like increment the currently logged in user's field value by 1. In the code below we are only able to do this by manually pasting "nDcAFLPpRuPXI9AOLkln" which we copied from fire base itself.
How do we automatically refer to the currently logged in user?
snapchot of our firestore data tree
#IBAction func bidButton(_ sender: Any) {
let updateScore = db.collection("users").document("nDcAFLPpRuPXI9AOLkln")
updateScore.updateData([
"leaderboardscore": FieldValue.increment(Int64(1))
])
db.collection("users").document("nDcAFLPpRuPXI9AOLkln")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
// there was an error
print("Error fetching document: \(error!)")
return
}
// no data to show
guard let data = document.data() else {
print("Document data was empty.")
return
}
self.leaderBoardScoreLabel.text = String("Current data: \(data)")
//print("Current data: \(data)")
}
If you are using Firebase Authentication, and asking how to get the currently signed in user, you should follow the instructions in the documentation:
The recommended way to get the current user is by setting a listener on the Auth object:
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
// ...
}
By using a listener, you ensure that the Auth object isn't in an
intermediate state—such as initialization—when you get the current
user.
You can also get the currently signed-in user by using the currentUser
property. If a user isn't signed in, currentUser is nil:
if Auth.auth().currentUser != nil {
// User is signed in.
// ...
} else {
// No user is signed in.
// ...
}
I would strongly suggest learning how to use the listener, which will give you a callback every time the user is seen to sign in or out.
Once you have a User object, you can use its uid property to get the string you're looking for:
let uid = user.uid

How to get ObjectID and search for specific ObjectID in CoreData in Swift 5?

I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.

Resources