Swift:呈现模态并解除导航控制器

我有一个非常常见的iOS应用场景:

该应用程序的MainVC是一个UITabBarController 。 我将此VC设置为AppDelegate.swift文件中的rootViewController:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow() window?.rootViewController = MainVC() window?.makeKeyAndVisible() } 

当用户注销时,我提供了一个导航控制器,其中LandingVC作为导航堆栈的根视图控制器。

 let navController = UINavigationController(rootViewController: LandingVC) self.present(navController, animated: true, completion: nil) 

LandingVC中 ,单击Login按钮, LoginVC将被推到堆栈顶部。

 navigationController?.pushViewController(LoginVC(), animated: true) 

当用户成功登录时,我从LoginVC内部解除()导航控制器。

 self.navigationController?.dismiss(animated: true, completion: nil) 

基本上,我正在尝试实现以下流程:

在此处输入图像描述

一切正常,但问题是LoginVC永远不会从内存中释放出来 。 因此,如果用户登录并注销4次(没有理由这样做但仍然有机会),我会在内存中看到LoginVC 4次,而LandingVC会看到0次。

我不明白为什么LoginVC没有被释放,但是LandingVC是。

在我的脑海里(并在我错误的地方纠正我),由于导航控制器出现并且它包含2个VC( LandingVCLoginVC ),当我在LoginVC中使用dismiss()时它应该关闭导航控制器,因此都包含VC 。

  • MainVC :介绍VC
  • 导航控制器 :介绍VC

来自Apple文档:

呈现视图控制器负责解除它所呈现的视图控制器。 如果您在呈现的视图控制器本身上调用此方法,UIKit会要求呈现视图控制器处理解雇。

我认为当我解除LoginVC中的导航控制器时会出现问题。 有没有办法在用户登录后立即在MainVC (呈现VC)中触发dismiss()?

PS:使用下面的代码不会起作用,因为它弹出到导航堆栈的根视图控制器,即LandingVC; 而不是MainVC。

 self.navigationController?.popToRootViewController(animated: true) 

任何帮助将非常感激!

====================================

我的LoginVC代码:

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

经过大量的搜索,我认为我找到了解决方案:

是什么激发了我所有人评论这个问题以及这篇文章:

https://medium.com/@stremsdoerfer/understanding-memory-leaks-in-closures-48207214cba

我将从我的编码哲学开始:我喜欢保持代码分离和清洁。 因此,我总是尝试使用我想要的所有元素创建一个UIView,然后将其“链接”到适当的视图控制器。 但是当UIView有按钮,按钮需要完成动作时会发生什么? 众所周知,观点内部没有“逻辑”空间:

 class LoginView: UIView { // connect to view controller var loginAction: (() -> Void)? var forgotPasswordAction: (() -> Void)? // some code that initializes the view, creates the UI elements and constrains them as well // let's see the button that will login the user if credentials are correct let loginButton: UIButton = { let button = UIButton(title: "Login", font: UIFont.FontBook.AvertaSemibold.of(size: 20), textColor: .white, cornerRadius: 5) button.addTarget(self, action: #selector(handleLogin), for: .touchUpInside) button.backgroundColor = UIColor(red: 0.80, green: 0.80, blue: 0.80, alpha: 0.6) return button }() // button actions @objc func handleLogin() { loginAction?() } @objc func handleForgotPassword() { forgotPasswordAction?() } } 

正如文章所说:

LoginVC强烈引用了LoginView ,它强烈引用了loginActionforgotPasswordAction闭包, 这些闭包刚刚创建了对self的强引用。

你可以清楚地看到我们有一个循环。 这意味着,如果退出此视图控制器,则无法从内存中删除它,因为它仍然由闭包引用。

这可能是我的LoginVC永远不会从内存中释放的原因。 [SPOILER ALERT:这就是原因!]

如问题所示,LoginVC负责执行所有按钮操作。 我以前做的是:

 class LoginVC: UIViewController { // reference LoginView var loginView: LoginView! override func viewDidLoad() { super.viewDidLoad() setupView() } 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 LoginVC // THIS IS WHAT IS CAUSING THE RETAIN CYCLE <-------------------- self.loginView.loginAction = loginButtonClicked self.loginView.forgotPasswordAction = forgotPasswordButtonClicked // pin view ..... } // our methods for executing the actions fileprivate func loginButtonClicked() { ... } fileprivate func forgotPasswordButtonClicked() { ... } } 

既然我知道导致保留周期的原因,我需要找到一种方法并打破它。 正如文章所说:

要打破一个循环,你只需要打破一个参考,你就想打破最简单的一个。 处理闭包时,你总是想要打破最后一个链接,这就是闭包引用的内容。

为此,您需要指定何时捕获不需要强链接的变量。 你有两个选择:弱或无主,你在关闭的最开始声明它。

所以我在LoginVC中为了实现这个目的而改变的是:

 fileprivate func setupView() { ... ... ... self.loginView.loginAction = { [unowned self] in self.loginButtonClicked() } self.loginView.forgotPasswordAction = { [unowned self] in self.forgotPasswordButtonClicked() } self.loginView.textInputChangedAction = { [unowned self] in self.textInputChanged() } } 

在这个简单的代码更改之后(是的,我花了10天时间才弄明白),一切都像以前一样运行,但记忆中感谢我。

有几件事要说:

  1. 当我第一次注意到这个内存问题时,我责怪自己没有正确地解除/弹出视图控制器。 您可以在我之前的StackOverflow问题中找到更多信息: ViewControllers,内存消耗和代码效率

  2. 在这个过程中,我学到了很多关于呈现/推动视图控制器和导航控制器的知识; 所以尽管我看错了方向,但我确实学到了很多东西。

  3. 没有什么是免费的,内存泄漏教会了我!

希望我能像我一样帮助别人解决同样的问题!