3
votes

I want to present a modal view controller (for a login screen) when my app launches, and also when it becomes active again after a user has hit the home button and then relaunched the app.

I first tried to present the modal view in the root view controller's viewDidAppear: method. That works great when the app first launches, but this method isn't called when the app becomes active again.

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self presentModalView];
}

- (void)presentModalView {
    if(![AuthenticationService sharedInstance].isAuthenticated) {
        _modalVC = [self.storyboard instantiateViewControllerWithIdentifier:self.modalViewControllerIdentifier];
        _modalVC.delegate = self;
        [self presentViewController:_modalVC animated:YES completion:nil];
    }
}

Next I tried to call this from my app delegate in the applicationDidBecomeActive: method.

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.

    ModalPresentingUISplitViewController *splitViewController = (ModalPresentingUISplitViewController *)self.window.rootViewController;
    [splitViewController presentModalView];
}

This appears to work fine on the surface, but I get a Unbalanced calls to begin/end appearance transitions for <ModalPresentingUISplitViewController: 0x7251590> warning in my log. I get the sense that I'm somehow presenting the modal view before the UISplitView is finished presenting itself, but I don't know how to get around this.

How can I "automatically" present a modal view from my root view controller when the app becomes active, and do it at the "right" moment as not to unbalance my split view controller?

3

3 Answers

1
votes

Forgot this question was here. Yes, I have a solution. I can't help but feel that there is a more elegant or right way to do this, but this worked for me...

This assumes you are using ARC and storyboards; You've created a UIViewController for your login view with a modal segue from the UISplitViewController (or whatever your root view controller is).

UISplitViewController (or whatever your root view controller is)

- (id)initWithCoder:(NSCoder *)aDecoder {
    if(self = [super initWithCoder:aDecoder]) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(presentModalView) name:UIApplicationDidBecomeActiveNotification object:nil];
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}

- (void) viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.viewHasAppeared = YES;
    [self presentModalView];
}

- (void) presentModalView {
    if(self.viewHasAppeared && !self.userAuthenticated) {
        [self performSegueWithIdentifier:@"ShowLoginView" sender:self];
    }
}

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if([[segue identifier] isEqualToString:@"ShowLoginView"]) {
        JDPLoginViewController *dest = [segue destinationViewController];
        dest.delegate = self;
    }
}

- (void) dismissLogin {
    self.userAuthenticated = YES;
    [self dismissViewControllerAnimated:YES completion:nil];
}

Here are the important parts of the code to note...

  1. We're calling presentModalView two places - in viewDidAppear which will take care of presenting our login view when the app first starts and
  2. We are registering the presentModalView as an observer to the UIApplicationDidBecomeActiveNotification event so the method gets called when the app becomes active after being in the background.
  3. Finally, we're creating a BOOL property viewHasAppeared on the UISplitViewController to keep track of whether the UISplitViewController's view has appeared or not so we don't try to present the modal login before the UISplitViewController's view has appeared.

Here are the different scenarios...

App First Starts:

  • presentModalView is called by the UIApplicationDidBecomeActiveNotification event, but since the UISplitViewController's view isn't loaded (and the viewHasAppeared BOOL is NO, nothing happens. Win. We don't present the view when we shouldn't.
  • Then eventually viewDidAppear is called, it sets viewHasAppeared to YES and then calls presentModalView. The login screen is presented. Everything works as expected - Yay!

App Becomes Active After Being in Background

  • presentModalView is called by the UIApplicationDidBecomeActiveNotification event again as in the first scenario, but this time viewHasAppeared is YES, so login view is presented as expected. Yay again!

Like I said, this feels kind of ugly, but it gets the job done until I find a better solution. Hope it works for you.

0
votes

Have you tried UIView's viewWillAppear?

0
votes

Chaining off @jpolete's answer I did things a little differently. In addition I wanted the login screen to only appear after the app has been in the background for more the 15 seconds (it's painful for the user to always have to log back in).

The source code for this demo can be found on github

Like @jpolete, I encapsulated most of the logic in the root view controller, which is a navigation controller in my case (iPhone example). The userLoggedIn flags whether or not the user has been authenticated. The presentingLoginController flag lets me know if the login screen is currently presented. backgroundTime holds a time stamp of when the user entered the background. Here is the class extension:

@interface RootNavigationController () <LoginDelegate>
@property (assign, nonatomic) BOOL userLoggedIn;
@property (strong, nonatomic) NSDate *backgroundTime;
@property (assign, nonatomic) BOOL presentingLoginController;
-(void)applicationDidBecomeActive:(NSNotification*) notification;
-(void)applicationDidEnterBackground:(NSNotification*) notification;
@end

When the view is loaded I add the appropriate notification hooks:

@implementation RootNavigationController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationDidBecomeActive:)
                                                 name:UIApplicationDidBecomeActiveNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(applicationDidEnterBackground:)
                                                 name:UIApplicationDidEnterBackgroundNotification
                                               object:nil];
}

-(void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

Here we trigger the login segue if the user is not authenticated and we are not currently presenting the login controller.

-(void)loginIfNecessary {
    if (!self.userLoggedIn && !self.presentingLoginController) {
        self.presentingLoginController = YES;
        [self performSegueWithIdentifier:@"RootLoginSegue" sender:self];
    }
}

Here we set the root view controller to be the loginDelegate of the login controller. This delegate is informed when a successful login occurs (the login controller is embedded in another nav controller):

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"RootLoginSegue"]) {
        UINavigationController *navController = segue.destinationViewController;
        LoginTableViewController *loginController = (LoginTableViewController *) navController.topViewController;
        loginController.loginDelegate = self;
    }
}

When a successful login occurs we do the following:

-(void)didLogin { // LoginDelegate method called to login controller after successsful login
    self.presentingLoginController = NO;
    self.userLoggedIn = YES;
}

When the view appears for the first time, when it appears after being in the background, or after being "covered up" we login (if needed):

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self loginIfNecessary];
}

When we enter the background we record the time:

-(void)applicationDidEnterBackground:(NSNotification*) notification {
    self.backgroundTime = [NSDate date];
}

When we enter the foreground and its the first time or sufficient time has passed then we force the user to log in again (if necessary):

-(void) applicationDidBecomeActive:(NSNotification*) notification {
    const NSTimeInterval maxBackgroundTime = 15.0;
    if (!self.backgroundTime || [[NSDate date] timeIntervalSinceDate:self.backgroundTime] > maxBackgroundTime) {
        self.userLoggedIn = NO;
    }
    [self loginIfNecessary];
}

@end