3

UPDATE 2

In my UITabBarController subclass I tried adding this:

-(void) tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {

        NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
        [[UIDevice currentDevice] setValue:value forKey:@"orientation"];

}

Now, everytime I select a tab item, the device rotates to portrait mode perfectly. However, now I can rotate the device while (a) is selected, and the device will rotate to landscape mode. How can I stop the device from rotating?

I believe this method in my tab controller subclass is what is causing this:

- (BOOL)shouldAutorotate {

    if(self.selectedIndex == 0)
    {
        return NO;
    }

    return [self.viewControllers.lastObject shouldAutorotate];
}

If I return 'NO' I cannot rotate when I'm in the view controller, but when I select it, it does not automatically rotate to portrait. If I return 'YES' I can rotate when I'm in the view controller, but when I select it, it automatically rotates to portrait.


I have a custom Tab Bar Controller in my app with the following hierarchy:

UITabBarController
    |
    UINavigationController
    |  |
    |  UIViewController(a)
    |
    UINavigationController
    |  |
    |  UIViewController(b)
    |
    UINavigationController
       |
       UIViewController(c)

I want View Controller (a) to only be able to be viewed in portrait mode, and View Controllers (b) and (c) to be viewable in all orientations but upside down. Now, I can do this with each view controller individually, but my issue comes in when I am in (b) in landscape mode, and select the tab item for (a), a is then displayed in landscape mode, which does not look good. How can I make sure the tab bar (or the view controller) checks to see if the to-be-selected view controller can be viewed in the current orientation?

If needed, here is my code for (a) that restricts it to portrait mode on its own:

- (BOOL) shouldAutorotate
{
    return NO;
}

- (NSUInteger) supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

UPDATE

I have added this to view controller (a), but I get a black square in the middle of my view, and the title in the nav bar is no longer centered.

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];


    NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
    [[UIDevice currentDevice] setValue:value forKey:@"orientation"];


}
Jacob
  • 2,338
  • 2
  • 22
  • 39

3 Answers3

7

Since iOS 7.0 UITabBarController supports orientation forwarding via - (NSUInteger)tabBarControllerSupportedInterfaceOrientations:(UITabBarController *)tabBarController.

It works great until you switch to the other tab while being at interface orientation that this particular tab does not support.

Unfortunately tabbar controller does not force the rotation in that case and the only way is to use private API.

Note: I use attemptRotationToDeviceOrientation to rotate interface to device orientation when switching back from portrait only tab to tab that supports any orientation.

This is my approach to problem:

static const NSInteger kPortraitOnlyTabIndex = 1;

@interface TabBarController : UITabBarController<UITabBarControllerDelegate>
@end

@implementation TabBarController

- (id)initWithCoder:(NSCoder *)aDecoder {
    if(self = [super initWithCoder:aDecoder]) {
        self.delegate = self;
    }
    return self;
}

- (NSUInteger)tabBarControllerSupportedInterfaceOrientations:(UITabBarController *)tabBarController {
    if(tabBarController.selectedIndex == kPortraitOnlyTabIndex) {
        return UIInterfaceOrientationMaskPortrait;
    }

    return UIInterfaceOrientationMaskAllButUpsideDown;
}

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
    [UIViewController attemptRotationToDeviceOrientation];

    if([self.viewControllers indexOfObject:viewController] == kPortraitOnlyTabIndex) {
        SEL sel = NSSelectorFromString(@"setOrientation:");
        if([[UIDevice currentDevice] respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [[UIDevice currentDevice] performSelector:sel withObject:(__bridge id)((void*)UIInterfaceOrientationPortrait)];
#pragma clang diagnostic pop
        }
    }
}

@end

I put example app on Github at https://github.com/pronebird/TabBarOrientationExample

pronebird
  • 12,068
  • 5
  • 54
  • 82
  • 1
    Thank you for your answer, but this does not answer my question. – Jacob Nov 28 '14 at 19:14
  • @Jacob in what way it does not answer your question? You use wrong enum, switch to right one. – pronebird Nov 28 '14 at 19:17
  • 1
    I don't see how changing my return type in one view controller affects the rotation in another when it is selected in the tab controller. – Jacob Nov 28 '14 at 21:22
  • @Jacob if you make tabbar forward rotation events to child controllers then UIKit will take the desired orientation from child controller instead of tabbar itself. Second, you can try using `[UIApplication setStatusBarOrientation:animated:]` in `viewWillAppear:` of controller A to force a particular orientation. – pronebird Nov 28 '14 at 23:18
  • Also, it seems your question is a duplicate of http://stackoverflow.com/questions/2689598/forcing-uiinterfaceorientation-changes-on-iphone – pronebird Nov 28 '14 at 23:24
  • I have changed the enum type as you suggested, but the problem still persists. I have also tried changing the orientation manually in view controller (a) but that does not work either. Any ideas? – Jacob Nov 29 '14 at 21:56
  • @Jacob I'll take a look on a sample app. – pronebird Nov 30 '14 at 16:10
  • @Jacob I have updated my answer. While it's very easy to define supported orientations for different tabs, it's hard to enforce orientation if you enter tab while being in unsupported orientation by this particular tab. – pronebird Dec 01 '14 at 16:45
  • Thank you for the very detailed answer and explanation. I downloaded the sample project and tried to implement it in my app, and I think I found the root of the problem. Before my tab controller is shown, I have a login view controller embedded in a navigation controller. After login, the tab controller is presented using a show (push) segue, and this is what allows the portrait only view controller to be able to rotate. I have added something like this to the sample project to test, and the second view does indeed rotate in that scenario. How can I fix this? Thanks again for helping! – Jacob Dec 01 '14 at 17:34
  • You're welcome! You can use similar approach with navigation controller, see its delegate protocol. You can forward orientation events to `topViewController` and get the right orientation for the whole interface. The logic depends on you, be creative :-) – pronebird Dec 01 '14 at 21:30
  • The other option you may consider is to present tabbar modally and avoid complicated relationship between your controllers. Thankfully you can use custom transitions on iOS 8 and make a smooth meaningful transition. – pronebird Dec 01 '14 at 21:34
  • Can I present the tab bar modally but make the animation similar to a push segue's? – Jacob Dec 01 '14 at 21:56
  • You can mimic this, CoreAnimation supports push animation, you can implement custom transition or custom segue to achieve that. – pronebird Dec 01 '14 at 22:06
2

I used a workaround once for this exact problem. The idea was to subclass UITabBarController and set its delegate to itself. Then in a subclass I did a following "hack": when the portrait-only controller was selected, I pushed and immediately dismissed an empty modal VC. It forced iOS to set the correct orientation somehow.

-(void) tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
    UIInterfaceOrientation currentOrientation = [UIApplication sharedApplication].statusBarOrientation;
    NSInteger currentViewControllerSupportsLandscape = ([viewController supportedInterfaceOrientations] & UIInterfaceOrientationMaskLandscape);
    if(UIInterfaceOrientationIsLandscape(currentOrientation) &&  !currentViewControllerSupportsLandscape) {
        //workaround to force rotating to portrait
        UIViewController *c = [[UIViewController alloc]init];
        [viewController presentViewController:c animated:NO completion:nil];
        [viewController dismissViewControllerAnimated:NO completion:nil];
    }
}

Note that it's probably dependent on some implementation detail and can possibly stop working in the future.

EDIT: You should also implement supportedInterfaceOrientations and shouldAutorotate in a UITabBarController subclass. And you should return YES in -shouldAutorotate in the view controller you want to stay in portrait. Otherwise it won't rotate from landscape to portrait when selected in tab bar.

- (NSUInteger) supportedInterfaceOrientations {
    return [self.currentViewController supportedInterfaceOrientations];

}

- (BOOL) shouldAutorotate {
    return [self.currentViewController shouldAutorotate];
}

- (UIViewController*) currentViewController {
    UIViewController* controller = self.selectedViewController;
    if([controller isKindOfClass:[UINavigationController class]]) {
        controller = [((UINavigationController*)controller).viewControllers objectAtIndex:0];
    }
    return controller;
}
Michał Ciuba
  • 7,876
  • 2
  • 33
  • 59
  • This does not work for me, only the status bar rotates to the portrait position... – Jacob Nov 29 '14 at 22:01
  • Maybe it's because you return `NO` in `shouldAutorotate`. Look at edited answer. – Michał Ciuba Nov 30 '14 at 13:22
  • As I said in my edited question, if I return YES in shouldAutorotate, I can simply rotate the device while the view controller is selected, and it will rotate to landscape mode instead of strictly staying in portrait. – Jacob Nov 30 '14 at 15:22
  • It shouldn't rotate if you return proper value in `supportedInterfaceOrientations` implemented in this VC. At least it works for me (the VC stays in portrait after rotating the device to landscape). – Michał Ciuba Nov 30 '14 at 16:28
  • It still rotates when I rotate the device after I have made your suggested changes. – Jacob Nov 30 '14 at 21:53
0

Its simple, Create subclass of UITabBarController, and implement delegate method

- (BOOL)tabBarController:(UITabBarController *)tbController shouldSelectViewController:(UIViewController *)viewController {
    //Check for orientation here
    if (supported orientation for view controller == [[UIApplication sharedApplication] statusBarOrientation]) {
        return YES;
    } else {
        return NO;
    }
}
Abhishek
  • 1,682
  • 2
  • 17
  • 29
  • This will not let the user select the tab still, though. Is there a way I can check for the orientation, rotate the device if the current orientation is not supported,can't then return yes? – Jacob Nov 28 '14 at 13:56