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?
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.