On iOS 8, UISplitViewController appears to save and restore the state of its subviews, for example, whether the master view is hidden or not.
This is undesirable because my app should always show the master view in landscape and always hide it in portrait. If the user closes the app in landscape (landscape state is saved) and reopens it in portrait (landscape state is restored), then the UISplitViewController shows the master view in the wrong configuration.
I still need to supply a restoration identifier to the UISplitViewController so that is subview controllers have their own state saved and restored. So how does one prevent UISplitViewController from restoring its own state, or override this behavior?
I solved this by subclassing UISplitViewController and overriding - (void)decodeRestorableStateWithCoder:(NSCoder *)coder
to do nothing. This way the split view controller does not have an opportunity to restore its views, but its child view controllers still participate in state restoration.
The first thing needed when implementing UI State Restoration is changing from using didFinishLaunchingWithOptions
to willFinishLaunchingWithOptions
. If you set the delegate now in the willFinish the collapse will be called as expected. The issue was likely the delegate was set too late and it had already collapsed without your special handling.
Another issue is that the restoration paths to the controllers are different when in landscape and portrait so could be getting in a weird state. Because of the change it can't automatically find the existing detail view controller and creates new instances and either both or one of them likely get thrown away by the split view delegate because of misconfiguration of the detail item. In the State Restoration docs under "Recreate Your View Controllers" on step 3 it says it looks for an already created view controller with the same path which sadly fails when restoring after an orientation/trait change because the path is different. So it falls back to step 4 and creates a brand new empty misconfigured detail controller and is the reason you see the wrong configuration of controllers.
To understand the restoration identifier paths, implement application:viewControllerWithRestorationIdentifierPath:coder:
in the app delegate and output the path components you'll see in portrait the last to be restored looks like:
SplitViewController,
MasterNavigationController,
DetailNavigationController,
DetailViewController
...which corresponds the single hierarchy primary of the split view controller (note: DetailNavigationController is a hidden nested navigation controller in this configuration).
And in landscape the last two to be restored are:
SplitViewController,
MasterNavigationController,
MasterViewController
and
SplitViewController,
DetailNavigationController,
DetailViewController
...which corresponds to the primary and secondary controller hierarchies of the split view.
So now knowing the restoration path to DetailViewController can be different, you can understand that if you try to automatically restore a portrait path while the storyboard has initlaised in landscape it won't find that detail view controller and resort to creating a new one. So I think the solution is to give it a helping hand to find it regardless of how the restoration path was saved:
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder{
if([identifierComponents.lastObject isEqualToString:@"DetailViewController"]){
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *secondaryNavigationController = splitViewController.viewControllers.lastObject;;
DetailViewController *detail = (DetailViewController *)secondaryNavigationController.viewControllers.firstObject;
return detail;
}
return nil;
}
Now the restoration will correctly use the existing detail controller which is configured correctly and it won't be thrown away by the split view delegate which was resulting in you being left with the master.
Another way this issue can manifest is seeing two detail controllers pushed onto the navigation stack after restoration, which is what happens if you force the split view delegate to not throw away the initial detail controller, and when the restoration creates another one you end up with two pushed on!