How to correctly manage memory stack and view controllers?
Asked Answered
G

7

6

I'm really struggling with this basic iOS programming stuff but I just can't figure out whats happening and how to solve it.

I have my main Login controller that detects when a user is logged in and presents next controller if auth succeed:

@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:^{

    }];

}

And that works well, no problem.

My trouble is handling logout. How do I delete completely the RootViewController instance and show a new one?

I can see that RootViewController instances are stacking cause I have multiple observers and after a logout and then login they are called multiple times (as many times I exit and re-enter).

I've tried the following with no success:

First detecting logout in RootViewController and dismissing:

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

            }];

And then in LoginViewController:

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

So how can I handle this? I know its a basic memory handle stuff but I just don't know how?

Gatha answered 3/5, 2018 at 17:20 Comment(3)
Why do you need to keep mainPlatformRootControler on LoginViewController? If RootViewController is presented from LoginViewController, you can get loginViewController from RootViewController by using self.presentingViewController and after that call a method without using notificationEisele
"I can see that RootViewController instances are stacking cause I have multiple observers " This is wrong, notificationcenter does not retain observers..Pilate
please provide the code where you add and "remove" RootViewController from your navigation stackPilate
E
0

The problem is likely that you are never dismissing the RootViewController when logout has happened. By setting the property mainPlatformRootControler to nil, you are just relinquishing ownership of the object from the perspective of LoginViewController. That says nothing about anything else that also owns a reference to the object behind mainPlatformRootControler.

To fix this add a notification observer inside RootViewController for the logout notification, and when that's received, dismiss itself via dismiss(animated:completion)

Bonus You also don't need the property mainPlatformRootControler if all you are doing is saving it around to nil it out. By properly dismissing it (in the manner I wrote above), it will automatically be cleaned up, and thus don't need to worry about niling it out either. (Now if you have other reasons for keeping mainPlatformRootControler around, then don't delete it obviously).

Eliseelisee answered 3/5, 2018 at 17:23 Comment(1)
Andy Im sorry I completely forgot to say Im dismissing the RootViewController inside it. I've updated my answerImpudence
M
1

First, you have to observe "shouldLogOut" in viewDidLoad should be like below:

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

and after that in dismissViewControllerAnimated should be like below:

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

you need to define shouldLogOut: selector in login view controller

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

Hope this will help you!

Mephitic answered 9/5, 2018 at 20:29 Comment(0)
E
0

The problem is likely that you are never dismissing the RootViewController when logout has happened. By setting the property mainPlatformRootControler to nil, you are just relinquishing ownership of the object from the perspective of LoginViewController. That says nothing about anything else that also owns a reference to the object behind mainPlatformRootControler.

To fix this add a notification observer inside RootViewController for the logout notification, and when that's received, dismiss itself via dismiss(animated:completion)

Bonus You also don't need the property mainPlatformRootControler if all you are doing is saving it around to nil it out. By properly dismissing it (in the manner I wrote above), it will automatically be cleaned up, and thus don't need to worry about niling it out either. (Now if you have other reasons for keeping mainPlatformRootControler around, then don't delete it obviously).

Eliseelisee answered 3/5, 2018 at 17:23 Comment(1)
Andy Im sorry I completely forgot to say Im dismissing the RootViewController inside it. I've updated my answerImpudence
D
0

Because login and logout is the one-time process, so after login, instead of presenting new controller just replace login controller with main controller.

Let's understand this: You have main application delegate with window.

Code in didFinishLaunch:

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

Code in LoginController: LoginController will have instance of AppDelegate, and after login, you have to change

appDelegate.window = mainController

Code in MainController: MainController will have instance of AppDelegate, and after logout, you have to change

appDelegate.window = loginController

I hope this helps !!

Dwightdwindle answered 8/5, 2018 at 10:6 Comment(0)
V
0

Did you add Notification observer in viewDidLoad of your LoginViewController look like below

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

I guess you missed this, then your login class can not receive notification after RootViewController dismissed.

Vanbuskirk answered 9/5, 2018 at 3:9 Comment(0)
S
0

As you have said there are multiple observer creates issue, then you must have to remove your observer when you don't need it.

In your 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];
}

So in this way you don't have to think about your RootViewController is in stack or it is loaded from fresh etc. Because actual problem is with your observer.

Sino answered 10/5, 2018 at 2:55 Comment(0)
U
0

There are many correct ways to manage view hierarchies, but I'll share one way I have found to be simple and affective.

Basically, I swap out the primary UIWindow's rootViewController at log out/in. Additionally, I programmatically provide the rootViewController rather than letting @UIApplicationMain to load the initial view controller. The benefit of doing this is that during app launch, if the user is logged in, then the Login.storyboard never has to be loaded.

The show function can be configured to suite your style, but I like cross dissolve transitions as they are very simple.

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

This code is a complete example, you can create a new project, clear out the "Main Interface" field, and then put this code in the app delegate.

The resulting transition:

enter image description here

Unschooled answered 10/5, 2018 at 12:13 Comment(0)
E
0

Since you are dismissing the RootViewController and you nil the reference after logout but the instance is not released, the only other possibility is that something else is keeping a reference to the RootViewController. You probably have a retain cycle. A retain cycle happens if two objects have a strong reference to each other. And because an object cannot be deallocated until all of its strong references are released, then you have a memory leak.

Examples of retain cycle include:

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

Or

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

The solution is to use a weak pointer for one of the references. Since a weak pointer is one that does not retain its target. e.g.

@property(weak) RootViewController *anotherInstance;

And

_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);
        };
El answered 12/5, 2018 at 8:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.