Im using the following code to adjust the height and frame of my view when flipping landscape/portrait (to compensate for hiding status bar):
-(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{
UIInterfaceOrientation toInterfaceOrientation = (UIInterfaceOrientation)[[UIDevice currentDevice]orientation];
if((toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)&&
!UIInterfaceOrientationIsLandscape(self.interfaceOrientation)){
NSLog(#"Going LandScape");
//self.view.frame = CGRectOffset(self.view.frame, 0, -20);
self.view.frame = CGRectMake(0, -20, self.view.frame.size.width, self.view.frame.size.height+20);
}
if(toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown){
NSLog(#"Going Portrait");
//self.view.frame = CGRectOffset(self.view.frame, 0, 20);
self.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height-20);
}
}
It works fine on my first view controller when loading the app. But whenever I present a new (or the same) view controller, it doesn't move up the frame when going landscape. I get the correct logs as for when going portrait or landscape, and obv I have the same code on all my view controllers. Can someone tell me whats going on? (note presenting the same view controller will still give me issues when going to landscape even though this was working initially)
EDIT:
To make things even worse, I tried adding a button in the if(landscape) statement. The button will be drawed initially, but whenever I re-present the view controller, it doesn't even draw the button. I think I'm losing my mind...
It seems that the first time i rotate to landscape, it works, no matter what VC I'm on. but then reloading any vc, will make it not work.
Solution is pretty simple in my opinion: The function viewWillTransitionToSize is getting called only after the orientations is changed so you should make helper class that will somehow manage rotation or check the orientation on view did load.
You can simply test my statement by adding breakpoint in the function.
This line of code needs to be added at start of viewWillTransitionToSize:
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
(and damn that was painful to realise what an awfully simple mistake I did (but it normally is like that:) ) .... been pulling hairs out for long with this problem)
Related
I know that this looks like a huge question but it's not so don' be afraid to read it. Mostly it's me explaining how stuff works.
I have two UIWindows in my app. The first one is the main window which gets created by the app by default. The second one is called modalWindow and is also created in the app delegate.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.modalWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.modalWindow.windowLevel = UIWindowLevelStatusBar;
//Other stuff I need
//....
return YES;
}
Most of my app is portrait only but there is one view controller where the user can switch to landscape orientation. I'm listening for landscape change via:
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
This works fine and in the orientationChanged method I present the landscape view controller. All is well at this point. If I rotate back to portrait I get the orientationChanged fired once more at which point I dismiss the landscape view controller.
Now let's explain how the second window comes into play. When in landscape mode there is an action (just a button press) which presents a new view controller on the second window (the modalWindow). Ok let's explain this.
The app delegate has two methods which look like so:
- (void)presentActivityOnModalWindow:(UIViewController *)viewController
{
self.modalWindow.rootViewController = viewController;
self.modalWindow.hidden = NO;
}
- (void)modalTransitionFinished:(NSNotification *)notif
{
self.modalWindow.hidden = YES;
self.modalWindow.rootViewController = nil;
}
I call the presentActivityOnModalWindow via the [[[UIApplication sharedApplication] delegate] performSelector....]. I know it isn't the best practice but it works fine. As for the dismissal I use NSNotificationCenter to post the notification about dismissal. The view controller that gets presented on the modalWindow is supporting only portrait mode. It's returning YES in shouldAutorotate and UIInterfaceOrientationMaskPortrait in supportedInterfaceOrientations.
Ok a lot of explaining but now we get to the problem. When I rotate to landscape, pop up the modalWindow, dismiss the modal window and rotate back to portrait my main window is still in landscape mode and everything looks catastrophic.
I tried adding an observer to both window and modalWindow and logging changes in frame and bounds and here are the results:
Did finish launching:
window frame {{0, 0}, {320, 568}}
window bounds {{0, 0}, {320, 568}}
modalWindow frame {{0, 0}, {320, 568}}
modalWindow bounds {{0, 0}, {320, 568}}
Rotate to landscape:
window frame {{0, 0}, {568, 320}}
Open activity in landscape:
modalWindow frame {{0, 0}, {320, 568}}
modalWindow frame {{0, 0}, {320, 568}}
Dismiss activity:
none
Rotate back to portrait:
none
So as it seems my window does not get back to normal frame when we get back to portrait mode. How to fix this? If I need to provide any more details feel free to ask.
The system will only handle rotation of your keyWindow. If you have other windows you'll have to handle rotation yourself.
I also think that modal controllers is the way to go. But if you really want to handle rotations take a look at how other "custom windows" libraries handle rotation. Alert views are a great example:
https://github.com/search?o=desc&q=uialertview&s=stars&type=Repositories&utf8=✓
Apple does not encourage using windows in this manner. It is probably best to use an UIViewController displayed modally in stead of your
self.modalWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
Presenting an UIViewController modally is done from an UIViewController instance using
[self presentViewController:<#(UIViewController *)#> animated:<#(BOOL)#> completion:<#^(void)completion#>]
You are creating 2 windows and Apple recommends against this. An app should really have as many windows as there are display devices.(SEE UIWindow reference )
I would suggest using 2 views and "drawing" everything onto those views according to the orientation. Then make one view hidden, when the other is displayed.
Alternatively you could just use 1 view and make adjustments at the time of orientation changes. The ease of doing this depends on the type of information being displayed.
Orientation changes are best detected by AppDelegate code.
Here is a sample of code I use to detect an orientation change and then a check for the new dimensions of the window.
- (void)application:(UIApplication *)application didChangeStatusBarOrientation:(UIInterfaceOrientation)oldStatusBarOrientation
{
//other stuff from rotate:
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
CGRect appFrame = [[UIScreen mainScreen ]applicationFrame];//using frame so status bar is not part of calculations.
if (orientation == UIInterfaceOrientationLandscapeRight
||
orientation == UIInterfaceOrientationLandscapeLeft
)
{
self.currentAppScreenWidth = appFrame.size.height;
self.currentAppScreenHeight = appFrame.size.width;
[YOURapp reArrangeSubViews_and_Layouts: appFrame];//Something like this - maybe.
}
else
{
self.currentAppScreenWidth = appFrame.size.width;
self.currentAppScreenHeight = appFrame.size.height;
[YOURapp reArrangeSubViews_and_Layouts: appFrame];//Something like this - maybe.
}
}
Hope that helps.
One other thing. If you are displaying the same info with a different layout, then you should be able to lay it out using Xcode tools, to adapt to orientation in a suitable way. If certain things are still not perfect, then try to tweak using code.
If you are displaying completely different information, the 2 view approach might be the easiest.
[Maybe some more information about the differences in the data being displayed on the 2 screens, if the above solution does not work for you.
Cheers.]
I have had this problem countless times and cannot figure out how to fix it. I am working in an Xcode project (empty project - NO XIB's!). I have my orientation set to landscape:
But this keeps happening:
The view is being cutoff. No matter what I do it doesn't seem to set to the proper size. For some reason it is displaying the view in landscape using portrait bounds. Does anyone know how to fix this? I also want to restrict the orientation to ONLY landscape.
UPDATE
The view does not get cutoff if I hard-code 1024 as the width and 768 as the height. This is obviously a terrible solution, but I cannot figure it out. Does anyone out there know of a solution?
Check the rooViewController that you are setting in application:didFinishLaunchingWithOptions: in app delegate class. Make sure you are returning proper allowed orientations in this view controller class whose object you are setting to rootViewController:
- (NSUInteger) supportedInterfaceOrientations{
return UIInterfaceOrientationMaskLandscape|UIInterfaceOrientationLandscapeRight;
}
- (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation{
return UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
}
In your app delegate add this function :
- (NSUInteger) application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
return UIInterfaceOrientationMaskLandscape|UIInterfaceOrientationLandscapeRight;
}
I have the answer! One of my friends helped me out with this one.
Views are not oriented until they appear, so, if you are going to add any components to the view and expect them to adhere to an orientation other than the default, which I suspect is portrait, you must add those components in the -(void)viewDidAppear:(BOOL)animated. I was calling methods that added several components to the view from within the viewDidLoad method, however, the view has not appeared, and the orientation not set, when that method is called. Moving my initialization code into the viewDidAppear method fixes my problem.
Here is an example:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidLoad];
//Probably dont want to draw stuff in here, but if you did, it would adhere to the
//correct orientation!
CAShapeLayer *layer = [CAShapeLayer layer];
layer.backgroundColor = [UIColor blueColor].CGColor;
layer.anchorPoint = CGPointMake(0, 0);
layer.bounds = CGRectMake(0, 0, self.view.bounds.size.width, 300);
[self.view.layer addSublayer:layer];
//Call methods from here
[self initializeScrollView];
[self addItemToScrollView];
[self addGraphToView];
}
I am now testing an app for Ipad. Basically I am using template for master detail application and have another portraitViewController. Now when application starts in portrait mode I want it to display only portraitViewController and when device is rotated e.g landscape mode I want to display only master-detailViewController. What is the best way to do this.
I was testing sample code for single view application but master-detail view refuses to hide:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)
interfaceOrientation duration:(NSTimeInterval)duration {
if (interfaceOrientation == UIInterfaceOrientationPortrait) {
self.view = self.portrait;
self.view.transform = CGAffineTransformIdentity;
self.view.transform =
CGAffineTransformMakeRotation(degreesToRadians(0));
self.view.bounds = CGRectMake(0.0, 0.0, 320.0, 460.0);
}
else if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
self.view = self.landscape;
self.view.transform = CGAffineTransformIdentity;
self.view.transform =
CGAffineTransformMakeRotation(degreesToRadians(−90));
self.view.bounds = CGRectMake(0.0, 0.0, 480.0, 300.0);
}
else if (interfaceOrientation ==
UIInterfaceOrientationLandscapeRight) {
self.view = self.landscape;
self.view.transform = CGAffineTransformIdentity;
self.view.transform =
CGAffineTransformMakeRotation(degreesToRadians(90));
self.view.bounds = CGRectMake(0.0, 0.0, 480.0, 300.0);
} }
The view controller should not be changing its view in real time like this. You're not seeing anything happening because the view controller's old view is still in the interface; you're not removing it from the interface. Nor should you do so. The way to make that happen is to swap out the view controller; use a different view controller and a different view.
You started with the master-detail view controller, so the app's root view controller is the UISplitViewController. It's view (the split view), it appears, is the view you want to remove. So you will have to replace the UISplitViewController as the window's rootViewController.
But that's a huge pain in the butt. I think you might be happier just putting a modal (presented) view controller in front of everything when in portrait orientation.
This is a downloadable example project that presents a view controller in response to device rotation:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch19p609rotationForcingModalView
Is it possible to get the animation properties (speed and easing curve) for a built in iOS system animation? Specifically, the UIStatusBarAnimationSlide when you hide or show the status bar. Right now I'm just eyeballing it and have come up with a good match: .35 seconds using the default animation curve. This works fine, but Apple is liable to change an animation like this in a future iOS update and it would be nice to match it exactly and not rely on hard-coded values I came up with myself.
For what it's worth, here is the method my view controller is calling when I tap the view to hide the status bar and resize the view to fill the screen.
-(void)tappedView:(UIGestureRecognizer *)gestureRecognizer
{
UIApplication *app = [UIApplication sharedApplication];
// First, toggle the visibility of the status bar
[[UIApplication sharedApplication] setStatusBarHidden:![app isStatusBarHidden] withAnimation:UIStatusBarAnimationSlide];
// Then scale this view controller's view, attempting to match the built-in
// UIStatusBarAnimationSlide animation
[UIView animateWithDuration:.35
animations:^{
self.view.frame = [UIScreen mainScreen].applicationFrame;
}];
}
As an aside, I'm surprised I couldn't find a built in way to handle resizing a VC's view when the status bar is hidden. After all, if the status bar doubles its height when a call is in progress, the view resizes automatically. Tell me I'm missing something and there's a way to get the view to grow automatically, too.
Here's a chuck of code I use in my app:
- (void)application:(UIApplication *)application willChangeStatusBarFrame:
(CGRect)oldStatusBarFrame {
[UIView animateWithDuration:0.355f animations:^{
if(floating_point_values_are_equal(oldStatusBarFrame.size.height, 20.0f)) {
for(UIViewController* VC in self.tabBarController.viewControllers) {
UIView* view = VC.view;
[view setTransform:CGAffineTransformMakeScale(1.0f, 1.0f)];
}
} else {
for(UIViewController* VC in self.tabBarController.viewControllers) {
UIView* view = VC.view;
CGFloat ratio = (view.frame.size.height - 20) / view.frame.size.height;
[view setTransform:CGAffineTransformMakeScale(1.0f, ratio)];
}
}
}];
}
It basically scales the entire app depending on the new screen dimensions. It only works because the scale ratio is not a big change- doing this for the new iPhone screen would not look right.
I created a UIViewController (based on How to switch views when rotating) to switch between 2 views when the device rotates. Each view is "specialized" for a particular orientation.
It uses the UIDeviceOrientationDidChangeNotification notification to switch views:
-(void) deviceDidRotate: (NSNotification *) aNotification{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
NSLog(#"Device rotated to %d!", orientation);
if ((orientation == UIDeviceOrientationPortrait) ||
(orientation == UIDeviceOrientationPortraitUpsideDown)) {
[self displayView:self.portraitViewController.view];
}else if ((orientation == UIDeviceOrientationLandscapeLeft) ||
(orientation == UIDeviceOrientationLandscapeRight)) {
[self displayView:self.landscapeViewController.view];
}
}
and sort of works. The problems shows up when I rotate to Landscape and then back to Portrait. When going back to portrait the subviews aren't displayed in the right place, specially the UIPickerView:
First Time Portrait:
Rotate to Landscape:
Back to Portrait:
If I repeat the rotation process, things just get worse. What am I doing wrong?
The source code is here: http://dl.dropbox.com/u/3978473/forums/Rotator.zip
Thanks in advance!
To solve your offset problems, rewrite the displayView: method as below.
-(void) displayView: (UIView *)aView{
self.view = aView;
}
Rotations however are strange. you should review that part of code.
Use the UIViewController rotation methods
(void)willRotateToInterfaceOrientation:duration:
(void)didRotateFromInterfaceOrientation:
instead of -(void)deviceDidRotate:
Much simpler, you will avoid that strange bouncing, and you don't need notifications any more.
Do some reading on the apple documentation on the methods i specified above.
Hope this helps.
OK, I found the error. It's pretty simple and stupid: I mixed frame and bounds.
In the displayView: code I was setting the frame of the child view to the frame of the parent view, when it should be the bounds of the parent.