【问题标题】:Swift: Presenting modally and dismissing a navigation controllerSwift:以模态方式呈现并关闭导航控制器
【发布时间】:2018-12-05 16:53:02
【问题描述】:

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

应用的MainVC是一个UITabBarController。我在 AppDelegate.swift 文件中将此 VC 设置为 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(LandingVCLoginVCLoginVC 中的 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)
        }
    }
}

【问题讨论】:

  • 在MainVC中调用deinit怎么样?
  • 您是如何得出 LoginVC 泄露的结论的?
  • 好吧,如果您的landing/login vc 仍然有强大的参考资料,就会发生这种情况。它可以是回调、委托甚至是属性。只要确保你也取消分配这些。完成后将它们设置为 nil,您应该能够看到 deinit 被调用。并且请不要明确调用 deinit
  • @AleksandrMedvedev 你可以在xCode的“Debug navigator”中使用“View memory graph hierarhcy”看到它。
  • 您的 LoginVC 中可能有一些强引用。您可能想在此处发布您的 VC 代码。

标签: ios swift uinavigationcontroller dismissviewcontroller


【解决方案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?()
    }
}

正如文章所说:

LoginVCLoginView 有强引用,对 loginActionforgotPasswordAction 闭包有强引用刚刚创建了一个对 self 的强引用。

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

这可能是我的 LoginVC 从未从内存中释放的原因。 [剧透警告:这就是原因!]

如问题所示,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() { ... }

}

现在我知道是什么导致了保留周期,我需要找到一种方法并打破它。正如文章所说:

要打破一个循环,你只需要打破一个引用,你会想要打破最简单的那个。在处理闭包时,您总是希望断开最后一个链接,这是闭包所引用的。

为此,您需要在捕获不需要强链接的变量时指定。您拥有的两个选项是:weak 或 unowned,并且您在关闭的一开始就声明它。

所以我在 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. 没有什么是免费的,内存泄漏教会了我!

希望我能帮助其他与我有同样问题的人!

【讨论】:

    猜你喜欢
    • 2020-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-30
    • 1970-01-01
    • 2017-06-16
    • 1970-01-01
    相关资源
    最近更新 更多