Multiple Presented View Controllers and Rotation on iPad - objective-c

I have recently run into an issue when porting some code from an iPhone app over to the iPad. It may be related to the issue described here, though I found the explanations/solutions of that question unsatisfactory. The scenario is this:
View controller 'A' (The Root View Controller) presents a modal view controller (call it 'B') with the "Form Sheet" modal presentation style.
View controller B presents view controller 'C' with the "Full Screen" modal presentation style.
The iPad is rotated while view controller C is the top-most presented view controller.
Upon dismissal of C, B is re-displayed, but has the incorrect orientation.
As far as I can tell, there should not be an issue with chaining of multiple presented view controllers -- in fact, this behavior is explicitly supported in the Presenting View Controllers from Other View Controllers documentation. I have also read the following statement in the iOS 5 Release Notes:
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.
As far as I can tell, this does not occur -- View controller B receives a call to -shouldAutoRotateToInterfaceOrientation but the value the interfaceOrientation parameter in this call is the value of view controller B's interface orientation when it presented view controller C, not the value of C's interface orientation upon dismissal. Since we're on an iPad, all these view controllers return YES in -shouldAutoRotateToInterfaceOrientation. Thus the bounds of B's view never change, so -willLayoutSubviews is never called.
I have tried saving the orientation of view controller C in a callback to B before B dismisses it, and then using that information the next time -shouldAutoRotateToInterfaceOrientation is called and returning YES only for the orientation of C when it is dismissed. This fixes, the broken UI that appears without making this check, but view controller B does not update its interface orientation to this value, so subsequent modal presentations will animate in/out from the wrong side of the device.
Has anyone been able to successfully get a view controller configuration like this working? It doesn't seem like that unusual of a scenario, so I am a little surprised that it isn't working as I initially expected it to.
Thanks in advance.

In my opinion multiple chained modal view controllers result in a confusing and annoying user experience if you don't use a navigation controller. I think View controller B should be in a navigation controller (you don't have to show the nab bar if you don't want).
Modal presentation is really supposed to be for single dead-ended entities (a single view controller or a navigation controller containing multiple children view controllers).
Out of interest, are you saying that this works fine on iPhone but not on iPad? Or did you not allow rotation on the iPhone version?
I've also found this thread which says that presenting your modal view controllers from the root view controller may help.

I have worked on multiple modal view controllers being presented on iPhone. There is no problem with layout, unless there is something wrong with my own code for handling multiple orientations. Auto rotation methods actually never get called when view controller is behind another view controller, so I would also adjust layout on viewWillAppear: as well.
On viewWillAppear:, willRotateToInterfaceOrientation:duration:, and didRotateToInterfaceOrientation:, I would adjust the layout to the correct orientation as needed, for example:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self switchToInterfaceOrientation:self.interfaceOrientation];
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willRotateToInterfaceOrientation:interfaceOrientation duration:duration];
[self switchToInterfaceOrientation:toInterfaceOrientation];
// be careful, self.view.frame is still according to self.interfaceOrientation
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// self.view.frame is updated
// update something else here
}
- (void)switchToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// update your layout here
}
I'm not sure about how the above code would behave on view controllers on iPad. Since it supports view controller with different modal presentation styles, there might be some surprises.

Related

Modally presented view controller can't unwind

I have a UIViewController sublcass (VC1) embedded in a UINavigationController. VC1 triggers a modal segue to another UIViewController subclass (VC2) which is embedded in its own, different UINavigationController. Inside of an action method triggered by a UIBarButtonItem in VC2's nav bar, I call
[self performSegueWithIdentifier:#"SomeString" sender:nil]
which corresponds to an unwind method inside VC1. For some reason, the transition does not occur.
It only became a problem after switching to XCode 6. It worked fine in XCode 5. Any ideas?
This issue has been doing the rounds and I have the exact same problem. Unfortunately there is no good solution to it yet, other than go back to the old delegate pattern.
If you subclass your parent view controller navigation controller and implement - (UIViewController*)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender you will see that the modal is actually looking for your unwindSegue method on the navigation controller instead of the view controller that presented the modal.
The problem gets even more amplified if you have a container view controller as the method above gets called all the way up the controller chain to the storyboard's initial view controller.
There's a potential workaround here Unwind Segue not working in iOS 8 but it has its downsides and side effects as well.

view controllers: presentation, dismissal

Just for the purpose of learning some particular aspects of xCode, I am creating a simple app that has 2 functional view controllers. Each contains a button that can be pressed to switch to the other. I am not using segues. I am using pointers retrieved from the app delegate.
visual illustration (click for higher resolution):
When the app loads, the root view controller presents view 1. When you click "switch to view 2," the following code causes view 2 to appear:
- (IBAction)buttonPressed:(id)sender
{
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[self presentViewController:appDelegate.view2 animated:YES completion:nil];
}
So far, so good.
But when you click "switch to view 1" on the second view, this same code (replacing "view2" with "view1") gives the following error:
Application tried to present modally an active controller.
So to summarize (where --> = presents), we have root --> view1 --> view2 -x-> view1
I don't care about retaining the history of who presents whom. I simply want the buttons to bring to the top (make visible) a previously displayed view controller, maintaining the state of its views.
It would be nice to know the following:
Is there a workaround that would enable me to achieve the intended behavior using presentViewController? E.g., root --> view2 --> view1
What other method(s) would be more practical for achieving the desired behavior? It/they must use the app delegate because in my real application that will be unavoidable.
Am I breaking the rules by trying to put a view controller on top without integrating into some larger architecture? E.g, is this sort of behavior supposed to be handled by navigation conrollers and pushing/popping? If so, can you explain why xCode doesn't want me to do this? Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers? (Maybe because that could lead to abuse of the app delegate?)
What does it really mean to "present" a view controller? What functional limitations or capabilities does it entail beyond creating pointers between presenting and presenter? What is the importance of leaving the presenting view controller "active"?
If instead make the button on view1 send the presentViewController message to the root view (which I hoped would just change the presentation chain from root --> view1 to root --> view2, leaving view1 still existing in memory but not part of this chain), I get a different error: "Attempt to present on whose view is not in the window hierarchy!" What does this mean? I can't find an explanation of window hierarchy.
Okay, I know I'm asking a lot here, but any amount of enlightenment will be greatly appreciated!!
The correct way to do this is to get the underlying rootVC to do the presenting and dismissing (as you attempt - without the dismissing part - in point 5). You can achieve this by sending a message + completion block back to the rootVC from each of view1 and view2 when you want to present the other.
When you are in view1:
- (IBAction)buttonPressed:(id)sender
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIViewController* presentingVC = self.presentingViewController;
[presentingVC dismissViewControllerAnimated:YES completion:^{
[presentingVC presentViewController:appDelegate.view2
animated:YES
completion:nil];
}];
}
and similarly for view2. Take care that you need this line:
UIViewController* presentingVC = self.presentingViewController;
as you can't refer to 'self.presentingViewController' inside the completion block as it's controller has been dismissed at this point.
I think that answers points 1 and 2.
To answer point 3 "Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers?" - well you can (via the rootViewController property of the window), but then you are going to have to implement navigation and manage your viewController pointers, which means you will end up creating a controller of some sort. Apple is helping you here by providing you with a few which cover most needs.
As regards your point 4 - the presenting of a viewController is controlled by the presenting VC, which is why you want to keep that one 'active'. When you send this message:
[self dismissViewControllerAnimated:completion:], self just reroutes the messge to it's presentingViewController. If you get rid of your presentingViewController your dismiss method will break.
Point 5 is answered above. You need to dismiss the topmost view first before asking an underlying view to present. Note that view1 is "still in memory" but only because you have retained a pointer to it in your app delegate.
update
As you are trying to get this to work with an initial launch-straight-to-view1, you could make a BOOL launched property and check/set it from your rootViewController's viewDidAppear:
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (!self.launched) {
self.launched = TRUE;
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[self presentViewController:appDelegate.view1
animated:YES
completion:nil];
}
}
Let me try to tackle your points one by one.
1) No, you shouldn't do this all with presentViewController.
2) If you want to do root --> view1 --> view2 --> view1, then you don't do that all with presentViewController. To go from view1 back to view2 you should use dismissViewControllerAnimated:completion.
3) The view controllers do have a relationship when you use presentViewController:animated:. The presenting controller has a pointer to the one it presents, and the presented one has a pointer to the one that presented it. So, you're getting these relationships whether you want it or not. There is a way to display whatever controller you want with no relationship between them -- just reset the window's root view controller. The old view controller will be deallocated (if you don't keep a strong pointer to it), and the new one becomes the window's root view controller.
4) Presenting a view controller makes that controller a modal view controller -- it takes over the whole screen and is intended to be used as an interruption in the flow of the app. You really shouldn't use them extensively to go from one controller to another (and especially not for going "backwards" to previous controllers). Because of the way it's supposed to be used, you normally want to go back to the controller that presented it, so that's why it's kept "active" (in the sense that it's not deallocated).
5) You get that error because root's view is not on screen, view1's is. You need to present a view controller from the controller on screen.

Only support orientation for modal UIViewController

My app has a UINavigationController, it does not support rotation. However I want to show a modal view controller on top of the navigationcontroller that should support rotation. Is this possible? I tried to override shouldAutorotateToInterfaceOrientation on the view controller that is shown modally but that doesn't seem to work.
I think that in addition to shouldAutorotateToInterfaceOrientation of the view controller that is shown modally returning YES, your navigation controller's shouldAutorotateToInterfaceOrientation should be overridden to return YES when that modal view controller is showing, and NO otherwise.
You need to make sure your device generates device orientation notifications (UIDeviceOrientationDidChangeNotification):
make sure:
generatesDeviceOrientationNotifications is set to true
From the Apple docs:
"The window object does much of the work associated with changing the current orientation. However, it works in conjunction with the root view controller to determine whether an orientation change should occur at all and, if so, what additional methods should be called to respond to the change. If this controller is a container, it may rely on a child to decide whether the orientation should occur."
So if you root view controller (UINavigationController?) doesn't support rotations then this might be set to false when your app starts. In which case you will need to turn it back on when appropriate:
UIDevice myDevice = [UIDevice currentDevice];
[myDevice beginGeneratingDeviceOrientationNotifications];
I found the answer in another post: UIViewController inside a root ViewController not rotating
The key is
- (void)addChildViewController:(UIViewController *)childController
and
- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers

The child view does not rotate from landscape to portrait and vice versa

I'm making an application for the iPad.
I have a view controll that has to stay in landscape, this works just fine.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
}
From this view i push towards an other view, this child view should rotate to the appropriate rotation (landscape or portrait) whenever the user tilts his ipad.
in this child view i use this code to make this happen, but it doesn't work. It sticks to the landscape.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}
In order for a view controller to support rotation, all view controllers in its hierarchy must support rotation.
From the Responding to Orientation Changes documentation:
The window looks for an appropriate view controller and calls itsshouldAutorotateToInterfaceOrientation:method to determine if it supports the new orientation.
Container view controllers may intercept this method and use their own heuristics to determine whether the orientation change should occur. For example, the tab bar controller allows orientation changes only if all of its managed view controllers support the new orientation.
Further, you should not be using multiple view controllers to manage a single screen.
From the View Controller Programming Guide documentation (emphasis mine):
The one-to-one correspondence between a view controller and the views in its view hierarchy is the key design consideration. You should not use multiple content view controllers to manage different portions of the same view hierarchy. Similarly, you should not use a single content view controller object to manage multiple screens’ worth of content.
In this case, I'd suggest disabling rotation handling in your parent view controller, changing the child view controller to be simply a view (to meet the above criteria), and manually monitoring orientation changes to update your child view's layout.
You can monitor for orientation changes by listening for the UIDeviceOrientationDidChangeNotification notification. Example code:
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(yourSelector:)
name:UIDeviceOrientationDidChangeNotification object:nil];
Update
If by "push" you mean push a view controller onto a navigation view controller then please disregard the second part of my response.
If this is the case, you must ensure that you have overridden the shouldAutorotateToInterfaceOrientation: method of your navigation controller to return YES as well as your view controller in order to support rotation handling.

viewWillAppear and viewWillDisappear callbacks when a ViewController's view is hidden by another view & UIViewController Containment in iOS5

The UIViewController docs mentions about -viewWillDisappear:
"This method is called in response to a view being removed from its
window or covered by another view. This method is called before
the view is actually removed or covered and before any animations are
configured."
In iOS 4.3 and lower we are supposed to present a viewController and not add a viewController's view to the view hierarchy explicitly, so the calls -viewWillDisappear or -viewDidDisappear would be triggered when a new view controller is being presented over the existing view, in which case 'covered by another view' is true! But what if a viewController's view is hidden since another view obstructs the viewController's view? Do we get these callbacks?
Well, in iOS 5 there is a UIViewController containment concept where views can be directly added as subviews in view hierarchy by setting the parent-child relationship between viewControllers. So, unlike <= 4.3 OS, -viewWillDisappear and -viewDidDisappear calls should ideally be triggered when a viewController's view is obstructed or covered by some other view, which I have verified by a sample project that it is not happening in SDK 5.0.
Has anyone found this problem related to these callbacks?
Or, is my understanding correct?
Thanks,
Raj
Someone has the same kind of problem here :
iOS 5 : -viewWillAppear is not called after dismissing the modal in iPad
You should read the answers, I found them very interesting.