I am using the Xcode 7 (though I think this applies to 6 also), master-detail template with a slight modification. The modification is to replace the master UINavigationController with a UITabBarController.
The UITabBarController contains a couple of UINavigationControllers which, in turn have segues to the Detail UINavigationController.
Working with the iPhone 6+ simulator, it works fine in landscape mode (non collapsed). When the UISplitViewController is collapsed, the app stops behaving as expected. Two things happen:
- In AppDelegate's
didFinishLaunchingWithOptions
, thesplitViewController.viewControllers
array contains theUITabBarController
(master) andUINavigationController
(detail). Somewhere betweendidFinishLaunchingWithOptions
andsplitViewController:collapseSecondaryViewController:ontoPrimaryViewController
the detailUINavigationController
is removed fromsplitViewController.viewControllers
. This doesn't happen if both master and detail areUINavigationController
objects. It's interesting to note that, in Apple's documentation forUISplitViewController
they say of theviewControllers
property: "When the split view interface is expanded, this property contains two view controllers; when it is collapsed, this property contains only one view controller." The assertion in the documentation only seems to hold true after I modified the storyboard to have the UITabBarController as the master, in the default master-detail template, the assertion doesn't hold (i.e. there are always two viewControllers in the array regardless of whether the split view is collapsed or expanded). - If I add an item while running in collapsed mode, then the detail view is presented modally rather than being pushed onto the selected tab's navigation controller, so there is no way to get back to the master view. Rotating to landscape leads to a crash with
EXC_BAD_ACCESS
- for some reason I also get an error:"<Error>: CGImageCreate: invalid image size: 0 x 0."
.
The only changes to the 'vanilla' template app at this stage are to replace the Master UINavigationController with a UITabBarController which contains UINavigationControllers, the storyboard is below:
I also commented out the following line in MasterViewController.m
to prevent an earlier crash:
self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];
My questions are:
- Why does the content of the
viewControllers
property change when I add a UITabBarController as master rather than leaving the UINavigationController in place? I ask because I think this is relevant in determining why the detail VC is presented modally rather than being pushed. I'm assuming that, since in collapsed mode, the SplitViewController behaves like a UINavigationController and there is some difficulty doing that when there is a non-UINavigationController between the UISplitViewController and the detail view. - Following on from question 1, why does the detail controller get presented modally?
I'm seeking to understand what is going on as much as I am trying to find a correct way to get this to work if there is one (or more).
Further discussion and related questions
My question is very similar to this one: UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone
I've had some success using a combination of the answers from 'Dreaming In Binary' and 'HpTerm'.
If I first take Dreaming In Binary's answer (using the code from that answer verbatim), then the app behaves almost the same as before; I observe the following in particular:
The detail view is still presented modally when selected from the portrait view; basically a slightly different path to presentation of the detail view is taken as we have added this method (taken from the aforementioned answer):
-(BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender { UITabBarController *masterVC = splitViewController.viewControllers[0]; if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) [masterVC.selectedViewController showViewController:detailVC sender:sender]; else [splitViewController setViewControllers:@[masterVC, detailVC]]; return YES; }
showViewController
still triggers a modal presentation of the detailViewController. There is no navigation button at the top right in portrait, but if you rotate the device to landscape, then the whole landscape is taken by the detail view, but we do see the leftBarButtonItem for the navigationItem being set with the splitViewController.displayModeButton item. The trouble is that the button has absolutely no effect other than that it shows the 'expand' button (for hiding the master view); if you press the button nothing happens to the view (the master view is already hidden anyway) but the button changes to the 'back' style which would normally allow you to show the master view again. In this case the 'back' button has no effect when you press it. Essentially you can toggle to button between 'expand' and 'back' but there is no other effect when pressing it. If you rotate back to landscape, then you once again have the modally presented detail view with no way to get back to the master view.- I still see the error
<Error>: CGImageCreate: invalid image size: 0 x 0.
but it is not accompanied by the crash with EXC_BAD_ACCESS.
- I still see the error
So next up, is HpTerm's solution in Swift which, in my world looks like this:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailViewController sender:(id)sender
{
UITabBarController * masterViewController = splitViewController.viewControllers[0];
if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
{
UIViewController * detailViewSubController = ((UINavigationController *)detailViewController).viewControllers[0];
[((UINavigationController *)masterViewController.selectedViewController) pushViewController:(UIViewController *)detailViewSubController animated:NO];
return YES;
}
else
{
return NO;
}
}
So in the above, rather than trying to showViewController
with the detail's UINavigationController
, we extract the DetailViewController
from the detail UINavigationController
and actually push the DetailViewController
onto the master UINavigationController
. The reason for not pushing the detail UINavigationController
onto the master UINavigationController
is that an exception is thrown (applicable if any navigation controller is pushed onto another).
This actually yields an almost perfect result, but not quite. First, the good bits:
- In portrait, everything works correctly we push the detail over the master on selection and when we navigate back, we get back to the master correctly.
- On rotating to landscape, the split view displays master and detail correctly and we can show/hide the master view properly using the bar button item.
Now the bad:
- On rotating back to portrait (with an item selected on the master side), we go to the master view when, in fact, we should be going to the selected detail item.
So the question here is, can the 'bad' point be corrected so that if an item is selected while in portrait, rotation back to landscape takes us to the selected detail item?
Apologies this is so long, and I salute you if you've made it this far! I'm open to suggestions on slimming the question down too, but wanted to supply sufficient detail to highlight exactly what is going on.