Multiple UISplitViewController Tutorial

This UISplitViewController tutorial shows you how to build an adaptive layout note-taking app using multiple UISplitViewControllers. By Warren Burton.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Ensuring Correct Behavior When Traits Change

So far, you’ve set up the app to behave correctly when launched in either a compact or regular environment.

You saw that a UISplitViewController could adapt itself to either a compact or regular layout. When you place a view controller in the secondary view with showDetailViewController(_:sender:):

  • In compact mode, UISplitViewController shows the detail view on the top of the navigation stack and adds a Back button to the navigation bar.
  • In regular mode, UISplitViewController places the detail view in the right-hand split.

Currently, if you try to go from regular mode to compact mode — or vice versa — the app will not work correctly. This will affect your users when they are using your app in split screen multitasking modes on iPad.

UISplitViewController needs some hints on how to reassemble itself during a trait change.

reassembly for trait change

In this section, you’ll add the code to allow the app to respond correctly to the change from compact to regular.

Controlling the Reassembly

UISplitViewControllerDelegate has a large set of methods that allow you to customize the behavior of UISplitViewController. You’ll use two that are called when the split needs to change its trait collection.

First, you’ll add helpers to the view factory.

Find RootViewController+ViewFactory.swift in the Project navigator (ViewControllers ▸ RootViewController).

Add this extension to the file:

extension RootViewController {
  func rootFileList(_ split: UISplitViewController)
      -> FileListViewController? {
    let navigation = primaryNavigation(split)
    guard let fileList = navigation.viewControllers.first as? 
      FileListViewController else {
      assertionFailure("Your split should have a FileListVC in the master nav")
      return nil
    }
    return fileList
  }
  
  
  func activeEditor(_ split: UISplitViewController) -> EditorViewController? {
    guard let navigation = split.viewControllers.last as? UINavigationController,
      let editor = navigation.viewControllers.first as? EditorViewController
      else { return nil }
    return editor
  }
  
}

These methods are helpers used to extract either a FileListViewController or an EditorViewController from a UISplitViewController.

Changing From Regular to Compact Traits

Now, you’ll manage the change from regular to compact traits.

Open RootViewController.swift. Locate the extension:

extension RootViewController: UISplitViewControllerDelegate

Add this method inside the body of the extension:

func splitViewController(_ splitViewController: UISplitViewController,
                         collapseSecondary secondaryViewController: UIViewController,
                         onto primaryViewController: UIViewController) -> Bool {
  //1
  let primaryNav = primaryNavigation(splitViewController)
  var currentStack = primaryNav.viewControllers
  
  //2
  if let secondarySplit = secondaryViewController as? UISplitViewController {
    //3
    if let fileList = rootFileList(secondarySplit) {
      currentStack.append(fileList)
    }
    //4
    if let editor = activeEditor(secondarySplit) {
      currentStack.append(editor)
    }
    //5
    primaryNav.viewControllers = currentStack
    return true
    
  } else if let folderList = currentStack.first {
    //6
    primaryNav.viewControllers = [folderList]
    return true
    
  }
  return false
}

This delegate method splitViewController(_:collapseSecondary:onto:) allows you to control the process of collapsing the secondary controller onto the primary. You return a result of true if you want to override the default behavior.

  1. First, you get a reference to the root navigation controller and take a snapshot of its view controller stack.
  2. Next, you check if there’s a split in the secondary position.
  3. You append the FileListViewController from that split onto the destination stack.
  4. If the secondary view in the secondary split is an EditorViewController, you append that also.
  5. Finally, reset the navigation stack with its new contents.
  6. When there’s no split in the secondary position, you reset the navigation stack with only the folder list.

And that’s it! You have covered the case of the trait collection changing from regular to compact.

Changing From Compact to Regular Traits

Now, you’ll manage the change from compact to regular traits.

Add this code to the UISplitViewControllerDelegate extension:

func splitViewController(_ splitViewController: UISplitViewController,
                         separateSecondaryFrom primaryViewController: UIViewController)
    -> UIViewController? {
  
  guard let primaryNavigation = primaryViewController as? UINavigationController else {
    return nil
  }
  
  return decomposeStackForTransitionToRegular(primaryNavigation)
}

func decomposeStackForTransitionToRegular(_ navigationController: UINavigationController)
    -> UIViewController? {
  //1
  let controllerStack = navigationController.viewControllers
  guard let folders = controllerStack.first else {
    return nil
  }
  
  //2
  defer {
    navigationController.viewControllers = [folders]
    rootSplitView.preferredPrimaryColumnWidthFraction = rootSplitSmallFraction
  }
  
  //3
  if navigationStack(navigationController, isAt: .foldersOnly) {
    //folder list only was presented  - return a placeholder
    return freshFolderLevelPlaceholder()
    
  } else if navigationStack(navigationController, isAt: .foldersFiles) {
    //folders and file list was presented  - return a split with files + placeholder
    let filesAndPlaceholder = configuredSplit(
      first: controllerStack[1], 
      second: freshFileLevelPlaceholder())
    return filesAndPlaceholder
    
  } else if navigationStack(navigationController, isAt: .foldersFilesEditor) {
    //folders, files and editor was presented  - return a split with files + editor
    let filesAndEditor = configuredSplit(
      first: controllerStack[1], 
      second: controllerStack[2])
    return filesAndEditor
  }
  
  return nil
}

func configuredSplit(first: UIViewController, second: UIViewController)
    -> UIViewController {
  let freshSplit = freshSplitViewTemplate()
  freshSplit.preferredPrimaryColumnWidthFraction = rootSplitLargeFraction
  let fileNavigation = primaryNavigation(freshSplit)
  fileNavigation.viewControllers = [first]
  freshSplit.viewControllers = [fileNavigation, second]
  return freshSplit
}

The delegate method splitViewController(_:separateSecondaryFrom:) allows you to return the UIViewController that is placed in the secondary position when transitioning to regular traits.

In this code above, you cast the primaryViewController as UINavigationController, then you pass that controller to decomposeStackForTransitionToRegular(_:):

  1. Take a copy of the current navigation stack and root view controller in that stack.
  2. Use defer to reset the input UINavigationController to only the folder list and correct the column width for double-split views.
  3. Depending on what the navigation stack is currently showing you, extract view controllers from the array and reassemble them into a UISplitViewController.

The method configuredSplit(first:second:) is a helper to create a populated UISplitViewController.

Playing With the Splits

You’re ready to exercise the app, now!

Choose the iPad Pro (9.7-inch) simulator in the Target settings.

Build and run. You can play with a few layout scenarios to see how the trait change works in practice.

Place the app in two-thirds split screen. You can see that the app is still horizontally regular.

2/3 split view

Slide the divider and split the screen to 50/50. The app is now horizontally compact. The active EditorViewController is placed at the top of the navigation stack and a Back button is added to the editor.

equal split

Drag the system control at the top of the app down to convert the app to Slide Over. The editor Back button has been compacted a little.

slide over display

The reverse journey back to horizontally regular will reconstruct the double split. Give it a try!

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

You have now learned some basic UISplitViewController mechanics and how to handle the transitions from compact to regular, then back again.

You’ve gone beyond the basics and offered an extra level of hierarchical selection by using a split view embedded in a split view. If your app has a simple hierarchy like TreeWorld, this technique is great for giving users visibility of where they are in their data. However, a note of caution — more levels of hierarchy would make the UI quite confusing, and a single split view with navigation on the left and detail on the right is a safer option.

UISplitViewController has a lot more to offer in terms of free behavior and customization using UISplitViewControllerDelegate. This tutorial has just scratched the surface of possibilities.

Hopefully, this tutorial inspires some great adaptive designs. I look forward to seeing some of your results in the discussion forum below!