Как правильно управлять стеком памяти и просматривать контроллеры? - PullRequest
0 голосов
/ 03 мая 2018

Я действительно борюсь с этим базовым программированием для iOS, но я просто не могу понять, что происходит и как это решить.

У меня есть основной контроллер входа в систему, который определяет, когда пользователь вошел в систему, и представляет следующий контроллер, если аутентификация прошла успешно:

@interface LoginViewController (){

    //Main root instance
    RootViewController *mainPlatformRootControler;
}

-(void)loggedInActionWithToken:(NSString *)token anonymous:(BOOL)isAnon{
    NSLog(@"User loged in.");

    mainPlatformRootControler = [self.storyboard instantiateViewControllerWithIdentifier:@"rootViewCOntrollerStoryIdentifier"];

    [self presentViewController:mainPlatformRootControler animated:YES completion:^{

    }];

}

И это хорошо работает, без проблем.

Моя проблема связана с выходом из системы. Как полностью удалить экземпляр RootViewController и показать новый?

Я вижу, что экземпляры RootViewController складываются, потому что у меня есть несколько наблюдателей, и после выхода из системы, а затем входа в систему они вызываются несколько раз (столько раз, сколько я выхожу и снова вхожу).

Я попробовал следующее безуспешно:

Сначала обнаружение выхода из системы в RootViewController и отклонение:

[self dismissViewControllerAnimated:YES completion:^{
                [[NSNotificationCenter defaultCenter] postNotificationName:@"shouldLogOut" object:nil];

            }];

А затем в LoginViewController:

-(void)shouldLogOut:(NSNotification *) not{
    NSLog(@"No user signed in");
    mainPlatformRootControler = NULL;
    mainPlatformRootControler = nil;
}

Так, как я могу справиться с этим? Я знаю, что это базовая работа с памятью, но я просто не знаю, как?

Ответы [ 7 ]

0 голосов
/ 10 мая 2018

Существует много правильных способов управления иерархиями представлений, но я поделюсь одним из способов, которые я считаю простыми и эффективными.

По сути, я заменяю основные UIWindow rootViewController при выходе из системы. Кроме того, я программно предоставляю rootViewController, а не позволяю @UIApplicationMain загрузить начальный контроллер вида. Преимущество этого состоит в том, что во время запуска приложения, если пользователь вошел в систему, тогда Login.storyboard никогда не должен загружаться.

Функцию show можно настроить в соответствии с вашим стилем, но мне нравятся перекрестные переходы растворения, поскольку они очень просты.

enter image description here

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    lazy var window: UIWindow? = {

        let window = UIWindow()
        window.makeKeyAndVisible()

        return window
    }()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // Your own logic here
        let isLoggedIn = false

        if isLoggedIn {
            show(MainViewController(), animated: false)
        } else {
            show(LoginViewController(), animated: false)
        }

        return true
    }
}

class LoginViewController: UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .red
        let logoutButton = UIButton()
        logoutButton.setTitle("Log In", for: .normal)
        logoutButton.addTarget(self, action: #selector(login), for: .touchUpInside)
        view.addSubview(logoutButton)
        logoutButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate(
            [logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
             logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)]
        )

        self.view = view
    }

    @objc
    func login() {
        AppDelegate.shared.show(MainViewController())
    }
}

class MainViewController: UIViewController {

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .blue
        let logoutButton = UIButton()
        logoutButton.setTitle("Log Out", for: .normal)
        logoutButton.addTarget(self, action: #selector(logout), for: .touchUpInside)
        view.addSubview(logoutButton)
        logoutButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate(
            [logoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
             logoutButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ]
        )

        self.view = view
    }

    @objc
    func logout() {
        AppDelegate.shared.show(LoginViewController())
    }
}

extension AppDelegate {

    static var shared: AppDelegate {
        // swiftlint:disable force_cast
        return UIApplication.shared.delegate as! AppDelegate
        // swiftlint:enable force_cast
    }
}

private let kTransitionSemaphore = DispatchSemaphore(value: 1)

extension AppDelegate {

    /// Animates changing the `rootViewController` of the main application.
    func show(_ viewController: UIViewController,
              animated: Bool = true,
              options: UIViewAnimationOptions = [.transitionCrossDissolve, .curveEaseInOut],
              completion: (() -> Void)? = nil) {

        guard let window = window else { return }

        if animated == false {
            window.rootViewController = viewController
            return
        }

        DispatchQueue.global(qos: .userInitiated).async {
            kTransitionSemaphore.wait()

            DispatchQueue.main.async {

                let duration = 0.35

                let previousAreAnimationsEnabled = UIView.areAnimationsEnabled
                UIView.setAnimationsEnabled(false)

                UIView.transition(with: window, duration: duration, options: options, animations: {
                    self.window?.rootViewController = viewController
                }, completion: { _ in
                    UIView.setAnimationsEnabled(previousAreAnimationsEnabled)

                    kTransitionSemaphore.signal()
                    completion?()
                })
            }
        }
    }
}

Этот код является полным примером, вы можете создать новый проект, очистить поле «Основной интерфейс», а затем поместить этот код в делегат приложения.

Результирующий переход:

enter image description here

0 голосов
/ 12 мая 2018

Поскольку вы закрываете RootViewController и обнуляете ссылку после выхода из системы, но экземпляр не освобождается, единственная другая возможность состоит в том, что что-то еще сохраняет ссылку на RootViewController. У вас, вероятно, есть цикл сохранения. Цикл сохранения происходит, если два объекта имеют сильную ссылку друг на друга. А поскольку объект не может быть освобожден до тех пор, пока не будут освобождены все его сильные ссылки, у вас возникает утечка памяти.

Примеры сохраняемого цикла:

    RootViewController *root = [[RootViewController alloc] init];
    AnOtherViewController *another = [[AnOtherViewController alloc] init];
    //The two instances reference each other
    root.anotherInstance = another;
    another.rootInstance = root; 

Или

    self.block = ^{
                //self is captured strongly by the block
                //and the block is captured strongly by the self instance
                NSLog(@"%@", self);
            };

Решение - использовать слабый указатель для одной из ссылок. Поскольку слабый указатель - это тот, который не сохраняет свою цель. например,

@property(weak) RootViewController *anotherInstance;

И

_typeof(self) __weak weakSelf = self
self.block = ^{
             _typeof(self) strongSelf = weakSelf
            //self is captured strongly by the block
            //and the block is captured strongly by the self instance
            NSLog(@"%@", strongSelf);
        };
0 голосов
/ 10 мая 2018

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

В вашем RootViewController

-(void)viewWillAppear:(BOOL)animated  
{  
  [super viewWillAppear:animated];  

  // Add observer
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldLogout:) name:@"shouldLogout" object:nil];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    // Remove observer by name
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"shouldLogout" object:nil];
}

Таким образом, вам не нужно думать о том, что ваш RootViewController находится в стеке или загружен из свежего и т. Д., Потому что реальная проблема с вашим наблюдателем.

0 голосов
/ 09 мая 2018

Во-первых, вы должны соблюдать «shouldLogOut» в viewDidLoad, как показано ниже:

    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(shouldLogout:) name:@"shouldLogout" object:nil];

и после этого в dismissViewControllerAnimated должно быть, как показано ниже:

[self dismissViewControllerAnimated:true completion:^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"shouldLogOut" object:nil];
    }];

вам нужно определить shouldLogOut: селектор в контроллере вида входа в систему

-(void)shouldLogOut:(NSNotification *) not{
    mainPlatformRootControler = nil;
}

Надеюсь, это поможет вам!

0 голосов
/ 09 мая 2018

Вы добавили наблюдателя уведомлений в viewDidLoad вашего LoginViewController, как показано ниже

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldLogOut:) name:@"shouldLogOut" object:nil];

Полагаю, вы пропустили это, тогда ваш класс входа не сможет получать уведомления после RootViewController отклонения.

0 голосов
/ 08 мая 2018

Поскольку вход в систему и выход из нее - это однократный процесс, поэтому после входа в систему вместо представления нового контроллера просто замените контроллер входа основным контроллером.

Давайте разберемся в этом: У вас есть основной делегат приложения с окном.

Код в didFinishLaunch:

if (loggedIn) {
     self.window = yourMainController
} else {
     self.window = loginController
}

Код в LoginController: LoginController будет иметь экземпляр AppDelegate, и после входа в систему вы должны изменить

appDelegate.window = mainController

Код в MainController: MainController будет иметь экземпляр AppDelegate, и после выхода из системы вам придется изменить

appDelegate.window = loginController

Надеюсь, это поможет !!

0 голосов
/ 03 мая 2018

Проблема , вероятно, заключается в том, что вы никогда не отклоняете RootViewController, когда произошел выход из системы. Установив для свойства mainPlatformRootControler значение nil, вы просто отказываетесь от владения объектом с точки зрения LoginViewController. Это ничего не говорит ни о чем другом, что также имеет ссылку на объект, стоящий за mainPlatformRootControler.

Чтобы исправить это , добавьте наблюдателя уведомлений в RootViewController для уведомления logout , а когда оно получено, отклоните себя через dismiss(animated:completion)

Бонус Вам также не понадобится свойство mainPlatformRootControler, если все, что вы делаете, это сохраняете его, чтобы обнулить. При правильном отклонении (как я писал выше) оно будет автоматически очищено, и, следовательно, не нужно беспокоиться о nil его удалении. (Теперь, если у вас есть другие причины для сохранения mainPlatformRootControler, очевидно, не удаляйте его).

...