Skip to content

Instantly share code, notes, and snippets.

@chrishulbert
Created May 25, 2011 05:00
Show Gist options
  • Save chrishulbert/990371 to your computer and use it in GitHub Desktop.
Save chrishulbert/990371 to your computer and use it in GitHub Desktop.
Finding the currently visible view in a UITabBarController
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Firstly, we want to figure out who is the currently visible view controller.
// There are 4 possibilities here:
// 1- They could be a non-nav controller, with a visible tab (ie not under the 'more' tab)
// Simple: self.selectedViewController
// 2- They could be a nav controller, with a visible tab (ie not under the 'more' tab)
// Get it's visible view: self.selectedViewController.visibleViewController
// 3- They could be a non-nav controller, under the 'more' tab
// Simple: self.selectedViewController
// 4- They could be a nav controller, under the 'more' tab
// This is where it gets tricky (or buggy, really? You decide.)
// What UIKit does is: its sets the self.selectedViewController to your nav controller, but steals the root view and
// pushes it onto the self.moreNavigationController's stack
// So if your self.selectedViewController.viewControllers is empty, use self.moreNavigationController.visibleViewController
UIViewController *visible = self.selectedViewController; // For non-nav controllers
if ([visible respondsToSelector:@selector(visibleViewController)]) // For nav controllers
visible = [((UINavigationController*)visible) visibleViewController];
if (!visible) // Exception for nav controllers under the more tab
visible = self.moreNavigationController.visibleViewController;
return [visible shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
@jdandrea
Copy link

jdandrea commented May 1, 2012

This has been dogging me for years now. I'm revisiting it yet again, and what I'm now seeing is that I can push a ViewController onto a NavigationController stack (itself part of a TabBarController), and I make sure that VC only allows Portrait. Then I push a VC that allows rotation to all but upside-down Portrait. If I am in Landscape and pop the stack, my previous VC shows up Landscape even though it only allows Portrait! Ensuring I followed this advisory didn't help much. Ever have this happen to you (and, if so, what was the trick to get around it)?

@chrishulbert
Copy link
Author

I've had exactly the same problem as you, and this is how we solve it. In the 'viewWillAppear' of the lower-level view that only supports portrait, we do the following:

    if (self.interfaceOrientation != UIInterfaceOrientationPortrait) {
        // http://stackoverflow.com/questions/181780/is-there-a-documented-way-to-set-the-iphone-orientation
        // http://openradar.appspot.com/radar?id=697
        // [[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait]; // Using the following code to get around apple's static analysis...
        [[UIDevice currentDevice] performSelector:NSSelectorFromString(@"setOrientation:") withObject:(id)UIInterfaceOrientationPortrait];
    }

Works a treat. Basically it forces the app to re-orient to portrait. We've gotten this accepted into the app store.

@jdandrea
Copy link

jdandrea commented May 2, 2012

Ahh, yes, good call. Why am I still paranoid about using it? Perhaps if I sneak in respondsToSelector: I'll be content.

Hmph. Cast of 'int' to 'id' is disallowed with ARC. NSInvocation, here I come ... <!>

This is not nearly as elegant, but ARC is happy with it. If only I could make this a class extension with a method like xyz_setInterfaceOrientation: or something similar. (This code won't work though - [UIDevice methodSignatureForSelector:NSSelectorFromString(@"setOrientation:")] returns nil. Sigh.)

if (self.interfaceOrientation != UIInterfaceOrientationPortrait) {
    // http://stackoverflow.com/questions/181780/is-there-a-documented-way-to-set-the-iphone-orientation
    // http://openradar.appspot.com/radar?id=697
    // [[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait];
    // Using the following code to get around apple's static analysis (ARC compatible)
    SEL message = NSSelectorFromString(@"setOrientation:");
    UIDevice *device = [UIDevice currentDevice];
    if ([device respondsToSelector:message]) {
        NSMethodSignature *signature = [UIDevice methodSignatureForSelector:message];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

        [invocation setTarget:device];
        [invocation setSelector:message];

        UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;

        [invocation setArgument:&orientation atIndex:2]; // 0 is for target, 1 is for selector
        [invocation invoke];
    }
}

@chrishulbert
Copy link
Author

chrishulbert commented May 2, 2012 via email

@jdandrea
Copy link

jdandrea commented May 8, 2012

OK. I started over, just using your code and your code alone.

Let's say I have view A (portrait only) and I push B onto the nav controller stack (any orientation).

At this point, if I change orientation to landscape then, when I pop the VC stack on my Nav controller, visible ends up being UISnapshotModalViewController. My actual VC (the one being popped to) is left in the lurch, and it ends up in landscape (which is wrong). However, if I then rotate to portrait, it switches back to and remains in portrait (as it should).

The interesting (twisted?) part is that the pop animation acts AS IF it's portrait, but the view shows up in landscape. If I do another push, the animation acts as if it's in landscape (matching the actual device orientation).

UPDATE: If I use topViewController, I get the proper controller. If I use visibleViewController, I get that UISnapshotModalViewController. So using top instead of visible helps a lot. MEANWHILE, in the More nav controller, the list of items is really the UIMoreListController (which I also don't want to be rotated). That's always at the top of the more nav controller, so now I have this test:

if (vc == self.moreNavigationController || vc == [[self.moreNavigationController viewControllers] objectAtIndex:0]) {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

I'm still unable to have select VCs in the more controller allow rotation though. At least nothing broke in the other VCs though, and I don't need that force rotation step now. Still ... so close!

@chrishulbert
Copy link
Author

Maybe put my code into a separate helper class, and disable ARC on just that file (-fno-objc-arc), that'd be a simple approach.

@jdandrea
Copy link

jdandrea commented May 9, 2012

Yup, that's exactly what I did, and it works! Meanwhile, I'm running into trouble with the code in this gist (I know, finally back on topic - ha!), but only when it comes to the More controller. Here's what I'm using now in order to avoid running into UIMoreListController and UISnapshotModalViewController:

 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
     // For non-nav controllers
     UIViewController *vc = self.selectedViewController;

     // For nav controllers
     if ([vc isKindOfClass:[UINavigationController class]]) {
        UINavigationController *navController = (UINavigationController *) vc;
        vc = navController.topViewController;
     }

     // Exception for nav controllers under the more tab
     // NOTE: With the More tab, I have yet to see vc be nil here. Hmm.
     if (!vc) {
        vc = self.moreNavigationController.topViewController;
     }

     // OPTIONAL: If this is the More navigation controller or the first view 
     // on its stack (UIMoreListController), keep it at portrait.
     if (vc == self.moreNavigationController ||
         [vc isKindOfClass:NSClassFromString(@"UIMoreListController")]) {
         return (interfaceOrientation == UIInterfaceOrientationPortrait);
     }

     return [vc shouldAutorotateToInterfaceOrientation:interfaceOrientation];
 }

@chrishulbert
Copy link
Author

Its funny you should mention it, somewhere on my blog i wrote a post on how to find the active VC when the 'more' tab is selected. Its there...somewhere...

Anyway sounds like you've licked all your problems. Congrats.

@jdandrea
Copy link

Thanks! I really appreciate your patience and willingness to indulge me here in the comments.

As for that blog post, yep, that's how I arrived here. It's this one, right?

The thing that's got me confused now is that visibleController shouldn't work (at least certainly not on iOS 5, with those two private VCs that Apple exposes in there), but that's what you use in your code ... unless Apple changed something subtle as of iOS 5.

@chrishulbert
Copy link
Author

chrishulbert commented May 10, 2012 via email

@jdandrea
Copy link

Thanks, and you're 100% correct - I want to lose that More tab so badly, you have no idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment