How to present a view controller in portrait or landscape, in application that is portrait-only

MusiGenesis picture MusiGenesis · Nov 10, 2014 · Viewed 10.3k times · Source

I have an iPhone application most of which is portrait-only, but with one view controller (a video player screen) that has to support both portrait and landscape (this is for iOS 8 only). To achieve this, I have set the app's plist to support portrait and both kinds of landscape, then subclassed UINavigationController; in this subclass I override

- (BOOL)shouldAutorotate {
    return YES;
}

and

- (NSUInteger)supportedInterfaceOrientations {

    if ([self.visibleViewController isKindOfClass:[MyVideoPlayerScreen class]]) {
        return UIInterfaceOrientationMaskAllButUpsideDown;
    } else {
        return UIInterfaceOrientationMaskPortrait;
    }

}

This mostly works as expected: the initial screens are all portrait-only, and remain in portrait even when the device is turned to landscape. The video player, when initially presented:

MyVideoPlayerScreen *newVC = [MyVideoPlayerScreen new];
[self.navigationController pushViewController:newVC animated:YES];

is also in portrait but will then rotate to landscape when the device is turned - all good so far.

The problem is that if I turn the device to landscape, the video player goes landscape as well, but then when I dismiss the video player screen via the back button, the underlying view controller (which is supposed to be portrait-only) is now also in landscape. If I rotate the device back to portrait the view controller rotates back to portrait as well, and it is then correctly locked in portrait-only from that point on.

How can I get the original view controller (which is supposed to be portrait-only) to automatically go back to portrait when the landscape view controller above it is popped?

This question has been asked a million times, but it seems that the fixes that were posted for it are all hacks that don't work in iOS 8 any more.

Update: I have found a "sort-of" fix for this that does work in iOS 8. In my UINavigationController subclass, I handle the <UINavigationControllerDelegate> protocol. I implemented this method:

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {

    if (![viewController isKindOfClass:[MyVideoPlayerScreen class]]) {

        // if the current orientation is not already portrait, we need this hack in order to set the root back to portrait
        UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
        if (orientation != UIInterfaceOrientationPortrait) {

            // HACK: setting the root view controller to nil and back again "resets" the navigation bar to the correct orientation
            UIWindow *window = [[UIApplication sharedApplication] keyWindow];
            UIViewController *vc = window.rootViewController;
            window.rootViewController = nil;
            window.rootViewController = vc;

        }

    }

}

This at least leaves the UI in the state I want it to be in. The problem is that when the top-level view controller is dismissed and animated off-screen, the underlying view controller is still in landscape; then it suddenly jumps to portrait. Not ideal, but better than nothing.

Update 2: I should have added that I am pushing the second view controller like so:

ViewControllerPortraitLandscape *newVC = [ViewControllerPortraitLandscape new];
[self.navigationController pushViewController:newVC animated:YES];

Update 3: OK, this is totally doable, in a way that works for iOS 6 and up. The fix even kind of makes sense, although the reason for it's working does not seem to be in the documentation anywhere. Basically, in the view controller that you need to be reset to portrait when the top-level view controller is dismissed while the device is still in landscape, you just need to add this:

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

Although my subclass of UINavigationController is entirely responsible for the rotation calls, breakpoints show that supportedInterfaceOrientations is called on the underlying view controller just before the top-level is dismissed, and it's called on the navigation controller after the top-level is dismissed. So I'm inferring that this call to the view controller itself is made by iOS in order to determine what orientation the underlying view controller should be in (and it does not ask the nav controller for this); if it's not explicitly overridden it will return the all-but-upside-down parameter, so iOS just leaves it where it is, in landscape.

Answer

MusiGenesis picture MusiGenesis · Nov 11, 2014

Turns out this is an easy fix: you can drive the entire process via a subclass of UINavigationController as I posted here (i.e. not implementing any of these rotation methods in the view controllers themselves), except that for any view controller that needs to be portrait-only, you also need to implement this:

- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

With breakpoints, you can see that this method is called before the pushed view controller above it is dismissed; iOS presumably uses this call to determine what orientation the "revealed" view controller should be in (the same call to the navigation controller subclass is called after the top-level view controller is dismissed).

With this method implemented in the view controller, everything works as expected.