Swift:模态呈现和关闭导航控制器 [英] Swift: Presenting modally and dismissing a navigation controller

查看:40
本文介绍了Swift:模态呈现和关闭导航控制器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a very common iOS app scenario:

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

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

当用户注销时,我会显示一个导航控制器,其中 LandingVC 作为导航堆栈的根视图控制器.

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)

LandingVC 中,您点击登录按钮,LoginVC 被推到堆栈的顶部.

Inside LandingVC you click on Login button and the LoginVC is pushed on the top of the stack.

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

当用户成功登录时,我会从 LoginVC 内部关闭()导航控制器.

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:

一切正常,但问题是 LoginVC 永远不会从内存中解除分配.因此,如果用户登录和注销 4 次(没有理由这样做,但仍有机会),我将在内存中看到 LoginVC 4 次和 LandingVC 0 次.

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.

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

I don't understand why the LoginVC is not deallocated, but the LandingVC is.

在我看来(并在我错的地方纠正我),因为导航控制器出现并且它包含 2 个 VC(LandingVCLoginVC),当我使用在 LoginVC 中关闭()它应该关闭导航控制器,因此两者都包含 VC.

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:介绍 VC
  • 导航控制器:展示的 VC
  • MainVC: presenting VC
  • Navigation Controller: presented VC

来自 Apple 文档:

From Apple docs:

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

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.

我相信当我在 LoginVC 中关闭导航控制器时出现问题.有没有办法在用户登录后立即触发 MainVC(呈现 VC)内的dismiss()?

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:使用下面的代码不会成功,因为它会弹出导航堆栈的根视图控制器,即 LandingVC;而不是 MainVC.

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)

任何帮助将不胜感激!

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

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

我的登录VC代码:

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

我将从我的编码哲学开始:我喜欢保持我的代码分离和干净.所以,我总是尝试用我想要的所有元素创建一个 UIView,然后将它链接"到适当的视图控制器.但是当 UIView 有按钮并且按钮需要完成操作时会发生什么?众所周知,视图内部没有逻辑"的空间:

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:

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 has a strong reference to LoginView, that has strong reference to the loginAction and forgotPasswordAction closures that just created a strong reference to self.

如您所见,我们有一个循环.这意味着,如果你退出这个视图控制器,它不能从内存中删除,因为它仍然被闭包引用.

As you can see pretty clearly we have a cycle. Meaning that, if you exit this view controller, it can’t be removed from memory because it’s still referenced by the closure.

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

That could be the reason why my LoginVC was never deallocated from the memory. [SPOILER ALERT: that was the reason!]

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

As shown in the question, the LoginVC is responsible for executing all button actions. What I was doing before was:

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() { ... }

}

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

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:

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

To break a cycle, you just need to break one reference, and you will want to break the easiest one. When dealing with a closure you will always want to break the last link, which is what the closure references.

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

To do so, you need to specify when capturing a variable that you don’t want a strong link. The two options that you have are: weak or unowned and you declare it at the very beginning of the closure.

为了实现这一点,我在 LoginVC 中所做的更改是:

So what I changed in LoginVC to achieve this was:

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 天才弄明白),一切都像以前一样运行,但内存在感谢我.

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.

要说的几件事:

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

在这个过程中,我学到了很多关于呈现/推送视图控制器和导航控制器的知识;所以即使我看错了方向,我也确实学到了很多.

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!

这篇关于Swift:模态呈现和关闭导航控制器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆