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 2 of 4 of this article. Click here to view the first page.

Updating the Root View Controller

RootViewController needs to own a StateCoordinator to coordinate state changes.

Open RootViewController.swift.

Add this extension to the end of RootViewController.swift:

extension RootViewController: StateCoordinatorDelegate {
  func gotoState(_ nextState: SelectionState, file: File?) {

  }
}

Add these two properties above viewDidLoad():

let dataStack = CoreDataStack()
lazy var stateCoordinator: StateCoordinator = StateCoordinator(delegate: self)

Here, you add a StateCoordinator instance to RootViewController and ensure that RootViewController conforms to StateCoordinatorDelegate. RootViewController can now react to selection changes within the app.

The dataStack will hold the data that you create in the app.

Updating the File Data Source

The FileDataSource will want to send selection actions to the StateCoordinator. You need to add a property to FileDataSource.

Open the folder Model Layer in the Project navigator. Open FileDataSource.swift.

Add this property to the top of the main class above init(context:presenter:rootFolder:):

var stateCoordinator: StateCoordinator?

Installing the File List Table View

You now have almost enough structure to install a FileListViewController in the root split view. First, you need to add a couple of helpers to the view factory extension.

Updating the View Factory

Open RootViewController+ViewFactory.swift.

Add these methods to the body of the extension:

func configureFileList(_ fileList: FileListViewController,
                       title: String,
                       rootFolder: File?) {
  let datasource = FileDataSource(context: dataStack.viewContext,
                                  presenter: fileList,
                                  rootFolder: rootFolder)
  datasource.stateCoordinator = stateCoordinator
  fileList.fileDataSource = datasource
  fileList.title = title
}

func primaryNavigation(_ split: UISplitViewController)
  -> UINavigationController {
  guard let nav = split.viewControllers.first
    as? UINavigationController else {
    fatalError("Project config error - primary view doesn't have Navigation")
  }
  return nav
}

The method configureFileList(_:title:rootFolder:) takes a FileListViewController, creates a FileDataSource and assigns it to the FileListViewController. The FileDataSource is given a reference to the StateCoordinator.

Next, primaryNavigation(_:) recovers the UINavigationController from the primary view of a split view. For this app, the navigation controller should always exist, so you throw a fatalError to warn you when a bug has been created.

Installing the File List

Now, you’re ready to install the FileListViewController.

Open RootViewController.swift and find the method installRootSplit().

Add this code after the line addChild(rootSplitView):

let fileList = FileListViewController.freshFileList()
let navigation = primaryNavigation(rootSplitView)
navigation.viewControllers = [fileList]
configureFileList(fileList, title: "Folders", rootFolder: nil)

In this fragment, you instantiate a FileListViewController from a storyboard. You then recover the UINavigationController from the split view and assign the file list as the navigation’s root view controller.

Build and run. You can now see the FileListViewController inside a parent UINavigationController.

root folder list

You can add folders to the database, rename with a swipe-right and delete with a swipe-left.

Place the app in the background (Hardware ▸ Home) to trigger a save.

Note: Most of the logic in FileDataSource is taken from the Xcode Master-Detail App + Core Data project template. Check out this Core Data tutorial if you want to learn more about Core Data and NSFetchedResultsController.

Filling in the Detail

Take a look at all that empty space to the right of the table. In this part of the tutorial, you’re going to fill in that space with cool content.

Am I Horizontally Regular?

The iOS view system uses a system of size classes to let the developer know how to layout views. The size class for a view can change at any time either by system or user interaction. As of iOS 12, the size class for a given axis can be compact or regular.

As a rough rule of thumb, all iPad full screen and two-thirds width split screen are regular and the rest are compact. In most conditions, modern iOS apps should no longer modify layout based on device type or screen size.

You want this app to react to size class changes depending on whether or not the app is being run in a horizontally compact or regular environment.

Add this extension to the top of RootViewController.swift below import UIKit:

extension UIViewController {
  var isHorizontallyRegular: Bool {
    return traitCollection.horizontalSizeClass == .regular
  }
}

This code is a convenience method to tell you whether or not the app is in regular layout on the horizontal axis.

Adding Placeholder Views

When there is nothing selected, you’ll want to fill in the detail with a placeholder.

Open RootViewController+ViewFactory.swift and add this extension:

extension RootViewController {
  func freshFileLevelPlaceholder() -> PlaceholderViewController {
    let placeholder = PlaceholderViewController
      .freshPlaceholderController(
        message: "Select file or create. Swipe left to delete")
    return placeholder
  }
  
  func freshFolderLevelPlaceholder() -> PlaceholderViewController {
    let placeholder = PlaceholderViewController
      .freshPlaceholderController(message: """
        Select folder or create. Swipe left to delete or swipe right to rename
        """)
    return placeholder
  }
}

These two methods create PlaceholderViewController instances with contextual messages.

Return to RootViewController.swift.

Add this extension to the end of the file:

extension RootViewController {
  func showFileLevelPlaceholder(in targetSplit: UISplitViewController) {
    if isHorizontallyRegular {
      targetSplit.showDetailViewController(freshFileLevelPlaceholder(), sender: self)
    }
  }
  
  func showFolderLevelPlaceholder(in targetSplit: UISplitViewController) {
    if isHorizontallyRegular {
      rootSplitView.preferredPrimaryColumnWidthFraction = rootSplitLargeFraction
      targetSplit.showDetailViewController(freshFolderLevelPlaceholder(), sender: self)
    }
  }
}

These two methods will install a placeholder view in the secondary view of a split view but only if the current trait collection is regular. In a compact environment, you’ll never see the placeholders, because if you have nothing selected in the list, you’ll still be looking at the list.

You’re building the set of components you need to handle all trait collections. Soon, all your hard work will pay off!

Handling the Navigation States

You’ll want to know the state of the navigation controller in the rootSplitView. In this section, you’ll add logic to help with that.

In RootViewController.swift, add this enum to the main RootViewController class definition:

enum NavigationStackCompact: Int {
  case foldersOnly = 1
  case foldersFiles = 2
  case foldersFilesEditor = 3
}

This enum describes all the possible stacks that can exist in a compact environment.

Add this extension to the end of RootViewController.swift:

extension RootViewController: UINavigationControllerDelegate {
  func navigationController(_ navigationController: UINavigationController, 
                            didShow viewController: UIViewController, 
                            animated: Bool) {
    if navigationStack(navigationController, isAt: .foldersOnly) {
      showFolderLevelPlaceholder(in: rootSplitView)
    }
  }
  
  func navigationStack(
    _ navigation: UINavigationController, 
    isAt state: NavigationStackCompact
  ) -> Bool {
    let count = navigation.viewControllers.count
    if let value = NavigationStackCompact(rawValue: count) {
      return value == state
    }
    return false
  }
}

This delegate method, navigationController(_:viewController:animated:), allows you to detect a change to the navigation stack and install a placeholder when needed.

The helper method navigationStack(_:isAt:) in combination with NavigationStackCompact returns an answer to the question, “What is the navigation showing right now?” without splashing magic numbers across the code.

Finally, find the method installRootSplit() in the class definition and add this line at the end of the method:

navigation.delegate = self

Build and run. You should see the placeholder appear.

folder placeholder installed