Search code examples
iosswiftautolayoutuisplitviewcontroller

Showing/Hiding primary view controller of UISplitViewController shifts detail view


I have a UISplitViewController for Master/Detail functionality on an iPad. The master view controller shows a list of items, and when the user selects one the detail information is shown in the detail view controller. I have the detail view controller set to navigate to another view controller to display a graph. When this happens, I hide the primary view controller with the following lines in my prepareForSeque.

if let svc = self.splitViewController {
    svc.preferredDisplayMode = .PrimaryHidden
}

This works great. When navigating back to the detail view from the graph view I would like to again show the primary view from the split view controller. I put this in viewWillAppear.

guard let svc = self.splitViewController else { return }

if svc.preferredDisplayMode != .Automatic {
    svc.preferredDisplayMode = .Automatic
}

Again this works exactly as I would expect. The problem is the detail view changes size during this process and it is not laid out properly when returning from the graph view.

Here is a screen shot before navigating to the graph view, and before hiding the primary view of the UISplitViewController. Before

And this is after returning from the graph view. After

My attempt to fix the issue was to force the detail view to layout itself. I tried the following:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    guard let svc = self.splitViewController else { return }
    let detailNavController = svc.viewControllers[svc.viewControllers.count-1] as! UINavigationController
    detailNavController.view.setNeedsLayout()
}

It sort of works but causes an ugly jump in the interface as the detail view appears and then a second later is relaid out. Is there a better way to get the view laid out properly before it is displayed?


Solution

  • I got it working, and figured I would share in case anybody else runs into the same issue. As Tim stated in the comments, it is a matter of running layout early enough in the chain to get things laid out before the view is presented on screen.

    I did the following in the Graph view controller scene:

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        guard let svc = self.splitViewController else { return }
        svc.preferredDisplayMode = .Automatic
    }
    

    This set the split view back to showing both the primary and secondary view controllers very early in the process.

    The place where I was going wrong was the parent of my detail view controller is a UINavigationController. I assumed that since this was being sized wrong, I needed to get it to lay itself out again after setting the split view controller to show the primary and secondary views. This was a wrong assumption. What ended up working was going up one more level to the UISplitviewController and having that perform a layout on itself.

    In my detail view controller I did the following:

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        guard let svc = self.splitViewController else { return }
        svc.view.setNeedsLayout()
        svc.view.layoutIfNeeded()
    }
    

    This was early enough in the chain that all the layout gets completed before the view is shown, but late enough that the split view controller has the appropriate sizing information for showing both the primary and secondary views.

    Hopefully this helps someone else, and thanks for the comments.