Swift:以模态呈现并关闭导航控制器

时间:2018-06-26 21:53:16

标签: ios swift uinavigationcontroller dismissviewcontroller

我有一个非常常见的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 内部,单击“登录”按钮,然后将 LoginVC 推入堆栈的顶部。

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

当用户成功登录后,我从LoginVC内部关闭导航控制器。

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

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

enter image description here

一切正常,但是问题是 LoginVC 从未从内存中取消分配。因此,如果用户登录并注销4次(没有理由这样做,但仍然有机会),我将在内存中看到 LoginVC 4次,而LandingVC则看到0次。

我不明白为什么 LoginVC 没有被释放,而 LandingVC 被取消分配。

在我的脑海中(并纠正我在哪里错了),因为当我使用导航控制器时,它包含2个VC( LandingVC LoginVC 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)
        }
    }
}

1 个答案:

答案 0 :(得分:1)

经过大量搜索,我想找到了解决方法:

启发我的是所有人都在评论这个问题以及这篇文章:

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 有很强的引用,对 loginAction forgotPasswordAction 闭包的引用也很强刚刚创建了一个强烈的自我参考。

     

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

这可能是为什么我的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, memory consumption and code efficiency

  2. 在此过程中,我学到了很多有关显示/推动视图控制器和导航控制器的知识;因此,即使我朝错误的方向看,我也确实学到了很多东西。

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

希望我可以和我一样解决其他问题!