Reputation: 2668
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?
Upvotes: 6
Views: 229
Reputation: 1688
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);
};
Upvotes: 0
Reputation: 6622
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.
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:
Upvotes: 0
Reputation: 473
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.
Upvotes: 0
Reputation: 162
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!
Upvotes: 1
Reputation: 87
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.
Upvotes: 0
Reputation: 3394
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 !!
Upvotes: 0
Reputation: 12842
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 nil
ing it out either. (Now if you have other reasons for keeping mainPlatformRootControler
around, then don't delete it obviously).
Upvotes: 0