More info on restore state between split and tab hierarchies needed please

Please explain how and when the state should be transferred from split VC to tab controller when traits change from regular to compact. Please also consider then the middle “supplementary” column is in use.

Please confirm we should no longer use adaptivity API given any showing alert or popover would likely be lost or should those also be in the state?

Sample code of the relevant parts of the Shortcuts app would be very useful as this is a very difficult design pattern and it seems it is essential for cross platform apps targeting iPhone/iPad/Catalyst.

Replies

For the UISVC behavior:

Use the delegate methods, -splitViewController:topColumnForCollapsingToProposedTopColumn: and splitViewController:displayModeForExpandingToProposedDisplayMode: to set up which vc's are displaying the data. Just return the value that's proposed (presuming that's what you want—if you have set a vc for the compact column, the compact column will be proposed for the collapse).

If you want to reparent view controllers at that time, be careful. In the long run, it's probably easier to have separate vc instances.
Ok so you can use those delegate methods to know when the transition is about to happen and then set your TabBarController or SplitViewController accordingly...

...and you say not to reparent views from your TabBarController to SplitViewController (and visa versa)

...but how exactly to you then restore your position in the hierarchy?

In the WWDC20 Build for iPad video @19:42... she mentioned something about the "Restorable" protocol?

Is this a protocol we make ourselves or is this part of UIKit?

I couldn't find any mention of it.

Thanks,

Kevin
If you are looking for more verbose solution:

I believe the protocol Restorable mentioned in the vid is supposed to be written by you. You would have to wait for callback to splitViewController:topColumnForCollapsingToProposedTopColumn: method. When this method is called it is up to you, to retrieve current hierarchy from split view controller and apply it to tab bar controller.
For example I use something like this:
Code Block
func splitViewController(_ svc: UISplitViewController,
topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
        if proposedTopColumn == .compact,
let compactController = viewController(for: .compact) as? NavigationRestorable {
            compactController.restoreHierarchy(from: currentHierarchy, selectedTab: selectedTab)
        }
        return proposedTopColumn
    }


NOTE: NavigationRestorable is another protocol, which contain restoreHierarchy method and selectedTab property in my project, for you it may be a different set of value that is needed to restore state.

and implementation of restoreHierarchy method in any UITabBarController subclass would be similar to this.

Code Block
func restoreHierarchy(from hierarchy: [HierarchyRestorableObject], selectedTab: Tab) {
setViewControllers(hierarchy.map({ $0.restore() }), animated: false)
/// update `selectedIndex` based on your `selectedTab` value.
}


NOTE: HierarchyRestorableObject - most likely would be a view controller, or a tuple of values where view controller would be.

then you would conform your view controller, to Restorable protocol

Code Block
class MyController: UIViewController, Restorable {
// ...
func restore() -> UIViewController {
let restoredController = UIViewController(nibName: nil, bundle: nil)
// ... restore search state or even scroll position if needed
return restoredController
}
}

and then you can simple conform UINavigationController to Restorable protocol

Code Block
extension UINavigationController: Restorable {
    func restore() -> UIViewController {
        let navController = NavigationController(navigationBarClass: NavigationBar.self, toolbarClass: nil)
        navController.tabBarItem = tabBarItem
        for viewController in viewControllers {
            guard let restorableController = viewController as? Restorable else {
                break
            }
            restoredNavigationController.pushViewController(restorableController.restore(), animated: false)
        }
        return navController
    }
}


and that would be the entire solution. Unfortunately you would have to conform every of your view controllers to Restorable, and restore their unique state.

And do not worry about controller that are shown using present method.
I agree that the Restore protocol appears to be something you write or perhaps already wrote. If you're using iPadOS maybe you already implemented multiple windows. Well, state restoration is a key concept in multiple windows and the required restoration is similar to what needs to be done when transitioning between different size class with side bars and tab bars. Hope this helps!
@roman19 Thanks a lot for your code snippet here. I do understand how we can splitViewController:topColumnForCollapsingToProposedTopColumn: to restore the state when switching from regular to compact mode. The thing I do not understand is how we can implement the other way around. So going from compact mode to regular mode. The only way to notice the transition, afaik, is splitViewController:displayModeForExpandingToProposedDisplayMode:. But when I try to reset the view controller to be shown by the SplitVC by calling setViewController(_ vc: UIViewController?, for column: UISplitViewController.Column) my app will crash with the following exception:

Code Block
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Mutating UISplitViewController with -setViewController:forColumn: is not allowed during a delegate callback.'


If I am not allowed to set the secondary view controller in the delegate method, how am I supposed to show the correct VC after the layout changed from compact to regular ?