is this firestore closure causing a memory leak? - swift

So my goal is to fix this condition issue when it comes to instantiating the right viewController. I have a function that I basically use to navigate a user to the right viewController depending on the type of user and if they're logged in or not.
Here is this function :
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let auth = Auth.auth()
let actualuser = Auth.auth().currentUser
auth.addStateDidChangeListener { (_, user) in
if user != nil {
db.document("student_users/\(actualuser?.uid)").getDocument { (docSnapshot, error) in
if error != nil {
print("\(error)")
} else {
let docSnap = docSnapshot?.exists
guard docSnap! else {
let alreadyLoggedInAsASchoolViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.SchoolEventDashboardStoryboardID) as! SchoolTableViewController
let navigationizedSchoolVC = UINavigationController(rootViewController: alreadyLoggedInAsASchoolViewController)
self.window!.rootViewController = navigationizedSchoolVC
self.window!.makeKeyAndVisible()
return
}
let alreadyLoggedInAsAStudentViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.StudentEventDashboardStoryboardID) as! StudentSegmentedTableViewController
let navigationizedVC = UINavigationController(rootViewController: alreadyLoggedInAsAStudentViewController)
self.window!.rootViewController = navigationizedVC
self.window!.makeKeyAndVisible()
}
}
} else {
let notLoggedInAtAll = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.GothereMainMenuStoryboardID) as! GothereMainMenuViewController
let navMainMenu = UINavigationController(rootViewController: notLoggedInAtAll)
self.window!.rootViewController = navMainMenu
self.window!.makeKeyAndVisible()
}
}
}
I also have the exact block of code like this for the sceneDidEnterForeground for push notification purposes. Now the issue is when I run the simulator and launch the app for the first time, the correct viewController will show up, but when I logout as a school user and login as a school user in that same simulator session, the wrong viewController (aka the viewController of the other type of user) shows up.
Not that it would be a real situation in production where a student user would just have access to a school user's account and log in like that in the same scene session, but still, better to be safe than sorry. So this leads to me ask, is this a memory leak or a completely different issue?
I also get this error :

Your query is based on the variable actualuser, which looks like it is only set once, when the scene is first set up. Inside the state change callback, it's never updated.
So, when you log out, then log back in as a different user, that initial value of actualuser will be used, explaining why you see the wrong view controller. Then, when you run the app again and the scene is set up, actualuser gets set to the auth().currentUser again, showing you the correct view controller.
The solution here is to base your query on the current (and current) user.
Something like:
db.document("student_users/\(user.uid)")
(Instead of checking user != nil, do an optional binding with let user = user and then you can avoid the ? unwrapping)
This is not, by the way, a memory leak, which is a different type of issue: https://en.wikipedia.org/wiki/Memory_leak

Related

Swift - Detecting an unspecified Interface style

I want to be able to read if the current app appearance is set to unspecified. When clicking on the desire table view cell, the appearance is properly set where changing the appearance of device mimics with the app. However I cannot find a way to read the value of appearance to be: light/dark/unspecified. Only light/dark. Upon reading some more I read this below.
The trait collection contains a complete set of trait values
describing the current environment, and does not include unspecified
or unknown values.
I need to check if the appearance is unspecified in other parts and I was curious if anyone had ideas on how to check. Here is a snippet of what I have, below is a helper function
let appearanceMap = [Appearance.device, Appearance.light, Appearance.dark]
switch appearanceMap[indexPath.row] {
case .light:
UIApplication.shared.changeInterfaceStyle(.light)
case .dark:
UIApplication.shared.changeInterfaceStyle(.dark)
case .device:
UIApplication.shared.changeInterfaceStyle(.unspecified)
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let keyWindow = appDelegate.window else { return }
let currentAppearance = keyWindow.traitCollection.userInterfaceStyle
print(currentAppearance.stringValue)
extension UIApplication {
func changeInterfaceStyle(_ mode: UIUserInterfaceStyle) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let keyWindow = appDelegate.window else { return }
keyWindow.overrideUserInterfaceStyle = mode
}
}
Maybe try to use this instead
if UIApplication.shared.windows.first?.overrideUserInterfaceStyle == .unspecified {
print("unspecified")
}

Understanding addStateDidChangeListener firebase authentication

Hi I am new to firebase and want to confirm my understanding of an auth function.(addStateDidChangeListener)
A little background:
When the app is first launched, the app delegate is called first and then this sceneDelegate function is called. Now it will present a screen based on whether the user is signed in or not. If they are not signed up it will present them with my signin/signup screen (self.createHandleSignInOrSignUp()).
Now 2 questions:
After they sign up or sign in the app jumps back to this block of code inside .addStateDidChangeListener.
This is because the auth state changes, right?
Also while the app is running you can sign out, and then this block of code is called.
This is because this function is always 'listening' for auth state changes while the app is running?
The code below is my part of my SceneDelegate.swift file and the function I have questions about.
Thank you
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
handle = Auth.auth().addStateDidChangeListener({ auth, user in
if (user != nil) {
self.window!.rootViewController = self.createTabBar()
}
else {
self.window!.rootViewController = self.createHandleSignInOrSignUp()
}
})
window?.makeKeyAndVisible()
}
The completion handler for your auth state change listener will be called each time the auth state changes. This typically happens asynchronously, for example when the user signs in or signs out.
The auth state listener remains active until you remove it, or until the app exits.

Memory leak when displaying a modal view and dismissing it

When an AVExportSession is finished exporting, I have my app display a modal view displaying the video and an array of images. Dismissing the modal view, and making it display again over and over shows a memory increase that continuously grows. I'm suspicious of a strong reference cycle that could be occurring.
I'm setting required variables on the modal view (manageCaptureVC). fileURL is a global variable that manageCaptureVC can read from to get the video. The video is removed based on that URL when the modal view is dismissed. The leak is larger depending on the size of the media that is captured and displayed in the modal view.
I have used the Leaks Instrument. Unfortunately, it never points to any of my functions. It shows memory addresses that displays assembly language. I am also using a device.
Here is a screen shot of my leaks instrument at the point I display and dismiss my view, and the instrument indicates leaks:
Anything obvious what could cause a leak in my case?
Presenting the modal view (manageCaptureVC)
// video done exporting
guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = mainVideoURL
exporter.outputFileType = AVFileType.mov
let manageCaptureVC = self.storyboard?.instantiateViewController(withIdentifier: "ManageCaptureVC") as! ManageCaptureVC
exporter.exportAsynchronously(completionHandler: {[weak self]
() -> Void in
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteExportFile() // delete export file
self?.myTimer.invalidate()
fileURL = mainVideoURL
guard let imgCaptureModeRawVal = self?.imageCaptureMode.rawValue else { return }
manageCaptureVC.imageCaptureMode = ManageCaptureVC.imageCaptureModes(rawValue: imgCaptureModeRawVal)!
manageCaptureVC.delegate = self
DispatchQueue.main.async(){
manageCaptureVC.modalPresentationStyle = .fullScreen
self?.present(manageCaptureVC, animated: true, completion: nil)
}
})
Dismissing the view:
func goBackTask(){
// turn off manage capture tutorial if needed
if debug_ManageCaptureTutorialModeOn {
debug_ManageCaptureTutorialModeOn = false
delegate?.resetFiltersToPrime()
}
// no longer ignore interface orientation
ignoreSelectedInterfaceOrientation = false
// remove observer for the application becoming active in this view
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
if let videoEndedObs = self.videoEndedObserver {
NotificationCenter.default.removeObserver(videoEndedObs)
}
// invalidate thumb timer
thumbColorTimer.invalidate()
// empty UIImages
uiImages.removeAll()
// delete video
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteFile()
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:false)
if let p = self.player, let pl = self.playerLayer {
p.pause()
pl.removeObserver(self, forKeyPath: "videoRect")
pl.removeFromSuperlayer()
p.replaceCurrentItem(with: nil)
}
group.leave()
}
let group2 = DispatchGroup()
group.notify(queue: .main) {
group2.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:true)
group2.leave()
}
}
group2.notify(queue: .main) {
self.dismiss(animated: true)
}
}
I came across this problem as well. It took me days to track it down.
Setting modalPresentationStyle to .fullScreen resulted in the View Controller not being released. I was able to reproduce this on a trivially simple example.
I got round it by setting modalPresentationStyle to .currentContext.
None of the Instruments identified this retain cycle - I guess because it was in low level Apple code.

Receive SIGABRT error when trying to hijack root view controller

I am trying to have my application open a different view controller based upon whether an array is empty in the user's NSUserDefaults. Essentially, if the user has previously saved data in the app, the app will open up to where they can select the data. Otherwise, the app will open to a welcome screen.
However, when the array is empty, I see the background color that I set for the welcome screen, but not the text or button that I laid out in the storyboard. When the array is not empty and the data page should open, my app crashes with a SIGABRT error. I checked all of the outlets for the view controller in question and nothing seems to be disconnected. Additionally, when I comment out the code in the app delegate and set the data view controller as my initial starting view, the app runs fine.
The full error is "Thread 1: signal SIGABRT" and it is tagged in the class AppDelegate line.
The code I used in the App Delegate is below:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
var accounts = loadAccounts()!
if accounts.isEmpty {
let welcomeController = WelcomeViewController()
self.window!.rootViewController = welcomeController
} else {
let tableController = AccountTableViewController()
self.window!.rootViewController = tableController
}
return true
}
func loadAccounts() -> [Account]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Account.ArchiveURL.path) as? [Account]
}
Maybe the UIWindow is not set properly.
let bounds = UIScreen.main.bounds
self.window = UIWindow(frame: bounds)
self.window?.rootViewController = `your view controller`
self.window?.makeKeyAndVisible()
What else can go wrong?
var accounts = loadAccounts()!
This line is the culprit for you. Precisely this symbol ! is I guess. You are trying to fetch data from a database or a filesystem and expect it will always be there.
Think about it, it can't be true all the time.
# check if account array is not empty; I would have returned nil if it would be empty and so we can avoid that extra check here.
if let accounts = loadAccounts(), !accounts.isEmpty {
let tableController = AccountTableViewController()
self.window!.rootViewController = tableController
return true
}
let welcomeController = WelcomeViewController()
self.window!.rootViewController = welcomeController
Also, if you can provide more info about the error message from your debug console. Then I would be able to help in a better way.

Swift Firebase Check if user exists

What am i doing wrong? I have a database structure like the one shown in this image.
In appleDelegate.swift i just want to check if a certain user token actually exists under the "users" node. that is, if "users" has the child currentUserID (a string token). I understand observeSingleEvent is executed asynchronously.I get this error in swift: 'Application windows are expected to have a root view controller at the end of application launch'. in "func application(_ application: UIApplication" i have this code. I also have my completion handler function below.
if let user = Auth.auth().currentUser{
let currentUserID = user.uid
ifUserIsMember(userId:currentUserID){(exist)->() in
if exist == true{
print("user is member")
self.window?.rootViewController = CustomTabBarController()
} else {
self.window?.rootViewController = UINavigationController(rootViewController: LoginController())
}
}
return true
} else {
self.window?.rootViewController = UINavigationController(rootViewController: LoginController())
return true
}
}
func ifUserIsMember(userId:String,completionHandler:#escaping((_ exists : Bool)->Void)){
print("ifUserIsMember")
let ref = Database.database().reference()
ref.child("users").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChild(userId) {
print("user exists")
completionHandler(true)
} else {
print("user doesn't exist")
completionHandler(false)
}
})
}
I would suggest moving the code out of the app delegate and into an initial viewController. From there establish if this is an existing user and send the user to the appropriate UI.
.observeSingleEvent loads all of the nodes at a given location - one use would be to iterate over them to populate a datasource. If there were 10,000 users they would all be loaded in if you observe the /users node.
In this case it's really not necessary. It would be better to just observe the single node you are interested in and if it exists, send the user to a UI for existing users.
here's the code to do that
if let user = Auth.auth().currentUser {
let ref = self.ref.child("users").child(user.uid)
ref.observeSingleEvent(of: .value, with: { snapshot in
self.presentUserViewController(existing: snapshot.exists() )
})
}
snapshot.exists will be either true if the user node exists or false if not so the function presentUserViewController would accept a bool to then set up the UI depending on the user type.