UISplitViewController Cannot Change the Master View Controller? - iphone

I am trying to change the master view controller (that appears in the popover in portrait, and on the left side in landscape) in a UISplitViewController. I'd like to switch between the master view controller being one of two different UIViewControllers (depending on an action taken elsewhere in my application).
I am using the following:
mySplitViewController.viewControllers = [NSArray arrayWithObjects:newMasterController, detailController, nil];
This correctly changes the master viewcontroller as you would expect in landscape mode. However it does not seem to change the viewcontroller used for the popover in portrait mode.
I notice that the barbuttonitem to show this master view controller is just calling showMasterInPopover: on the splitviewcontroller, and so would expect it to work in portrait mode as well, but it does not.
In fact, I can set the master view controller, see the new viewController correctly in landscape mode, rotate back to portrait, and the viewcontroller used in the popover is still the old one.
Is this a bug?

In case anyone is looking for a solution to this issue (or a word-around), I had a similar issue and worked it out here: Changing the master view in Split View does not update the popover in portrait
Basically, I used a navigation controller for the master view controller, and pushed/poped view controllers within that navigation controller, in order to change view controllers in the master view whilst still displaying the correct viewcontroller in portrait orientation, in the popup view.

UPDATED: please read final update at bottom! Original answer + update below may not be useful!
We have just had exactly the same problem. Sometimes I wonder if Apple actually test the classes they write with anything resembling realistic use cases, because UISplitViewController is not their finest moment.
The problem is that when you replace the master view controller in the split view, the code inside UISplitViewController doesn't update its popover controller's contentViewController property. The result is that the popover controller still has a handle on an out of date view controller, resulting in old UIs appearing, or even memory faults, when in portrait mode.
Here is our workaround.
I will assume that you have a UISplitViewControllerDelegate conforming class which stores the popoverController as a class property (see the standard sample code for UISplitViewController).
At the point at which you set the new master view controller, you also need to update the contentViewController, as follows:
mySplitViewController.viewControllers
= [NSArray arrayWithObjects:newMasterController, detailController, nil];
// in the cases where the uisplitview has already shown a popovercontroller,
// we force the popovercontroller to update its content view controller.
// This ensures any old content view in popover actually gets released by
// the popovercontroller.
if (popoverController) {
[popoverController setContentViewController:theMasterViewController
animated:NO];
}
You also must set the popover's contentViewController when your UISplitViewControllerDelegate gets informed that the popover controller is going to present a view controller:
- (void)splitViewController:(UISplitViewController*)svc
popoverController:(UIPopoverController*)pc
willPresentViewController:(UIViewController *)aViewController
{
// set the popoverController property - as per Apple's sample code
self.popoverController = pc;
// THE LINE BELOW IS THE NEW LINE!
[popoverController setContentViewController:aViewController animated:NO];
Yes, I know the above code looks insane, and you're wondering why apple couldn't just set the content view controller themselves. But they apparently don't, and this is the fix.
UPDATE
The above scheme, with setting the content view, turns out not to work after all. For example, if you set the content view to be a uinavigationcontroller, later on you get passed the root view inside the nav controller, instead of the nav controller itself. UISplitViewController just doesn't handle changing the master view in any workable way, as far as I can see.
The workaround I currently have is to install a UINavigationController as the master view, and change the root view controller of that nav controller. So I get to change the master view 'by the back door', in a way.
UPDATE 2
I give up. The approach in the first update above is flawed too; I get problems upon rotation still. Basically, it seems that if you use UISplitViewController, you shouldn't attempt any changing of the master view controller (even if you're switching the master view when the master view, e.g. as a popover, has been hidden again). Fiddling with the contents of a UINavigationController in the master view (while the master view is showing) appears like it will be ok, but anything beyond that leads to problem after problem.
Technical note: I believe the problems stem from an apparent weakness in Apple's handling of UIs: namely, Apple code will call release on UIViews and controller once hidden or removed from view, but will then later, if the containing viewcontroller is shown again, send deferred messages like viewDidDisappear to the released views/controllers (which at that point may have been deallocated).

Related

What happens under the hood when we do presentViewController?

Given the below code
self.view.backgroundColor = [UIColor yellowColor];
MyViewController *myVC = [[MyViewController alloc] initWithNibName:#"MyView" bundle:nil]
myVC.view.backgroundColor = [UIColor clearColor];
myVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:myVC animated:NO completion:nil];
What happens under the hood when we call presentViewController ? When myVC is visible I cannot see yellow color, then I checked myVC.view.superView in it's viewDidAppear method and it is UIWindow.
Q1. Is that mean until the modal window is up presentingViewController.view (self.view in above case) is removed from the View hierarchy and presentedViewController.view (myVC.view in above case) is added over UIWindow ?
Q2. What will be the case if myVC.modalPresentationStyle != UIModalPresentationFullScreen ?
Q3. Does iOS also remove all the views from UIWindow except presentedViewController.view until the full screen modal dialog is up for optimization ? If NO why not ?
First, let's discuss the case without animation.
Before calling present:
Your window has one view hierarchy, starting from its rootViewController view.
After calling present:
The view hierarchy still exists without change.
A special full-screen view called "dimming view" is added to the window (that is, not inside the rootViewController's view but inside the window (window is a UIView, too). This view is transparent, dims the presenting controler and blocks user interaction.
The presented (modal) controller's view is then added also to the window.
There are some other views added in between the window and the presented controller's window. If you log your view hierarchy, you'll see classes named _ControllerWrapperView or something similar. However, this has changed between iOS versions and you shouldn't rely on the view structure.
Note that that the modal controller can't ever be transparent because it is not direct subview of the window and the wrappers between the controller and the window are not transparent.
The animated case is almost the same. Only there are some fancy animations between the steps.
Edit 2:
The answer was really a bit incorrect. There is a big difference between iPhone and iPad presented controllers.
On iPhone, the presented controllers are always displayed full screen and the presenting controllers are actually removed from the window.
On iPad, if the presented controller is not fullscreen (see UIModalPresentationStyle), the presenting controller stays in the window.
Your questions:
Is that mean until the modal window is up presentingViewController.view (self.view in above case) is removed from the View hierarchy and presentedViewController.view (myVC.view in above case) is added over UIWindow ?
If the controller is full screen, then this claim is true. Otherwise, the presenting view controller stays there but the whole contents are overlapped by other views (even if they are semi-transparent). Also, there are always some views between the presented and the presenting controller views.
What will be the case if myVC.modalPresentationStyle != UIModalPresentationFullScreen ?
See the answer to the previous question - on iPhone, there would be no difference.
Does iOS also remove all the views from UIWindow except presentedViewController.view until the full screen modal dialog is up for optimization ? If NO why not ?
From my tests, only the presenting controller is removed from the window hierarchy. This is probably to optimize drawing performance. This is the only controller the system can safely remove. Removing any other view could cause problems (e.g. views that should be always visible).
Edit:
If you want to make a transparent controller, you can:
Add the view directly to your view hierarchy (either to the controller's view or to the window) with a transition animation (+[UIView transition...])
The same but also adding a child controller to your controller.

Switch between UIViewControllers using UISegmentedControl

I have a tabbar -> navigationcontroller structure. In one of these tabs, I want to switch between two UIViewControllers (a KalViewController and a UITableViewController to be be exact), using a UISegmentedControl located in the Navigation Bar.
Currently, I have a third UIViewController, that pops and pushes the appropriate ViewControllers on segment value change. I don't think thats the right way to do it and it also destroys the navigation stack (when I tap on the bar item, the navigation controller goes the root controller, which won't work). And there's even another bug, related to the Kal Component.
So, what's the right way to do it?
The right way to do it is to have the controller handling the UISegmentedControl add the views of the controllers as subviews.
[self.view addSubview:controller.view];
It's your responsibility to send viewWillAppear: and so on.
EDIT: The offset you're talking about can be adjusted using:
controller.view.frame = CGRectMake(x, y, width, height);
EDIT 2: In response to tc.'s comment:
From the documentation of UISplitViewController:
Message Forwarding to Its Child View Controllers
A split view controller interposes itself between the application’s window and its child view controllers. As a result, all messages to the visible view controllers must flow through the split view controller. This works generally as you might expect and the flow of messages should be relatively intuitive. For example, view appearance and disappearance messages are sent only when the corresponding child view controller actually appears on screen. Thus, when a split view controller is first displayed in a portrait orientation, it calls the viewWillAppear: and viewDidAppear: methods of only the view controller that is shown initially. The view controller that is presented using a popover does not receive those messages until the popover is shown or until the split view controller rotates to a landscape orientation.
This is not magical and there is no reason why you wouldn't be able to write a similar controller yourself. In fact I've done it and it worked just fine.

UIView subview doesn't change orientation

I have a view controller which manages a view.
I'm adding the my view controller subclass as a subview of the window swapping out another view.
I'm running landscape mode on an iPad.
The view apparently doesn't know that its in landscape mode. Its frame is confused.
Is there something I can/should do to tell it that its in landscape, and/or that the orientation has changed. How does this normally happen. Why isn't it happening?
I used to have my view controller within a UITabBarController and it worked fine there.
Override:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return YES;
}
Your ViewController is not getting rotation events because you have not presented the viewController but have added the viewController's view in the view hierarchy.
Your Tab bar controller previously used to take the responsibility to forward the rotation events to the view controller which it manages, that was how it used to work.
I would though suggest that swapping the view out of window is a bad idea. Instead you should have a main viewController which accepts the rotation events and then swap the view within this viewController based on the current orientation. Consider re-desiging before you code further.
My problem was that my storyboard was overriding my existing custom coded app delegate. After I deleted the story board file, and custom generated view controller code, it worked for me.

iPhone Landscape FAQ and Solutions

There has been a lot of confusion and a set of corresponding set of questions here on SO how iPhone applications with proper handling for Landscape/Portrait mode autorotation can be implemented. It is especially difficult to implement such an application when starting in landscape mode is desired. The most common observed effect are scrambled layouts and areas of the screen where touches are no longer recognized.
A simple search for questions tagged iphone and landscape reveals these issues, which occur under certain scenarios:
Landscape only iPhone app with multiple nibs:
App started in Landscape mode, view from first nib is rendered fine, everything view loaded from a different nib is not displayed correctly.
Iphone Landscape mode switching to Portraite mode on loading new controller:
Self explanatory
iPhone: In landscape-only, after first addSubview, UITableViewController doesn’t rotate properly: Same issue as above.
iPhone Landscape-Only Utility-Template Application: Layout errors, controller does not seem to recognize the view should be rotated but displays a clipped portrait view in landscape mode, causing half of the screen to stay blank.
presentModalViewController in landscape after portrait viewController: Modal views are not correctly rendered either.
A set of different solutions have been presented, some of them including completely custom animation via CoreGraphics, while others build on the observation that the first view controller loaded from the main nib is always displayed correct.
I have spent a significant amount of time investigating this issue and finally found a solution that is not only a partial solution but should work under all these circumstances. It is my intend with this CW post to provide sort of a FAQ for others having issues with UIViewControllers in Landscape mode.
Please provide feedback and help improve the quality of this Post by incorporating any related observations. Feel free to edit and post other/better answers if you know of any.
What's in the documentation:
In your view controller, override shouldAutorotateToInterfaceOrientation: to declare your supported interface orientations. This property will/should be checked by the controller infrastructure everytime the device orientation changes.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
{
return (orientation == UIInterfaceOrientationLandscapeRight);
}
This is the absolute minimum your view controller needs to do. If you want to launch your application in landscape mode, you need to add the following key to your .plist file:
<key>UIInterfaceOrientation</key>
<string>UIInterfaceOrientationLandscapeRight</string>
Apple recommends starting landscape only applications in Landscape Right mode (see the HIG under User Experience Guidelines > Start Instantly).
What's not in the documentation:
A little background:
Everytime you try to load a different view controller other than that loaded from the main nib, your view controller is neither interrogated about it's supported interface orientations nor is its frame set correctly. Only the first view controller bound to the window will be layed out correctly.
Other people have suggested using a "MasterViewController" hooked up to the main window to which other controllers add their views as subviews instead of hooking directly into the window. While I have found this solutions is a viable option, it does not work correctly in the case of modal view controllers added to those said subviews. There's also a problem if you have some subviews that should be able to autorotate (what the master controller will prevent).
The usage of undocumented API's to force a certain interface orientation is not an option either.
The solution:
The best solution I have found so far is a modification of the "MasterViewController" workaround. Instead of using a custom "MasterViewController", a UINavigationController with hidden Navigation Bar and hidden Tab Bar is used. If all other views are pushed/popped from the navigation stack of this controller, auto-rotations of controllers on that stack will be managed correctly.
Modal controllers presented via presentModalViewController:animated: from any of the view controllers on the UINavigationController's navigation stack will be rotated and rendered with correct layout. If you want your modal view controller to be rotatable to a different orientation than that of the parent view controller, you need to return the desired orientation from the shouldAutorotateToInterfaceOrientation method of the parent controller while the modal view is presented. In order to properly restore the interface orientation when the modal controller is dismissed, you need to make sure shouldAutorotateToInterfaceOrientation returns the desired orientation for the parent controller before you call dismissModalViewController:animated:. You can use a private BOOL on your view controller to manage that (e.g. BOOL isModalMailControllerActive_).
I'll add a piece of sample code soon, It's just to late now. Please let me know if any unresolved issues remain or anything is unclear about this post. Feel free to edit and improve.
I had an interesting requirement for ios application:
main viewController should be only landscape, but all others (which can be pushed from main) can be landscape and portrait.
Problem occours - when I push to a new viewController, which then is rotated to portraited - and the pop back - main view is no longer landscape. Also - opening application, it is not in landscape.
In order to keep main viewcontroller landscape, no matter from what orientation it was popped/pushed, I did the following thing: (in viewWillAppear:)
//set statusbar to the desired rotation position
[[UIApplication sharedApplication] setStatusBarOrientation:UIDeviceOrientationLandscapeLeft animated:NO];
//present/dismiss viewcontroller in order to activate rotating.
UIViewController *mVC = [[[UIViewController alloc] init] autorelease];
[self presentModalViewController:mVC animated:NO];
[self dismissModalViewControllerAnimated:NO];
Hopefully it will help someone!
P.S.Tested on sdk 3.2.5 ios 5.0.1.
P.S. Thanks for all the info in this FAQ!
For the second bullet point, if you want to use pushViewController to go from Portrait-only to Landscape-only view, one simple hack I found is to put the following code into your pushed controller's viewDidLoad:
UIViewController *viewController = [[UIViewController alloc] init];
[self presentModalViewController:viewController animated:NO];
[self dismissModalViewControllerAnimated:NO];
[viewController release];
I'd like to add to Johannes' answer (using the UINavigationController as MasterViewController).
The disadvantage I have found is that ViewControllers that are newly pushed onto the master VC's navigation stack do not adjust to any prior orientation changes. In short, VCs already on the stack are rotated, modal view controllers presented from them are also rotated, but newly added VCs are not rotated.
I have tried many tricks to fix this before finding one that works. Most only work for pushViewController: with animated set to YES.
To fix the issue entirely, I have subclassed UINagivationController and overriden pushViewController:animated: as follows:
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// Correctly autoOrient the given view controller
[self presentModalViewController:viewController animated:NO];
[self dismissModalViewControllerAnimated:NO];
// Push it as normal (now in its correct orientation)
[super pushViewController:viewController animated:animated];
}
Temporarily (and quite invisibly) presenting the view controller allows it to receive its orientation update. It can then be pushed onto the navigation stack in its correct orientation.
Please let me know if this works for you. All updates and improvements are very welcome!
Finally, I highly recommend Johannes' approach to rotation management.
EDIT: Update on popping view controllers from the stack
It seems that all the popViewController-related selectors go crazy when performed with animated:YES. Specifically, the view controller is animated out in the wrong direction. You can use animated:NO and restrict the use of such animations to other UINavigationControllers deeper down in your hierarchy (i.e. the ones that you push onto the root navigation controller's stack).
Any input is greatly appreciated.
I'm developing an iPad app that displays vertically scrolling gallery view of an array of items upon startup. In landscape mode there are 4 items across. In portrait there are three. When turning the iPad orientation it is supposed to refresh the gallery to have the items fit neatly across the screen. Then I double tap on an item to drill down to a modal view of that item. Then I do stuff with that item. Finally I dismiss the modal view.
During the refresh or orientation change the gallery view calculates the number of items to display based on the screen width (or height) and the current orientation from UIViewController.interfaceOrientation.
I was having a problem getting this to work correctly. Sometimes it would display only two items in landscape orientation after I dismissed the modal dialog.
At first I was using the UIViewController.view.frame.size values to calculate the number of gallery items. When the modal view was dismissed this frame size was incorrect e.g. the width and height had been reversed even though the orientation had not changed while the modal dialog was displayed.
I switched to using the application delegate ([[UIApplication sharedApplication] delegate]] and taking the window.frame.size to calculate the number of gallery items to display across. The window.frame.size stays correct under orientation changes and modal dialogs. The gallery items display correctly now.
This will work...
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIView *view = [window.subviews objectAtIndex:0];
[view removeFromSuperview];
[window addSubview:view];

iPhone SDK: How to display a view controller within another?

Fundamentally, what I want to do is within, for example, ViewControllerA display ViewControllerB and ViewControllerC. How would I do that?
Thanks in advance.
You don't display view controllers, you display views. Having said that, you can do something like this:
UIViewController *a = ...;
UIViewController *b = ...;
[a.view addSubview:b.view];
Now, having said that, you shouldn't do it. Tons of stuff does not behave properly, because there are tons of undocumented interactions between UIView, UIWindow, and UIViewController. There is nothing in the documentation that says it won't work, but random things stop behaving properly (viewWillAppear: on the interior view's VC doesn't get called, etc).
If you need this functionality, you should file a bug with Apple.
The default template for a navigation view controller should do what you want assuming you want two different screens (not two different sections on the same screen). Whenever you want to change the view from the current one to another, just tell the navigation controller to push it on the stack:
[self.navigationController pushViewController:viewBoards animated:YES];
The default navigation view controller gives you a root view controller with a navigation view controller in it. It also gives you one view controller called MainWindow. Just add as many copies of MainWindow as you need to get your functionality.