Chapters

Hide chapters

Catalyst by Tutorials

Third Edition · iOS 15 · Swift 5.6 · Xcode 13.3

Section I: Making a Great iPad App

Section 1: 7 chapters
Show chapters Hide chapters

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.

Select an entry to see it's details.
Select an entry to see it's 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.

Attributes inspector of the split view controller.
Attributes inspector of the split view controller.

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!

Journalyst with a split view controller.
Journalyst 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.

Selecting first entry in Journalyst.
Selecting first entry in Journalyst.

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:

Adding images and sharing.
Adding images and sharing.

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:

Split view controller works on a Mac.
Split view controller works on a Mac.

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:

Background style of a side bar changed in Mac.
Background style of a side bar changed in Mac.

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.