I have a very common iOS app scenario:
The MainVC of the app is a UITabBarController. I set this VC as the rootViewController in the AppDelegate.swift file:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
window?.rootViewController = MainVC()
window?.makeKeyAndVisible()
}
When the user logs out, I present a navigation controller with the LandingVC as the root view controller of the navigation stack.
let navController = UINavigationController(rootViewController: LandingVC)
self.present(navController, animated: true, completion: nil)
Inside LandingVC you click on Login button and the LoginVC is pushed on the top of the stack.
navigationController?.pushViewController(LoginVC(), animated: true)
When the user successfully logs in I dismiss() the navigation controller from inside the LoginVC.
self.navigationController?.dismiss(animated: true, completion: nil)
Basically, I am trying to achieve the flow below:
Everything works, but the problem is that the LoginVC is never deallocated from the memory. So if a user logs in and logs out 4 times (no reason to do that but still there is a chance), I will see LoginVC 4 times in the memory and LandingVC 0 times.
I don't understand why the LoginVC is not deallocated, but the LandingVC is.
In my mind (and correct me where I am wrong), since the navigation controller is presented and it contains 2 VCs (LandingVC and LoginVC), when I use dismiss() inside LoginVC it should dismiss the navigation controller, and therefore both contained VCs.
- MainVC: presenting VC
- Navigation Controller: presented VC
From Apple docs:
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
I believe that something is going wrong when I dismiss the navigation controller within LoginVC. Is there a way to trigger dismiss() inside MainVC (presenting VC) as soon as the user logs in?
PS: using the code below will not do the trick since it pops to the root view controller of the navigation stack, which is the LandingVC; and not to MainVC.
self.navigationController?.popToRootViewController(animated: true)
Any help would be much appreciated!
====================================
My LoginVC code:
import UIKit
import Firebase
import NotificationBannerSwift
class LoginVC: UIViewController {
// reference LoginView
var loginView: LoginView!
override func viewDidLoad() {
super.viewDidLoad()
// dismiss keyboard when clicking outside textfields
self.hideKeyboard()
// setup view elements
setupView()
setupNavigationBar()
}
fileprivate func setupView() {
let mainView = LoginView(frame: self.view.frame)
self.loginView = mainView
self.view.addSubview(loginView)
// link button actions from LoginView to functionality inside LoginViewController
self.loginView.loginAction = loginButtonClicked
self.loginView.forgotPasswordAction = forgotPasswordButtonClicked
self.loginView.textInputChangedAction = textInputChanged
// pin view
loginView.translatesAutoresizingMaskIntoConstraints = false
loginView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
loginView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
loginView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
loginView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
fileprivate func setupNavigationBar() {
// make navigation controller transparent
self.navigationController?.navigationBar.isTranslucent = true
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
// change color of text
self.navigationController?.navigationBar.tintColor = UIColor.white
// add title
navigationItem.title = "Login"
// change title font attributes
let textAttributes = [
NSAttributedStringKey.foregroundColor: UIColor.white,
NSAttributedStringKey.font: UIFont.FontBook.AvertaRegular.of(size: 22)]
self.navigationController?.navigationBar.titleTextAttributes = textAttributes
}
fileprivate func loginButtonClicked() {
// some local authentication checks
// ready to login user if credentials match the one in database
Auth.auth().signIn(withEmail: emailValue, password: passwordValue) { (data, error) in
// check for errors
if let error = error {
// display appropriate error and stop rest code execution
self.handleFirebaseError(error, language: .English)
return
}
// if no errors during sign in show MainTabBarController
guard let mainTabBarController = UIApplication.shared.keyWindow?.rootViewController as? MainTabBarController else { return }
mainTabBarController.setupViewControllers()
// this is where i dismiss navigation controller and the MainVC is displayed
self.navigationController?.dismiss(animated: true, completion: nil)
}
}
fileprivate func forgotPasswordButtonClicked() {
let forgotPasswordViewController = ForgotPasswordViewController()
// present as modal
self.present(forgotPasswordViewController, animated: true, completion: nil)
}
// tracks whether form is completed or not
// disable registration button if textfields not filled
fileprivate func textInputChanged() {
// check if any of the form fields is empty
let isFormEmpty = loginView.emailTextField.text?.count ?? 0 == 0 ||
loginView.passwordTextField.text?.count ?? 0 == 0
if isFormEmpty {
loginView.loginButton.isEnabled = false
loginView.loginButton.backgroundColor = UIColor(red: 0.80, green: 0.80, blue: 0.80, alpha: 0.6)
} else {
loginView.loginButton.isEnabled = true
loginView.loginButton.backgroundColor = UIColor(red: 32/255, green: 215/255, blue: 136/255, alpha: 1.0)
}
}
}
After a lot of searching, I think I found the solution:
What inspired me was all guys commenting this question and also this article:
https://medium.com/@stremsdoerfer/understanding-memory-leaks-in-closures-48207214cba
I will start with my philosophy of coding: I like to keep my code separated and clean. So, I always try to create a UIView with all the elements I want and then "link" it to the appropriate view controller. But what happens when the UIView has buttons, and the buttons need to fulfill actions? As we all know, there is no room for "logic" inside the views:
So as the article says:
That could be the reason why my LoginVC was never deallocated from the memory. [SPOILER ALERT: that was the reason!]
As shown in the question, the LoginVC is responsible for executing all button actions. What I was doing before was:
Now that i am aware of what is causing the retain cycle, I need to find a way and break it. As the article says:
So what I changed in LoginVC to achieve this was:
After this simple code change (yeah it took me 10 days to figure it out), everything is running as before, but the memory is thanking me.
Couple things to say:
When I first noticed this memory issue, I blamed myself for not dismissing/popping the view controllers correctly. You can find out more in my previous StackOverflow question here: ViewControllers, memory consumption and code efficiency
In the process, I learned a lot about presenting/pushing view controllers and navigation controllers; so even though I was looking in the wrong direction, I surely learned a lot.
Nothing comes for free, memory leak taught me that!
Hope I could help others with the same issue as me!