Multiple UISplitViewController Tutorial
This UISplitViewController tutorial shows you how to build an adaptive layout note-taking app using multiple UISplitViewControllers. By Warren Burton.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Multiple UISplitViewController Tutorial
35 mins
- Getting Started
- Model
- Views
- Installing the Root Split View Controller
- Creating a Split View Controller
- Managing State
- Updating the Root View Controller
- Updating the File Data Source
- Installing the File List Table View
- Updating the View Factory
- Installing the File List
- Filling in the Detail
- Am I Horizontally Regular?
- Adding Placeholder Views
- Handling the Navigation States
- Splitting the Split
- Creating a Split and Adding to View
- Choosing the Correct Split to Use
- Adding the Table View of Files
- Responding to State Changes
- Configuring Root View to Respond to State Changes
- Updating File Data Source to Trigger State Changes
- Responding to Selections
- Responding to Deletions
- Testing in Compact Environments
- Ensuring Correct Behavior When Traits Change
- Controlling the Reassembly
- Changing From Regular to Compact Traits
- Changing From Compact to Regular Traits
- Playing With the Splits
- Where to Go From Here?
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
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.
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
!