iosuisplitviewcontrolleruisplitviewdelegate

How to detect when user toggles sidebar hidden in split view controller to preserve secondary only display mode?


I’m using UISplitViewController to create a sidebar for my app - its style is .doubleColumn and I leave the preferredDisplayMode set to its default automatic behavior. It appears like the Photos app where in landscape both the primary and secondary columns are visible and in portrait only the secondary is visible (with a back button to reveal the primary overtop).

When in landscape, if the user taps the toggle sidebar button to hide the primary column (or toggles it via keyboard shortcut), rotates to portrait, and then rotates back to landscape, the sidebar undesirably becomes unhidden. This is unlike the Photos app where once the user hides the sidebar, it stays hidden until they unhide it (even across app launches). I want to achieve that same behavior.

To do this, I figured I could use the delegate function splitViewController(_willChangeTo:) and check if the new display mode is .secondaryOnly and the old display mode is .oneBesideSecondary then I’d store a bool in UserDefaults indicating the user hid the sidebar and I'd set the preferredDisplayMode to .secondaryOnly to preserve its hidden state between rotation. On the next app launch I’d check if that’s true and set the preferredDisplayMode to .secondaryOnly. (And similarly reset the flag and preferred display mode to .automatic when going from secondary only to one beside secondary.) The problem is that delegate function gets called with those same states when you rotate the device, which would cause me to set the flag and preferred display mode inappropriately. I need to only do that when the user manually toggled the sidebar, not when the system hid it due to a change in available space for example.

How can this be achieved?


Solution

  • I brought this question to a lab at WWDC where I received a good solution! In order to preserve the sidebar's hidden state between rotation, you can implement viewWillTransition(to:with:) to set a flag such as systemIsChangingViewSize to true, call super, then set it to false. In the delegate function .splitViewController(_:willChangeTo:) check this flag. It will be true when the system initiated a size change and false when the user initiated it by toggling the sidebar. Execute the logic noted in the question only if it's false in order to preserve the user's desired sidebar visibility.

    private var systemIsChangingViewSize = false
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        systemIsChangingViewSize = true
        super.viewWillTransition(to: size, with: coordinator)
        systemIsChangingViewSize = false
    }
    
    func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
        navigationDelegate?.navigationSplitViewController(self, willChangeDisplayMode: displayMode)
        if !systemIsChangingViewSize {
            if displayMode == .secondaryOnly && svc.displayMode == .oneBesideSecondary {
                DispatchQueue.main.async {
                    svc.preferredDisplayMode = .secondaryOnly
                }
            } else if displayMode == .oneBesideSecondary && svc.displayMode == .secondaryOnly {
                DispatchQueue.main.async {
                    svc.preferredDisplayMode = .automatic
                }
            }
        }
    }