How to implement UIViewController rotation in response to orientation changes? - iphone

My app has about 10 different UIViewControllers, just one of which I want to switch to landscape mode if the device is rotated. (All the rest, I want to keep in portrait.)
In order to implement rotation on that one view, I needed to implement its controller's 'shouldAutorotate' method and return YES. Since this view is accessed via a navigation controller, I also needed to create a subclass of UINavigationController that implements 'shouldAutorotate' and return YES.
This solution works, but too well. I find that all of the UIViewControllers I push onto my subclass of UINavigationController respond to rotation, even if I implement 'shouldAutorotate' and return NO. (Remember: I only want one particular UIViewController to respond to rotation, not every one in the navigation controller's stack.)
So, my question is: how do I best do this? All the solutions I can come up with seem 1) cumbersome, and 2) worse, don't seem to work.
Thanks very much.

when you implement UINavigationController, this class is your the parent that controls all the children viewControllers that will be pushed into the stack. Therefore, the RootViewController is the only controller that say Yes or No to autorotation. Even if, you are passing Yes to auto-rotation in children view controllers, they don't count!
This is the nature of UINavigationController. So to change it, you have two choices:
1- Manually manipulate it, which requires you to go through some cumbersome codes.
2- Change your design so that it is more compatible to UINavigationController. That single view that should rotate, should be get called by the RootViewController (not the Navigation Root View Controller -- they are named the same, but are totally different), the view that hosts NavController. And when the device rotates, it will either push the NavController to the view or the other.
3- The other method, which will also work, but is not recommended because it violates the concept of MVC, is that your NavController can listen to Notifications. That particular child view that CAN and SHOULD rotate can cast a notification -- e.g. rotateMe, and once the NavController hears it, it rotates.
As I said, it will work, but it violates the MVC model -- which is fine to Apple, but from programming perspective is not recommended.
If you need further explanation about either of them, please let me know.

I do this by having a root view controller (this could be a UITabBarController) and in it's viewDidLoad method i subscribe to rotation events:
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(didRotate:)
name:#"UIDeviceOrientationDidChangeNotification"
object:nil];
Then in the didRotate: method i look at which view controller is visible when the rotation happened, and what orientation the phone is in:
- (void) didRotate:(NSNotification *)notification {
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
/*
DEVICE JUST ROTATED TO PORTRAIT MODE
orientation == UIDeviceOrientationFaceUp ||
orientation == UIDeviceOrientationFaceDown
*/
if(orientation == UIDeviceOrientationPortrait) {
/*
DEVICE JUST ROTATED TO LANDSCAPE MODE
*/
}else if(orientation == UIInterfaceOrientationLandscapeLeft ||
orientation == UIInterfaceOrientationLandscapeRight) {
}
}
Within that didRotate: you can look at which is the visible viewController and do what you want from there.
I present a modal view controller in landscape mode when a particular view controller is visible and the phone is rotated into landscape. If any other view controller is visible, i ignore the event.
I force my modal view controller to display in landscape mode in its viewWillAppear method - i can give anyone this code if they want it.
Hope this helps.
Dave

Returns YES on the navigation controller subclass only if [[navController topViewController] isKindOfClass:[RotatingViewController class]]

I have seen examples using the way you are doing it but couldn't get it to work properly. I found the better way to do it from Apples examples. Basically you implement a presentModalViewController and create another view. UIKit does a basic rotation animation and fades between the view. You have to implement the rotated view as a delegate class so it can call back to its calling class to dismiss it and update the orientation.
- (void)orientationChanged:(NSNotification *)notification
{
// We must add a delay here, otherwise we'll swap in the new view
// too quickly and we'll get an animation glitch
NSLog(#"orientationChanged");
[self performSelector:#selector(updateLandscapeView) withObject:nil afterDelay:0];
}
And then to display a landscape screen:
- (void)updateLandscapeView
{
PortraitView *portraitView = [[PortraitView alloc] init];
portraitView.delegate = self;
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation) && !isShowingLandscapeView)
{
[self presentModalViewController: portraitView animated:YES];
isShowingLandscapeView = YES;
}
else if (deviceOrientation == UIDeviceOrientationPortrait && isShowingLandscapeView)
{
[self dismissModalViewControllerAnimated:YES];
isShowingLandscapeView = NO;
}
[portraitView release];
}

Related

Navigation Based App : only one landscape UIVIewController

still new to objective C and iOS i'm facing a new problem.
That's quite simple, i needed only one UIViewController in my app to be in landscape mode so i built my app this way :
One custom UINAvigationController : MyNavController set as "Is Initial View Controller" in my MainStoryBoard.storyboard. Then i add in this navController... :
- (NSUInteger)supportedInterfaceOrientations
{
return [[self topViewController] supportedInterfaceOrientations];
}
... to be able to control which UIVIewController childs of the custom nav controller can be in landscape orientation.
In the landscape UIVIewController i set :
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscapeLeft ;
return UIInterfaceOrientationMaskPortrait;
}
so i can get this controller landscape or not.
In this landscape controller in my nib (landscape.xib) i set a welcome view in UIPortrait mode and behind this view i had my landscape view. So when the user pushed the controller he sees that he has to turn his iPhone screen. I listen to orientation changing using :
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationChanged) name:UIDeviceOrientationDidChangeNotification object:nil];
In orientationChanged i hide the portrait view and then we can see the landscape view.
It works, but when you rotate fast it works very bad, sometime the view is hidden sometimes not. So there is my questions :
On a nav based app, what is the best way to make only one controller landscape capable ?
I use :
[self.navigationController pushNavigationController:landscapeController];
Is there a another way not to push the ladnscapeController so that it won't be navController dependant anymore ?
Thanls for all, any help will be welcome.
use this code befor you push next controller
[UIApplication sharedApplication].statusBarOrientation = UIInterfaceOrientationLandscapeRight;
use this code when pop to back controller
[UIApplication sharedApplication].statusBarOrientation = UIInterfaceOrientationIsPortrait(UIInterfaceOrientationMaskPortrait);

How to detect that device orientation is different from the interface orientation?

Initially a subclass of UINavigationController (Navigator) is a root controller and it supports all orientations.
The subclass overrides supportedInterfaceOrientations and provides properties to set what orientation is supported.
The root view controller of Navigators navigation stack (subclass of UITableViewContreller) controls supported orientations (depending which view controller is on top of a stack). It sets Navigators orientation properties in the didSelectRowAtIndexPath override.
If a transition is made when a device is in different orientation (because current view does not support it and this is not a supposed way to interact) and new view supports that device orientation, the view remains in different orientation than the device orientation.
Then one needs to rotate the device and move it back to bring a proper orientation.
This is if someone for some reason would hold a device in landscape mode in Contacts App, but suddenly one of it's subviews would support landscape and rotate automatically without rotating device to portrait and then landscape. The question is how to implement it?
Use this in every method:
if (([[UIDevice currentDevice] orientation] == UIDeviceOrientationLandscapeLeft) ||
([[UIDevice currentDevice] orientation] == UIDeviceOrientationLandscapeRight))
{
}
else
{
}
or check the [[UIScreen mainScreen] bounds]
I think this is want you want.
To get the device orientation:
[[UIDevice currentDevice] orientation];
To get the current orientation of the views showed:
[UIApplication sharedApplication].statusBarOrientation;
Add this method to your subclass of UINavigationController:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
return;
if ([UIViewController respondsToSelector:#selector(attemptRotationToDeviceOrientation)]) {
//present/dismiss viewcontroller in order to activate rotating.
UIViewController *mVC = [[UIViewController alloc] init];
[self presentModalViewController:mVC animated:NO];
[self dismissModalViewControllerAnimated:NO];
}
}
(Found it here on SF, but cannot find a link to that question)

In iOS6, trouble forcing ViewController to certain interfaceOrientation when pushed on stack

I have the following view controller set up:
viewController1 is able rotate freely to any orientation except portrait upside down.
viewController2 gets pushed on top of viewController1, and I'd like for it to be the same orientation viewController1 is and I'd like for it not to be able to rotate.
viewController3 gets pushed on top of viewController2. I'd like for viewController3 to be in portrait mode.
I'm having a lot of issues trying to accomplish this in iOS6 (haven't tried yet in iOS5). First off, I have already created my own Navigation Controller and put the following in it:
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [self.topViewController preferredInterfaceOrientationForPresentation];
}
- (NSUInteger)supportedInterfaceOrientations
{
return [self.topViewController supportedInterfaceOrientations];
}
- (BOOL) shouldAutorotate
{
return [self.topViewController shouldAutorotate];
}
I've tried a lot of different combinations of these things to know avail. Mainly where I'm struggling is forcing vc3 to be presented as portrait if vc2 is in landscape. Any help would be appreciated.
What you're trying to do here is fighting the framework. What you're describing is simply not how a navigation controller architecture works in iOS 6. If you want to show a view controller's view and force rotation, use a presented view controller. That's the only time preferredInterfaceOrientationForPresentation is meaningful, and your view controller's supportedInterfaceOrientations will actually be consulted because, being presented, it will be at the root of the interface.
I have explained in a different answer that it is not supported, in iOS 6, to force rotation when pushing a new view controller on to a navigation controller. You can structure rules about compensatory rotation (i.e. what should happen if the user rotates the device), but you can't force the interface to rotate. The only situation in which iOS 6 is happy to let you force rotation is when presenting or dismissing a view controller (presentViewController:animated: and dismissViewControllerAnimated:).
However, it is possible to use a presented view controller in such a way that it kind of looks like you're pushing onto the navigation controller. I've made a movie showing what I mean:
http://youtu.be/O76d6FhPXlE
Now, that's not totally perfect by any means. There is no rotation animation of the status bar, and there is a kind of black "blink" between the two views - which is intentional, because it's there to cover up what is really going. What's really going on is that there are really two difference navigation controllers and three view controllers, as shown in this screen shot of the storyboard.
What we have is:
a nav controller subclass set to portrait orientation, and its root view controller
a second nav controller subclass set to landscape orientation, and its root view controller, which is black and functions as an intermediary
a third view controller to be pushed onto the second nav controller's stack
When the user asks to go "forward" from the first view controller, we present the second navigation controller, thus seeing the black view controller momentarily, but then we immediately push the third view controller. So we get forced rotation, along with a kind of black flash and a push animation. When the user taps the Back button in the third view controller, we reverse the process.
All the transitional code is in the black view controller (ViewControllerIntermediary). I've tried to tweak it to give the most satisfying animation I can:
#implementation ViewControllerIntermediary {
BOOL _comingBack;
}
- (void) viewDidLoad {
[super viewDidLoad];
self.navigationController.delegate = self;
}
-(void)navigationController:(UINavigationController *)nc
willShowViewController:(UIViewController *)vc
animated:(BOOL)anim {
if (self == vc)
[nc setNavigationBarHidden:YES animated:_comingBack];
else
[nc setNavigationBarHidden:NO animated:YES];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (!_comingBack) {
[self performSegueWithIdentifier:#"pushme" sender:self];
_comingBack = YES;
}
else
[self.navigationController dismissViewControllerAnimated:YES
completion:nil];
}
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if ([self.window.rootViewController.presentedViewController isKindOfClass: [SecondViewController class]])
{
SecondViewController *secondController = (SecondViewController *) self.window.rootViewController.presentedViewController;
if (secondController.isPresented)
return UIInterfaceOrientationMaskAll;
else return UIInterfaceOrientationMaskPortrait;
}
else return UIInterfaceOrientationMaskPortrait;
}
And for Swift
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow) -> Int {
if self.window?.rootViewController?.presentedViewController? is SecondViewController {
let secondController = self.window!.rootViewController.presentedViewController as SecondViewController
if secondController.isPresented {
return Int(UIInterfaceOrientationMask.All.toRaw());
} else {
return Int(UIInterfaceOrientationMask.Portrait.toRaw());
}
} else {
return Int(UIInterfaceOrientationMask.Portrait.toRaw());
}
}
For more details check this link

Is it possible to have different orientations for viewControllers inside UINavigationController?

I want all view controllers to support only portrait mode, except one view controller lets call it "LandscapeSupportViewController" that should support also landscape mode.
The problem is when I'm in LandscapeSupportViewController in landscape mode and then push a new view controller that only support portrait mode, the pushed view controller will be in landscape mode also! How could I force it to be portrait?
I saw few apps that do it, take for example Skype iPhone app, the Messages tab is portrait only -> then if you press to enter the message itself you get a view controller that support landscape also because it make sense to enable landscape mode when user is chatting -> then if you press to view the persons profile, a new view controller will be pushed but in portrait! the same happen if you go back, you will forced to return to portrait even if you came from landscape...
Thanks
I'd had students try to accomplish exactly what you are trying to accomplish, and after much research, the general consensus is: this is a bad idea and requires a lot of (App Store legal) hacks to accomplish, and still doesn't turn out too pretty (status bar, for example, screws up). You'll notice in the Skype app that when you go into the IM section, rotate to landscape, and hit back, the UI "snaps", or sort of gets instantly reloaded.
This is not a good user experience, and I'd recommend rethinking your design to be more in line with what Apple recommends.
If i got you correctly you want to change device orientation in some conditions.
[[UIApplication sharedApplication] setStatusBarOrientation:UIDeviceOrientationPortrait animated:NO];
set your own orientation using above line, just put this lines inside the if condition. condition is depends on you.
Thank you!!
Write this lines before you push viewController which supported only portrait From landscapeViewController
[appdel.navigationController.view removeFromSuperview];// This navcontroller used with rootviewcontroller
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait];
[ [UIApplication sharedApplication].self.delegate.window addSubview:appdel.navigationController.view];
self.navigationController.navigationBar.hidden=NO;
Here is a solution.
You can add a category for UINavigationController which manages the view controller orientation. See code below:
#interface UINavigationController (MyViewOrientations)
#end
#implemetation UINavigationController (MyViewOrientations)
- (BOOL)supportLandscapeModeForViewController:(UIViewController *)controller {
return [controller isKindOfClass:[LandscapeSupportViewController class]]
}
- (NSUInteger)supportedInterfaceOrientation {
UIViewController *controller = [self visibleViewController];
NSUInteger orientationMasks = UIInterfaceOrientationMaskPortrait
if([self supportLandscapeModeForViewController:controller]) {
orientationMasks |= UIInterfaceOrientationMaskLandscapeLeft;
orientationMasks |= UIInterfaceOrientationMaskLandscapeRight;
}
return orientationMasks;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
UIViewController *controller = [self visibleViewController];
if([self supportLandscapeModeForViewController:controller]) {
return UIInterfaceOrientationLandscapeLeft; // Your call
}
else {
return UIInterfaceOrientationPortrait;
}
}
- (BOOL)shouldAutorotate {
UIViewController *controller = [self visibleViewController];
return [self supportLandscapeModeForViewController:controller];
}
#end
If the situation is more complex, different views support different orientations. You can override "supportedInterfaceOrientation", "preferredInterfaceOrientationForPresentation", "shouldAutorotate" in your view controllers, and delegate calls from UINavigationController category code with "visibleViewController".

Iphone: Cannot switch back from my landscape view to a portrait view

I am working on an app (my first one), which is basically a TabBar app.
To be more precise there are:
- a login view controller
- a tab bar controller (when login is done)
- a landscape view controller that is used when the first itel of the TabBar is switch from Portrait to Landscape.
So, when I am in the first tab, I need to be able to move to landscape view to display some other data. In my tab bar controller, I have implemented those methods:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
if([self selectedIndex] == 0)
return YES;
return NO;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Get AppDelegate
MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
// Remove TabBarView and add graph Landscape View
[self.view removeFromSuperview];
[delegate setSubViewLandscapeViewController];
}
}
In the delegate, I have implemented the setSubViewLandscapeViewController and the setSubViewTabBarController:
- (void)setSubViewTabBarViewController {
[window addSubview:[tabBarController view]];
}
- (void)setSubViewGraphLandscapeViewController {
[window addSubview:[landscapeViewController view]];
}
I want the landscapeViewController to display only in landscape mode, I have then (in my landscapeViewController):
- (void)viewWillAppear:(BOOL)animated {
[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationLandscapeRight];
}
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationLandscapeRight);
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
NSLog(#"willRotateToInterfaceOrientation");
}
A part of this works fine, I mean the switch from portrait to landscape is ok (when I am in the first tab), the tabbarcontroller is remove from the SuperView and the landscape view is added as a subview instead.
The thing is... I do not know how to switch back to portrait mode (and then load the previous controller, the tabBar one using the setSubViewTabBarViewController of my delegate). It seems none of the willRotateToOrientation, willRotateFromOrientation, .. are triggered when I actually move the device from the landscape view...
In short, when I am in the landscape view I do not know what to do to move back to the tabbar view... I am kind of stuck in the landscape view once I am in this one.
Thanks a lot for your help,
Luc
Look at the pie chart in CPTestApp-iPhone in the examples folder. It handles rotation by implementing -(void)didRotateFromInterfaceOrientation: and resizing the graph after a rotation.
Well, I managed to get a solution for this problem.
In fact, while moving from portrait to landscape I removed the tabbarcontroller from window subview and add the landscapeviewcontroller instead.
It seems it was not the correct thing to do.
Instead, I add the landscapeViewController as subview of the tabbarcontroller and remove it when going from landscape to portrait.
I still have a problem however with the y position of the landscape view which seems to changes when I do several decive rotation in a row....
Regards,
Luc