Swift: Представление модально и отклонение навигационного контроллера
У меня очень распространенный сценарий приложения для iOS:
Основным приложением приложения является 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 вы нажимаете кнопку 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 (LandingVC и LoginVC), когда я использую dismiss () внутри LoginVC, он должен отклонить контроллер навигации, и поэтому оба содержат VC,
- MainVC: представление ВК
- Контроллер навигации: представлен ВК
Из документов 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 ответ
После долгих поисков, я думаю, я нашел решение:
Меня вдохновили все, кто комментировал этот вопрос, а также эту статью:
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 дней, чтобы выяснить это), все работает как прежде, но память благодарит меня.
Пара вещей, чтобы сказать:
Когда я впервые заметил эту проблему с памятью, я обвинил себя в том, что неправильно уволил / выкинул контроллеры представления. Вы можете узнать больше в моем предыдущем вопросе Stackru здесь: ViewControllers, потребление памяти и эффективность кода
В процессе я узнал много нового о представлении / представлении контроллеров представления и контроллеров навигации; поэтому, хотя я смотрел в неправильном направлении, я, безусловно, многому научился.
Ничто не приходит бесплатно, утечка памяти научила меня этому!
Надеюсь, что смогу помочь другим с той же проблемой, что и я!