2.
Migrating to Split View Controller
Written by Andy Pereira
The split view controller provides a way to manage two view controllers at the same time. The split view controller takes advantage of the iPad’s significantly larger screen size, making it easy to display a master-detail style interface. This also makes it easy to adapt to the screen size of the device someone may be using. In this chapter, you’ll convert the iPhone “master-detail” pattern found in Journalyst to a split view controller, then update the UI to take advantage of the view.
Getting Started
Open the starter project for the chapter. Select an iPad Pro simulator in the active scheme, then build and run. In its current state, the iPad version of Journalyst requires you to select an entry before you can see the details.
Integrating Split View Controller
Open Main.storyboard and select the Entry Table View Controller scene. From the menu bar, select Editor ▸ Embed In ▸ Navigation Controller.
This inserts a navigation controller and keeps the segue you had before, but you need to change a property of it. Select the segue between the Journalyst scene and your new navigation controller and in the Attributes inspector change the Kind to Show Detail.
The segue already has a segue action attached to it from the starter project, and you won’t need to change that.
Now, open MainTableViewController.swift and add the following property:
var entryTableViewController: EntryTableViewController?
The segue action connection didn’t need to change, but with your new navigation controller, the code it runs will. Replace entryViewController(coder:sender:segueIdentifier:)
with the following:
@IBSegueAction func entryViewController(
coder: NSCoder,
sender: Any?,
segueIdentifier: String?
) -> UINavigationController? {
guard let cell = sender as? EntryTableViewCell,
let indexPath = tableView.indexPath(for: cell),
let navigationController
= UINavigationController(coder: coder),
let entryTableViewController
= navigationController.topViewController as?
EntryTableViewController else { return nil }
entryTableViewController.entry
= dataSource?.itemIdentifier(for: indexPath)
self.entryTableViewController = entryTableViewController
return navigationController
}
It’s important to understand that an NSCoder
gets passed into this method, where it’s used to initialize the navigation controller found in the storyboard. Notice that once the navigation controller is initialized, an EntryTableViewController is already the topViewController
.
Back in Main.storyboard, open the Library by clicking the add button at the top-right of the canvas. Then, drag a Split View Controller to your canvas. Delete the following scenes from the canvas attached to the split view controller:
- Navigation Controller
- Root View Controller
- View Controller
Next, control-drag from the split view controller to the navigation controller attached to the Journalyst scene, and select primary view controller. Then, connect the split view controller to the navigation controller of the entry table view scene, and select secondary view controller.
Finally, select the split view controller scene and in the Attributes inspector, set the following properties:
- Is Initial View Controller
- Style is set to Double Column.
You’re almost there!
In AppDelegate.swift, add the following to the end of the file:
extension AppDelegate: UISplitViewControllerDelegate {
func splitViewController(
_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController
) -> Bool {
guard let secondaryNavigationController
= secondaryViewController as? UINavigationController,
let entryTableViewController
= secondaryNavigationController.topViewController
as? EntryTableViewController else {
return false
}
if entryTableViewController.entry == nil {
return true
}
return false
}
}
This conforms AppDelegate to UISplitViewControllerDelegate
. Here, it is simply configuring the behavior of how the split view controller will handle the view on the right hand side of the controller.
Last, add the code below to the main body of AppDelegate:
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
if let window = window,
let splitViewController
= window.rootViewController as? UISplitViewController {
splitViewController.preferredDisplayMode = .automatic
splitViewController.delegate = self
}
return true
}
Here, you set the preferredDisplayMode
of split view controller to automatic. This wraps up the work needed to get your split view controller to work.
Build and run. Rotate the simulator orientation to landscape and your app is now updated with a split view controller!
Updating the UI
Even though you have a working split view controller, you’ll notice that not everything is working smoothly. The first entry in the list does not automatically get shown in the EntryTableViewController. To get that working, open MainTableViewController.swift and add this code to viewDidLoad
:
if let splitViewController = splitViewController,
let splitNavigationController
= splitViewController.viewControllers.last
as? UINavigationController,
let topViewController
= splitNavigationController.topViewController
as? EntryTableViewController {
entryTableViewController = topViewController
}
This will give the view controller access to the EntryViewController present on the right side of the screen.
Next, replace populateMockData
with the following:
private func populateMockData() {
reloadSnapshot(animated: false)
if let entryTableViewController = entryTableViewController,
let entry = entries.first,
entryTableViewController.entry == nil {
tableView.selectRow(
at: IndexPath(row: 0, section: 0),
animated: false,
scrollPosition: .top
)
entryTableViewController.entry = entry
}
}
Now, whenever the data is initially loaded, the first entry in the table view will get selected automatically.
Build and run everything again. You see the time of the first entry appear at the top of the EntryTableViewController navigation controller.
We’re getting close, but there are still a few more pieces to consider. Currently, text added to an entry doesn’t save, and adding an entry image or trying to share an entry causes a crash.
Open EntryTableViewController.swift and add the following to the UITextViewDelegate
extension:
func textViewDidEndEditing(_ textView: UITextView) {
entry?.log = textView.text
}
Now, when you switch entries in the left-hand side, the text contained in the text view will be saved to the entry you were previously editing.
Next, you need to fix image and sharing functionality. When the app is running on an iPhone, the system will handle showing the activity controller and action sheet for you. However, on iPad you need to do a little more for it to work.
Because iPad wants to present the action sheet in a UIPopoverPresentationController
, you’ll need to tell it where to present from. In addImage(_:)
, add the following just before the last line of the method:
if let sender = sender,
let popoverController =
actionSheet.popoverPresentationController {
popoverController.sourceRect =
CGRect(
x: sender.frame.midX,
y: sender.frame.midY,
width: 0,
height: 0
)
popoverController.sourceView = sender
}
Here, you check if there is a popover presentation controller, and then set the sourceRect
and sourceView
properties. Now, when you tap the camera button, the modal will be presented from it.
Last, add the following in share(_:)
. Once again, place this code just before the last line:
if let popoverController =
activityController.popoverPresentationController {
popoverController.barButtonItem =
navigationItem.rightBarButtonItem
}
While very similar to the previous additions for addImage(_:)
, in this case, the share button is in a navigation bar. Therefore, you simply set the barButtonItem
of the popoverController. Now, both modals will work properly.
Build and run. Now, add some text to a journal entry, add a few images and then share.
Everything is in working order:
macOS Split Views
In traditional macOS apps, split views have long been the standard way of presenting a UI in a similar way to the split view controller. Catalyst makes it easy to take a split view controller and turn it into something that looks right at home on a Mac.
To see what the app looks like on a Mac, change the device in your active scheme to My Mac, then build and run again and add a few more journal entries:
You see the app looks almost exactly the same as it did on iPad.
Note: Don’t forget to set your team in your project’s settings for Signing & Capabilities.
There are a few pieces of the user interface that aren’t quite right, like the entry table view. Typically, the side views on Mac have some transparency. The first thing you can do to address this is set the split view controller’s primary background style.
Open AppDelegate.swift and add the following to the end of the if let
block, right after setting splitViewController.delegate
:
splitViewController.primaryBackgroundStyle = .sidebar
This style will not have any affect on iPad or iPhone apps, but will make your app feel much more at home on a Mac.
Build and run one last time, and again, add a few entries. You’ll see there’s a much better look and feel now:
Key Points
- Split view controllers make it easy to create an iOS and macOS app from the same codebase.
- Popovers on iPadOS and macOS are handled identically.
- There is minimal work to make a split view controller look proper on macOS.
Where to Go From Here?
This chapter got you from an iPhone-only app to a working iPad and macOS app that feels right at home on each platform. You learned what changes you need to make to handle the different platforms, and how much Catalyst will handle for you.
You can learn more about the following subjects here:
- UISplitViewController: Check out Apple’s documentation.
- IBSegueAction: Watch WWDC 2019 video.
- Catalyst: Read Apple’s human interface guideline.