I'm having a hard time understanding why the following is happening (and how to fix it).
I've created an application using the split-view based application.
I've added a UiBarButtonItem called showTheModal which calls this method found in RootViewController.m:
- (IBAction)showTheModal:(id)sender {
theModalController.modalPresentationStyle = UIModalPresentationFullScreen;
theModalController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:theModalController animated:YES];
if ([detailViewController popoverController] != nil)
[[detailViewController popoverController] dismissPopoverAnimated:YES];
The BarButtonItem of course, is shown at the bottom of the Default Root Controller (left side of the of the split view in landscape) or at the bottom of the popup (if in landscape).
The modal view is dismissed by a button placed in a toolbar. It calls the following:
[self dismissModalViewControllerAnimated: YES];
The problem I'm having is if rotate the screen, while the modal is up. Here is what happens in different scenarios (start refers to the orientation when the showTheModal button is hit, end refers to the orientation when I hit the dismissModal button).
1)Start landscape, end landscape: Everything appears fine. willHideViewController and willShowViewController methods are not called in the RootViewController (as expected)
2) Start landscape, end portrait: UI appears fine. willHideViewController is run TWICE (WHY?)
3) Start portrait, end portrait: UI appears fine. willHideViewController is run once (as expected)
4) Start portrait, end landscape: The 'Root List' button remains in the detail view (right side of the split view. Neither willHideViewController and willShowViewController are invoked (WHY??)
Any thoughts as to why #2 and #4 don't behave quite the expected way?
I've had exactly the same problem (#4, above). I worked around it using viewDidAppear:animated, and then checking the height of the view to see if it is in landscape vs. portrait. (Yuck, gag, etc.) I'm not satisfied at all with that "solution".
Possibly related: I've noticed that the button in portrait mode is slow to disappear after rotating to landscape, i.e. the button appears for a second after the rotation finishes. However, in Mail.app, the "Inbox" button disappears as soon as the rotation starts. Is Apple doing things differently than they recommend in their docs? Perhaps there is a more efficient way to show/hide the master view button?
Unfortunately this is not a bug. It appears to be an expected behavior.
I found this in iOS Release Notes for iOS 5.0, in "Notes and Known Issues" section:
Rotation callbacks in iOS 5 are not applied to view controllers that
are presented over a full screen. What this means is that if your code
presents a view controller over another view controller, and then the
user subsequently rotates the device to a different orientation, upon
dismissal, the underlying controller (i.e. presenting controller) will
not receive any rotation callbacks. Note however that the presenting
controller will receive a viewWillLayoutSubviews call when it is
redisplayed, and the interfaceOrientation property can be queried from
this method and used to lay out the controller correctly.
For diagnostics, have you tried dismissing the popover view first? Or logging who is calling the method by printing (id) sender?
I was having the same exact problem.
In answer to (2), it appears to be a bug. I noticed that when a modal view is pushed over a splitview, the orientation messages are queued up somewhere and not processed until the modal view is dismissed and the splitview is visible but I would still expect to only get one callback.
For (4), this too appears to be a bug. Fortunately, the didRotate... events still get through, so my solution was to subclass UISplitViewController and explicitly call the delegate's willShowViewController method in this case:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
//Work around a bug where UISplitViewController does not send
//willShowViewController after a modal is presented in portrait
//but dismissed in landscape.
UIInterfaceOrientation orientation = self.interfaceOrientation;
if ( (orientation == UIInterfaceOrientationLandscapeLeft )
|| (orientation == UIInterfaceOrientationLandscapeRight) )
{
UINavigationItem* item = [detail.navigationBar.items objectAtIndex:0];
UIBarButtonItem* barButtonItem = [item leftBarButtonItem];
[super.delegate splitViewController:self willShowViewController:master invalidatingBarButtonItem:barButtonItem];
}
}
Here, "master" is an IBOutlet that refers to the master view controller (left hand side) of the splitview and "detail" is an IBOutlet for the detail view controller (right hand size).
Note that in my case, the detail view is a UINavigationController. You may require different code to get the barButtonItem from your view controller.
Also, this has the side-effect of calling willShowViewController twice for normal rotation, but that is not an issue in my case.
I think that this is a bug that needs to be reported to Apple Development.
I worked around part of this issue by presenting my modal view using the UIModalPresentationPageSheet format.
Related
I have been trying to get multiple orientations to work with a single view controller. Currently it checks for the device orientation and view controller. Then switches based on whether it's landscape or portrait. The problem is that it works fine in portrait, but since it pushes another view on the stack whenever it's in landscape the back button links to the portrait view instead of the actual screen we want to get back to (which is one further step away).
if (UIDeviceOrientationIsLandscape(deviceOrientation) &&
self.navigationController.visibleViewController == self)
{
self.landscapeViewController =
[self.storyboard instantiateViewControllerWithIdentifier:#"view_landscape"];
[self.navigationController pushViewController:self.landscapeViewController
animated:NO];
}
else if (UIDeviceOrientationIsPortrait(deviceOrientation) &&
self.navigationController.visibleViewController == self.landscapeViewController)
{
[self.navigationController popViewControllerAnimated:NO];
}
I cant present the landscape view controller in modal fashion, since there is a navigation controller involved.
Another thing is that I'm instantiating the same view controller for each orientation (using the same class but linking to different identifiers in the storyboard).
The thing you're trying to do is REALLY bad and goes against Apples way of doing things.
There's something called Autolayout, with which you can design a single view to work both in landscape and portrait mode.
It is possible you can handle programatically or simply use auto-layout depends on your requirement .just prefer this LINK
I've noticed that most consumer-friendly Android and iPhone fitness apps have two interface modes - in portrait mode the user gets more detailed information, but when the user turns the device to landscape mode, a full screen graph is added to cover the entire screen.
I'm interested in how to implement transition to a different view controller in response to device rotation on iPhone. My initial thoughts are to intercept (willRotateToInterfaceOrientation event, then get the app delegate and add a full screen graph view controller to the window).
Is there a better way of turning an iPhone rotation into a transition to another view controller? Like hiding the status bar and pushing a modal view controller in landscape mode with animation?
First ask yourself whether you really need a separate view controller. One view controller can easily hide or unhide a graph.
If this graph needs its own view conroller then you could use a container view that contains the graph which refers to its own view conroller. That is what container views are made for.
The "Master" view controller then would just hide and unhide the container view in response to rotation events (and layout them accordingly etc. pp.)
If you prefer to add or remove the container view from its super view (most probably self.view from the "Master" view controller's point of view) then do that instead of hiding and unhiding. That is probably most appropriate.
The upside of this appoach would be that it works regardless of the navigaiton structure you are in, regardless of whether the rotated view controller was pushed or presented modally, regardless of whether you are in a tab bar driven app or a single view app, whether you are using storyboard, works with IB as well as programmatically, etc. pp.
There is nothing wrong with fetching the window instance from the app's delegate. I just don't see the need for doing so. Seems rather complicated to me compared to the alternatives.
The willRotateToInterfaceOrientation method works well.
In addition to switching views, two other useful things you might want to do in there are:
1) Hide/Show the status bar. (I like to hide it in landscape)
[[UIApplication sharedApplication] setStatusBarHidden:UIInterfaceOrientationIsLandscape(toInterfaceOrientation) withAnimation:UIStatusBarAnimationSlide];
2) Hide/Show any UINavigationBar. (Maybe your landscape view will benefit from the extra height)
[self.navigationController setNavigationBarHidden:UIInterfaceOrientationIsLandscape(toInterfaceOrientation) animated:YES];
You could have one view controller that has the willRotateToInterfaceOrientation method, and that viewcontroller has two other viewcontrollers as variables.
Once the device rotates, you switch the viewcontrollers' views (very crude code example:)
-(void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration {
if ((orientation == UIInterfaceOrientationLandscapeLeft) || (orientation == UIInterfaceOrientationLandscapeRight)) {
[self.secondViewController.view removeFromSuperView];
self.firstViewController.view.frame = self.bounds;
[self.view addSubView:self.firstViewController.view];
} else {
[self.firstViewController.view removeFromSuperView];
self.secondViewController.view.frame = self.bounds;
[self.view addSubView:self.secondViewController.view];
}
}
We used following code to align one of screen in landscape mode
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationLandscapeLeft);
}
It shows as expected in 5.1 simulator(in landscape), but shows in portrait mode
in iPad. Pl suggest
This may be the problem that Filip refers to.
However, another problem I've noticed with real hardware — even without iOS 6 involved — is that the ordering is slightly different.
If you are trying to a modal view controller from a view controller before it's fully processed its own rotation, the modal view controller will appear in portrait mode. The first view controller hasn't fully handled its own rotation until events are processed for it.
In other words, if you try to present a modal view controller from an early event in a view controller (such as viewWillAppear) it will always show up in portrait mode.
To fix this, instead of presenting the view controller immediately, just schedule it to the main loop using a block.
Change the line that invokes the view controller, which might look something like this:
[self performSegueWithIdentifier: #"firstRun" sender: self];
To:
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier: #"firstRun" sender: self];
});
If you're using another method to present the new view controller, try the same approach with it: wrap it in a dispatch_async to the main queue so it's done later.
As has been reported in other questions here on SO, iOS 5 changes how rotation callbacks for split view controllers are sent as per this release note. This is not a dupe (I think), as I can't find another question on SO that deals with how to adjust split view controller usage in iOS 5 to cope with the change:
Rotation callbacks in iOS 5 are not applied to view controllers that
are presented over a full screen. What this means is that if your code
presents a view controller over another view controller, and then the
user subsequently rotates the device to a different orientation, upon
dismissal, the underlying controller (i.e. presenting controller) will
not receive any rotation callbacks. Note however that the presenting
controller will receive a viewWillLayoutSubviews call when it is
redisplayed, and the interfaceOrientation property can be queried from
this method and used to lay out the controller correctly.
I'm having trouble configuring the popover button in my root split view controller (the one that is supposed to show the left pane view in a popover when you're in portrait). Here's how my app startup sequence used to work in iOS 4.x when the device is in landscape mode:
Install split view controller into window with [window addSubview:splitViewController.view]; [window makeKeyAndVisible];. This results in splitViewController:willHideViewController:withBarButtonItem:forPopoverController: being called on the delegate (i.e. simulating a landscape -> portrait rotation) even though the device is already in landscape mode.
Present a fullscreen modal (my loading screen) which completely covers the split view underneath.
Finish loading and dismiss the loading screen modal. Since the device is in landscape mode, as the split view controller is revealed, this causes splitViewController:willShowViewController:invalidatingBarButtonItem: to be called on the delegate (i.e. simulating a portrait -> landscape rotation), thereby invalidating the bar button item, removing it from the right-side of the split view, and leaving us where we want to be. Hooray!
So, the problem is that because of the change described in that release note, whatever happens internally in iOS 4.3 that results in splitViewController:willShowViewController:invalidatingBarButtonItem: being called no longer happens in iOS 5. I tried subclassing UISplitViewController so I could provide a custom implementation of viewWillLayoutSubviews as suggested by the release note, but I don't know how to reproduce the desired sequence of internal events that iOS 4 triggers. I tried this:
- (void) viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
UINavigationController *rightStack = [[self viewControllers] objectAtIndex:1];
UIViewController *rightRoot = [[rightStack viewControllers] objectAtIndex:0];
BOOL rightRootHasButton = ... // determine if bar button item for portrait mode is there
// iOS 4 never goes inside this 'if' branch
if (UIInterfaceOrientationIsLandscape( [self interfaceOrientation] ) &&
rightRootHasButton)
{
// Manually invoke the delegate method to hide the popover bar button item
[self.delegate splitViewController:self
willShowViewController:[[self viewControllers] objectAtIndex:0]
invalidatingBarButtonItem:rightRoot.navigationItem.leftBarButtonItem];
}
}
This mostly works, but not 100%. The problem is that invoking the delegate method yourself doesn't actually invalidate the bar button item, so the first time you rotate to portrait, the system thinks the bar button item is still installed properly and doesn't try to reinstall it. It's only after you rotate again to landscape and then back to portrait has the system got back into the right state and will actually install the popover bar button item in portrait mode.
Based on this question, I also tried invoking all the rotation callbacks manually instead of firing the delegate method, e.g.:
// iOS 4 never goes inside this 'if' branch
if (UIInterfaceOrientationIsLandscape( [self interfaceOrientation] ) &&
rightRootHasButton)
{
[self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[self willAnimateRotationToInterfaceOrientation:self.interfaceOrientation duration:0];
[self didRotateFromInterfaceOrientation:self.interfaceOrientation];
}
However this just seems to cause an infinite loop back into viewWillLayoutSubviews :(
Does anyone know what the correct way to simulate the iOS4-style rotation events is for a split view controller that appears from behind a full-screen modal? Or should you not simulate them at all and is there another best-practices approach that has become the standard for iOS5?
Any help really appreciated as this issue is holding us up from submitting our iOS5 bugfix release to the App Store.
I don't know the right way to handle this situation. However, the following seems to be working for me in iOS 5.
In splitViewController:willHideViewController:withBarButtonItem:forPopoverController:, store a reference to the barButtonItem in something like self.barButtonItem. Move the code for showing the button into a separate method, say ShowRootPopoverButtonItem.
In splitViewController:willShowViewController:invalidatingBarButtonItem:, clear that self.barButtonItem reference out. Move the code for showing the button into a separate method, say InvalidateRootPopoverButtonItem.
In viewWillLayoutSubviews, manually show or hide the button, depending on the interface orientation
Here's my implementation of viewWillLayoutSubviews. Note that calling self.interfaceOrientation always returned portrait, hence my use of statusBarOrientation.
- (void)viewWillLayoutSubviews
{
if (UIInterfaceOrientationIsPortrait(
[UIApplication sharedApplication].statusBarOrientation))
{
[self ShowRootPopoverButtonItem:self.barButtonItem];
}
else
{
[self InvalidateRootPopoverButtonItem:self.barButtonItem];
}
}
I am creating an application with Landscape Right orientation. For that I set the Initial interface orientation property in info.plist file. Then in every view I handled
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return(interfaceOrientation==UIInterfaceOrientationLandscapeRight);
}
It works fine in simulator but in device its behave differently.
My first view is in proper orientation. There is is popover which display another view that comes in portrait mode. Still my status bar is in Landscape Right..
For navigating from one view to another view I am using..
self.window.rootViewController = self.myNav;
I have multiple navigation Controller and adding those using the upper code.
I am not getting what is the problem.
Any help will be appreciated.
EDIT: I had used
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
I never get this issue in simulator but getting this in device and not every time. I have used Supported interface orientations (iPad) too and set Landscape (right home button) value for item0.
Thanks In advance
You need to set the "Simulated Metrics > Orientation" property of your top view (in all your xib files) to be "Landscape". The default is portrait.
The question was answered pretty well here - Landscape Mode ONLY for iPhone or iPad .
I also have an app that like yours only supports UIInterfaceOrientationLandscapeRight. I haven't run into any orientation issues so far. I only have one UIViewController under the window. This UIViewController has its own UITabBar that I use to change pages. So, we change pages differently. I set my UIViewController using the rootViewController property of the window just like you, but again I only have one.
Also, I never had to do anything like the [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications] call that you included. Since you only support LandscapeRight orientation, you shouldn't care to be notified of changes.
EDIT
I created a sample project, which I can post if necessary, and I think I may know your problem. First - do you only encounter the sizing issue inside popovers? If so, then I don't think orientation is throwing you off but the popover itself. My sample project has 3 view controllers. The first loads the second by changing the window's rootViewController. That worked fine. The second view controller has a button to open a popover. This will show up wrong unless you set the contentSizeForPopover on the view controller as shown below:
- (IBAction) showVC3InPopover
{
UIViewController * vc3 = [[VC3 alloc] init];
/** Set the contentSizeForViewInPopover property to determine how
large the view will be in the popover! You can do this in the
init method(s) of the view controller if you prefer, it's just
easy to do here */
vc3.contentSizeForViewInPopover = vc3.view.frame.size;
[_popover release], _popover = nil;
_popover = [[UIPopoverController alloc] initWithContentViewController:vc3];
[vc3 release];
[_popover presentPopoverFromRect:_showPopoverButton.frame inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
See if this fixes your problem. If it does, there are other ways to control the popover size, but this is just what I typically do. However, just know that the PopoverController ignores the size of the view in the viewcontroller you load.
How many views (or viewControllers) you have? You might need to implement this orientation logic in all those views??