Multiple UISplitViewController Tutorial
This UISplitViewController tutorial shows you how to build an adaptive layout note-taking app using multiple UISplitViewControllers.
Version
- Swift 4.2, iOS 12, Xcode 10

An essential part of a modern iOS app is the need to adapt to the device and environment that it’s run on. An app that runs on iPad has a lot of horizontal space to play with unless it’s run in Split Screen or Slide Over column mode. Your app should adapt to all possible layouts with grace.
UISplitViewController
is a very versatile component that can adapt to all trait collections with very little need for modification by the developer for simple cases. In this tutorial, you’ll learn the simple case, after which you’ll double the fun by placing a split view inside your split view, to add an extra level of hierarchy!
Getting Started
Download the starter app using the Download Materials button at the top or bottom of the page.
The project app, TreeWorld, is similar to the Notes app – there is a simple file system with folders, that can contain files. The files have text that you can edit.
Take a look around the project first. Open the TreeWorld.xcodeproj file in the TreeWorld-Starter folder. Expand the TreeWorld folder.
Model
Expand the Model Layer folder. Inside is a Core Data model with two entities:
-
File
represents a node in the file system. -
Content
represents a container for text.
Xcode automatically generates File.swift and Content.swift so that you can use them in the project.
CoreDataStack.swift is a simple Core Data setup that also saves the model when you background the app.
The model obeys these arbitrary rules:
- A folder is a
File
that hasisFolder
set totrue
. - A folder never has a
Content
object. - A folder can’t contain folders.
- A file is a
File
that hasisFolder
set tofalse
. - A folder can contain any number of child files.
- A file must have a
Content
object. - A file never has children.
FileDataSource.swift is a set of query and business logic for File objects, and it embodies the rules above.
Views
Inside the group Presentation Layer ▸ Member View Controllers is a set of pre-made view controllers and storyboards that you’ll use in this app.
EditorViewController.swift is a view to allow you to edit the Content
object of a File
.
FileListViewController.swift is a UITableViewController
to present File
objects. The FileDataSource
class provides the UITableViewDataSource
and UITableViewDelegate
implementations to keep unnecessary logic out of the view controller.
PlaceholderViewController.swift displays a message.
You can investigate them later if you want. They are composed of familiar UIKit components and should have no surprises for you.
Inside Presentation Layer ▸ Root View Controller is RootViewController.swift. This file is where you’ll make most of the changes in this tutorial. It will act as a coordinator for all the views.
Choose the iPad Pro (9.7-inch) simulator in the target settings. Build and run.
You should see a green view appear. You’re ready to start splitting, here.
Installing the Root Split View Controller
In this section, you’ll create and install the root UISplitViewController
that will exist for all layouts.
Creating a Split View Controller
Select the folder Presentation Layer ▸ View Controllers ▸ Root View Controller in the Project navigator. Add a new file with Command-N (or File ▸ New ▸ New File…).
- Select a new Swift File.
- Click Next.
- Name the file RootViewController+ViewFactory.
- Click Create.
Open RootViewController+ViewFactory.swift and add the following code:
import UIKit
extension RootViewController {
func freshSplitViewTemplate() -> UISplitViewController {
let split = UISplitViewController()
split.preferredDisplayMode = .allVisible
let navigation = UINavigationController()
split.viewControllers = [navigation]
return split
}
}
Here, you instantiate a UISplitViewController
and a UINavigationController
. You then set the navigation controller as the primary view controller of the split.
UISplitViewController
has a primary and a secondary view controller. You can set and get these controllers via the viewControllers
property.
To ensure both primary and secondary views are visible at the same time, you set the preferredDisplayMode
property to .allVisible
.
Open RootViewController.swift and add this code to the body of the main class:
let rootSplitSmallFraction: CGFloat = 0.25
let rootSplitLargeFraction: CGFloat = 0.33
lazy var rootSplitView: UISplitViewController = {
let split = freshSplitViewTemplate()
split.preferredPrimaryColumnWidthFraction = rootSplitLargeFraction
split.delegate = self
return split
}()
override func viewDidLoad() {
super.viewDidLoad()
installRootSplit()
}
func installRootSplit() {
view.addSubview(rootSplitView.view)
view.pinToInside(rootSplitView.view)
addChild(rootSplitView)
}
Add this extension to the end of the file:
extension RootViewController: UISplitViewControllerDelegate {
//you'll add to this later
}
In this fragment, you create a UISplitViewController
and embed it within the RootViewController
. You also set the primary view to take 33% of the available width.
Build and run. You should see the empty navigation bar for the primary view in the top-left. There’s not much to look at, yet.
Managing State
You now need to set up some infrastructure. The app can have three possible selection states that you want to keep track of:
- Nothing selected
- Folder selected
- File selected
Select the folder Presentation Layer ▸ View State in the Project navigator. Create a new Swift file (Command-N) and name it SelectionState.
Add this code to the file:
enum SelectionState {
case noSelection
case folderSelected
case fileSelected
}
Now, create a second file in the View State folder. Name this file StateCoordinator.swift.
Add this code to the body of StateCoordinator.swift:
import UIKit
// 1
protocol StateCoordinatorDelegate: class {
func gotoState(_ nextState: SelectionState, file: File?)
}
// 2
class StateCoordinator: NSObject {
// 3
private(set) var state: SelectionState = .noSelection
private weak var delegate: StateCoordinatorDelegate?
init(delegate: StateCoordinatorDelegate) {
self.delegate = delegate
}
// 4
private(set) var selectedFile: File? {
didSet {
guard let file = selectedFile else {
state = .noSelection
return
}
state = file.isFolder ? .folderSelected : .fileSelected
}
}
// 5
var selectedFolder: File? {
guard let file = selectedFile else {
return nil
}
return file.isFolder ? file : file.parent
}
}
Reviewing step by step:
- First, you declare a delegate protocol,
StateCoordinatorDelegate
, to allow another object to react to changes in theStateCoordinator
. - Next, you declare the
StateCoordinator
class. This class has the job of keeping track of the current selectedFile
. The class will change to the appropriateSelectionState
when aFile
is selected or deleted. -
StateCoordinator
has two properties: one forSelectionState
that is publicly read-only and the other that is a weak reference to theStateCoordinatorDelegate
. - The
selectedFile
property is publicly read-only and uses a property observer to change thestate
. - The
selectedFolder
property is computed and allows you to get the currently selected top-level folder.
Now, add this extension to the end of StateCoordinator.swift:
extension StateCoordinator {
func didSelectFile(_ file: File?) {
selectedFile = file
delegate?.gotoState(state, file: selectedFile)
}
func didDeleteFile(parentFolder: File?) {
selectedFile = parentFolder
state = .noSelection
delegate?.gotoState(state, file: selectedFile)
}
}
Here, you have two public APIs to allow callers to change the state in a managed way.
didSelectFile(_:)
tells the StateCoordinator
that a File
was selected. This has the effect of setting the state
and notifying the downstream delegate that this happened.
didDeleteFile(parentFolder:)
is almost the same as didSelectFile
, but it ensures that the state
is set to noSelection
before notifying the delegate.
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
.
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.
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.
Splitting the Split
You’re now ready to install a UISplitViewController
inside a UISplitViewController
.
Creating a Split and Adding to View
You want to place a UISplitViewController
inside rootSplitView
like this:
In RootViewController.swift, insert this method below viewDidLoad()
:
func installDoubleSplitWhenHorizontallyRegular() -> UISplitViewController? {
guard isHorizontallyRegular else {
return nil
}
if let subSplit = rootSplitView.viewControllers.last
as? UISplitViewController {
return subSplit
}
let split = freshSplitViewTemplate()
split.delegate = self
rootSplitView.preferredPrimaryColumnWidthFraction = rootSplitSmallFraction
rootSplitView.showDetailViewController(split, sender: self)
return split
}
This method checks to see if there is a split view installed in the secondary view, and it returns the if it exists. Otherwise, a new split view is created and returned.
Choosing the Correct Split to Use
When you add the list of files contained in a folder, you’ll target a different split view depending on the current trait collection. This will happen in several places, so you’ll place all that logic inside a helper method.
Add this method below the method installDoubleSplitWhenHorizontallyRegular()
:
func targetSplitForCurrentTraitCollection() ->
UISplitViewController {
if isHorizontallyRegular {
guard let subSplit = installDoubleSplitWhenHorizontallyRegular() else {
fatalError("you must have a UISplitViewController here")
}
return subSplit
} else {
return rootSplitView
}
}
This method always returns the correct split view depending on the trait collection.
Adding the Table View of Files
The list of folders will always appear in the primary navigation controller of rootSplitView
, but the list of files can either appear:
- Stacked in the primary navigation.
- As the root view controller of the sub-split’s primary navigation.
In RootViewController.swift, find the extension that contains the method showFolderLevelPlaceholder()
.
Add this code inside the extension:
func installFileList(fileList: FileListViewController) {
if
isHorizontallyRegular,
let subSplit = installDoubleSplitWhenHorizontallyRegular() {
//1
let navigation = primaryNavigation(subSplit)
navigation.viewControllers = [fileList]
//2
subSplit.preferredDisplayMode = .allVisible
subSplit.preferredPrimaryColumnWidthFraction = rootSplitLargeFraction
rootSplitView.preferredPrimaryColumnWidthFraction = rootSplitSmallFraction
//3
showFileLevelPlaceholder(in: subSplit)
} else {
let navigation = primaryNavigation(rootSplitView)
navigation.pushViewController(fileList, animated: true)
}
}
When the app has a regular horizontal width:
- You install the
FileListViewController
as the root view of the sub-split’sUINavigationController
. - You configure the split’s
preferredPrimaryColumnWidthFraction
property to divide the screen 25/25/50. - You install a placeholder in the detail view.
Otherwise, not in a regular horizontal width, you push the FileListViewController
onto the UINavigationController
in the rootSplitView
.
Responding to State Changes
Earlier, you configured RootViewController
as the delegate of the StateCoordinator
. The StateCoordinator
calls gotoState(_:file:)
on its delegate when the selection state changes.
In this next section, you’ll add the glue code to allow the UI to react to those changes.
Configuring Root View to Respond to State Changes
Within RootViewController.swift, find the method gotoState(_:file:)
in the StateCoordinatorDelegate
extension.
Add these four helpers below that method:
//1
func gotoNoSelection(_ folder: File?) {
let navigation = primaryNavigation(rootSplitView)
if navigationStack(navigation, isAt: .foldersOnly) && folder == nil {
showFolderLevelPlaceholder(in: rootSplitView)
} else {
showFileLevelPlaceholder(in: targetSplitForCurrentTraitCollection())
}
}
//2
func gotoFolderSelected(_ folder: File) {
if folder.isFolder {
let fileList = FileListViewController.freshFileList()
let title = folder.name ?? "Untitled"
configureFileList(fileList, title: title, rootFolder: folder)
installFileList(fileList: fileList)
}
}
//3
func gotoFileSelected(_ file: File) {
if !file.isFolder {
let detail = EditorViewController.freshDetailController(file: file)
let navigation = freshNavigationController(rootViewController: detail)
targetSplitForCurrentTraitCollection()
.showDetailViewController(navigation, sender: self)
}
}
//4
func freshNavigationController(rootViewController: UIViewController)
-> UINavigationController {
let nav = UINavigationController(rootViewController: rootViewController)
nav.navigationBar.prefersLargeTitles = true
return nav
}
-
gotoNoSelection(_:)
checks the state of the root navigation and makes a decision regarding which placeholder to install. This method is called when the user has deleted a file or a folder. When the user deletes aFile
, theStateCoordinator
passes theparent
File
to theStateCoordinatorDelegate
. A folder has no parent, so thefile
argument will benil
. -
gotoFolderSelected(_:)
creates a newFileListViewController
, configures that file list with a reference to the selected parent folder, and then it installs the file list in the correct place. -
gotoFileSelected(_:)
creates a newEditorViewController
with a reference to the selected file, places that editor inside aUINavigationController
, and then it installs that navigation into the secondary view of the relevant split. -
freshNavigationController(rootViewController:)
creates a newUINavigationController
instances with the correct configuration.
Finally, you’ll fill in the body of gotoState(_:file:)
with this code:
if nextState == .folderSelected, let folder = file {
gotoFolderSelected(folder)
} else if nextState == .fileSelected, let file = file {
gotoFileSelected(file)
} else if nextState == .noSelection {
gotoNoSelection(file)
}
With this code, you pick the correct destination based on the nextState
argument.
You have finished with the RootViewController
for now!
Updating File Data Source to Trigger State Changes
Responding to Selections
The RootViewController
can now react to the changes broadcast by the StateCoordinator
but nothing is triggering those changes yet.
In this section, you’ll add the code to let FileDataSource
connect to StateCoordinator
.
Open the Model Layer folder in the Project navigator and select FileDataSource.swift.
Find the extension extension FileDataSource: UITableViewDelegate
.
Add this code inside the body of the method tableView(_:didSelectRowAt:)
:
stateCoordinator?.didSelectFile(object(at: indexPath))
When you touch a cell in FileListViewController
, the delegate method tableView(_:didSelectRowAt:)
in FileDataSource
is called.
In turn, FileDataSource
tells the StateCoordinator
that a File
was selected, at which point the StateCoordinator
changes state and tells the RootViewController
about that change.
Responding to Deletions
You also want to address the deletion of files as that action can change the selection state. In this section, you’ll add the code to address this.
Find the extension extension FileDataSource: UITableViewDataSource
.
Find the method deleteAction(_:)
and replace the line try operations.delete(file: file, force: true)
inside the do
block with this code:
//1
let parent = file.parent
//2
try operations.delete(file: file, force: true)
//3
stateCoordinator?.didDeleteFile(parentFolder: parent)
In this change, you:
- Get a reference to the
parent
of theFile
before deleting from the database. - Delete the file.
- Tell the
StateCoordinator
that a deletion happened.
Build and run to see all your hard work pay off.
When you select a folder, a second FileListViewController
appears wherein you can create, rename and delete file objects.
Testing in Compact Environments
Thanks to UISplitViewController
, your app will also adapt to running on iPhone.
Pick the iPhone 8 simulator target.
Build and run. Your UISplitViewController
now behaves like a UINavigationController
. You’ll start with an empty folder list, because this is a new device. Add a folder:
Select it and go to the files list, where you can add files:
Select a file to go to the editor:
That’s great! When running in a compact environment, you see one view at a time, because there isn’t room for more. That’s the power of UISplitViewController
!
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.
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.
- First, you get a reference to the root navigation controller and take a snapshot of its view controller stack.
- Next, you check if there’s a split in the secondary position.
- You append the
FileListViewController
from that split onto the destination stack. - If the secondary view in the secondary split is an
EditorViewController
, you append that also. - Finally, reset the navigation stack with its new contents.
- 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(_:)
:
- Take a copy of the current navigation stack and root view controller in that stack.
- Use
defer
to reset the inputUINavigationController
to only the folder list and correct the column width for double-split views. - 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.
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.
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.
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!
Comments