Swift: Представление модально и отклонение навигационного контроллера - PullRequest
0 голосов
/ 27 июня 2018

У меня очень распространенный сценарий приложения для iOS:

MainVC приложения представляет собой UITabBarController . Я установил этот VC как rootViewController в файле AppDelegate.swift:

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 ), когда я использую dismiss ( ) внутри LoginVC он должен отклонить навигационный контроллер, и поэтому оба содержат VC.

  • MainVC : представление VC
  • Контроллер навигации : представлен VC

Из документов Apple:

Представляющий контроллер представления отвечает за отклонение представленного контроллера. Если вы вызываете этот метод на самом представленном контроллере представления, UIKit просит представляющий контроллер представления обработать отклонение.

Я считаю, что что-то идет не так, когда я отключаю контроллер навигации в LoginVC . Есть ли способ вызвать dismiss () внутри MainVC (представляя VC), как только пользователь входит в систему?

PS: использование приведенного ниже кода не поможет, так как он появляется в корневом контроллере представления стека навигации, который является LandingVC; а не на 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)
        }
    }
}

1 Ответ

0 голосов
/ 28 июня 2018

После долгих поисков, я думаю, я нашел решение:

Меня вдохновили все, кто комментировал этот вопрос, а также эту статью:

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 никогда не был освобожден из памяти. [ПРЕДУПРЕЖДЕНИЕ СПОЙЛЕРА: это была причина!]

Как показано в вопросе, 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. Ничто не приходит бесплатно, утечка памяти научила меня этому!

Надеюсь, я смогу помочь другим с той же проблемой, что и я!

...