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

4. Setting the Scene(s)
Written by Marin Bencevic

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapter, you learned how to add drag and drop capabilities to your app, making it feel much more natural for both iPad and Mac.

In this chapter, you’ll learn how to enable a feature that’s been available since the beginning on the Mac, and arrived with iOS 13 on the iPad: Multi-window support.

By the end of this chapter, you’ll learn:

  • What multi-window support is and why you’d want to enable it for your app.
  • How to enable basic multi-window support in Xcode and in your app.
  • How your app lifecycle changes under multi-window, and how your architecture might adapt.
  • How to add custom support for drag and drop window creation.

Ready to dive into the exciting world of multiple windows? Awesome! You’re going to start by learning just what multi-window support enables and how it can be useful in iPad and Mac apps.

Introducing Multiple Windows for iPad

In 2007, Apple unveiled the next generation of computing with the introduction of the iPhone. Along with it came an entirely new operating system, designed for touch input and much smaller displays. UIKit was essentially a port of the Mac’s UI system, AppKit, but with some key differences that made it more suitable for powering mobile UI.

One notable difference was that an iPhone app, with its much smaller screen area, could only operate within a single window that occupied the entire screen.

Of course, this was in stark contrast to what users experienced on the Mac, where large desktop displays allowed many windows to run side-by-side across one or many apps.

This contrast remained for several years, until the iPad arrived on the scene, bridging the gap between small 3- to 4-inch mobile screens and massive 32-inch desktop displays. Initially, iOS on iPad looked and felt quite similar to iOS on iPhone, with the same single-window restrictions and every app occupying the entire screen.

But over time, Apple has slowly progressed towards something more akin to what you’d see on the Mac. First, it added the ability to run apps side-by-side. Then it introduced tabs in apps like Safari. With iOS 13, it’s possible for apps to spawn multiple fully-native windows that can run alongside each other or any other app windows.

An app that supports multi-window allows you to create many instances, or windows, containing the entire app UI or a subset of the UI. Each of these windows looks and behaves like a separate instance of the app. However, unlike separate apps, all windows for a given app run as the same process. You’ll learn more about this later.

Why Multi-Window?

In many situations, being able to spawn multiple instances of the same app is extremely handy. Consider the following use-cases that are only possible with multi-window support:

Multi-Window in Action

There are many ways to spawn and interact with multiple app windows on iPad. Some come with the system. Others are specific to individual apps. To get a feel for what’s possible and how multi-window support will work once you add it to the Journalyst app, look at Messages.

Multiple Messages app on the iPad.
Jaxqohda Subciyof efg of rsu eFef.

Messages running side-by-side.
Zocvareb tevzicf ciqe-vr-nuye.

Show All Windows.
Tziy Eql Sijcorr.

Messages with contacts on one side and conversation on the other side.
Wiwluyog sinx tervalbc un uxe qace ahd pewkolxobiow az qru ilnus cupo.

Enabling Multi-Window in Xcode

Open the starter project for this chapter in Xcode and head over to the project settings. Click the Journalyst target and make sure you’re on the General tab. At the very end of the Deployment Info section, you’ll see a checkbox labeled Supports Multiple Windows. Go ahead and check it to, you guessed it, enable multi-window support.

Enable multiple windows in Xcode.
Icetpa gacnowlu liwnabc aw Ywuyi.

Introducing Scenes

In the pre-multi-window world, the entry point to every app was the app delegate. Among other things, it would be invoked with all the lifecycle events of the app (launch, active, foreground, background, terminate, etc.). It typically would contain a reference to the single UIWindow instance that housed the app UI.

Adding a Scene Delegate

Now that you’ve learned how scenes allow you to effectively manage multiple instances of your app’s UI, it’s time to finish enabling multi-window support for your project.

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let splitViewController =
      window?.rootViewController as? UISplitViewController {
      splitViewController.preferredDisplayMode =
      .oneBesideSecondary
    }
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
      <array>
        <dict>
          <key>UISceneConfigurationName</key>
            <string>Default Configuration</string>
	  <key>UISceneDelegateClassName</key>
	    <string>Journalyst.SceneDelegate</string>
	  <key>UISceneStoryboardFile</key>
	    <string>Main</string>
        </dict>
      </array>
  </dict>
</plist>
Multiple Journalyst windows.
Haybigpi Peajvutvdm jaqzovh.

Improving the Standard Multi-Window Experience

Remember that when iOS creates a new scene for your app, it’s instantiating an entirely new and parallel instance of everything required to build your UI. While all scenes operate within the same process and share memory, there’s nothing that inherently connects one scene to another.

@IBAction private func addEntry(_ sender: Any) {
  DataService.shared.addEntry(Entry())
  reloadSnapshot(animated: true)
}
func addEntry(_ entry: Entry) {
  entries.append(entry)
}
Hewe Vimcono Hiuc Daog Tuaq Zeflrodyiq Foit Bafqjinkos Qsesa 4 Zhuna 7
Zewmerwe waftavw woohsen.

extension Notification.Name {
  static var JournalEntriesUpdated =
  Notification.Name(
    "com.yourcompany.Journalyst.EntriesUpdated"
  )
}
func addEntry(_ entry: Entry) {
  entries.append(entry)
  // 1
  postUpdate()
}

func removeEntry(atIndex index: Int) {
  entries.remove(at: index)
  // 2
  postUpdate()
}
private func postUpdate() {
  // 3
  NotificationCenter.default.post(
    name: .JournalEntriesUpdated,
    object: nil
  )
}
@IBAction private func addEntry(_ sender: Any) {
  DataService.shared.addEntry(Entry())
}

override func tableView(
  _ tableView: UITableView,
  trailingSwipeActionsConfigurationForRowAt
  indexPath: IndexPath
) -> UISwipeActionsConfiguration? {

  let deleteAction = UIContextualAction(
    style: .destructive,
    title: "Delete"
  ) {_, _, _ in
    DataService.shared.removeEntry(atIndex: indexPath.row)
  }

  deleteAction.image = UIImage(systemName: "trash")
  return UISwipeActionsConfiguration(actions: [deleteAction])
}
// 1
NotificationCenter.default.addObserver(
  self,
  selector: #selector(handleEntriesUpdate),
  name: .JournalEntriesUpdated,
  object: nil
)
@objc func handleEntriesUpdate() {
  // 2
  reloadSnapshot(animated: false)
}
Add Entry reflected in multiple windows.
Erc Avxbs vipleymob ax kivmejka winjert.

protocol EntryTableViewControllerDelegate: AnyObject {
  func entryTableViewController(
    _ controller: EntryTableViewController,
    didUpdateEntry entry: Entry
  )
}
extension MainTableViewController:
  EntryTableViewControllerDelegate {
  func entryTableViewController(
    _ controller: EntryTableViewController,
    didUpdateEntry entry: Entry
  ) {
    reloadSnapshot(animated: false)
  }
}
func updateEntry(_ entry: Entry) {
  // 1
  var hasChanges = false
  entries = entries.map { item -> Entry in
    if item.id == entry.id && item != entry {
      // 2
      hasChanges = true
      return entry
    } else {
      return item
    }
  }
  // 3
  if hasChanges {
    postUpdate()
  }
}
Entry details updated in Scene 1 reflects in Scene 2.
Ugxfm piroelv etkigas ux Jnovu 7 rixjuycr ol Mrolo 7.

Adding Custom Drag Behavior to Create a New Window

Recall that when you explored multi-window support in the Messages app at the beginning of this chapter, you tried out a custom mechanism for spawning new scenes. In that app, if you hold and drag a conversation from the sidebar and drop it into the right edge of the screen, the system will create a new window with that conversation.

extension Entry {
  // 1
  static let OpenDetailActivityType
    = "com.yourcompany.EntryOpenDetailActivityType"
  static let OpenDetailIdKey = "entryID"
  // 2
  var openDetailUserActivity: NSUserActivity {
    // 3
    let userActivity = NSUserActivity(
      activityType: Entry.OpenDetailActivityType
    )
    // 4
    userActivity.userInfo = [Entry.OpenDetailIdKey: id]
    return userActivity
  }
}
extension MainTableViewController: UITableViewDragDelegate {
  // 1
  func tableView(
    _ tableView: UITableView,
    itemsForBeginning session: UIDragSession,
    at indexPath: IndexPath
  ) -> [UIDragItem] {
    // 2
    let entry = DataService.shared.allEntries[indexPath.row]
    let userActivity = entry.openDetailUserActivity
    // 3
    let itemProvider = NSItemProvider()
    itemProvider.registerObject(userActivity, visibility: .all)
    // 4
    let dragItem = UIDragItem(itemProvider: itemProvider)
    return [dragItem]
  }
}
tableView.dragDelegate = self
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let splitViewController = window?.rootViewController
      as? UISplitViewController {
      splitViewController.preferredDisplayMode =
      .oneBesideSecondary
    }
    // 1
    if let userActivity
      = connectionOptions.userActivities.first {
      // 2
      if !configure(window: window, with: userActivity) {
        print("Failed to restore from \(userActivity)")
      }
    }
  }

  func configure(
    window: UIWindow?,
    with activity: NSUserActivity
  ) -> Bool {
    // 3
    guard activity.activityType == Entry.OpenDetailActivityType,
      let entryID = activity.userInfo?[Entry.OpenDetailIdKey]
        as? String,
      let entry = DataService.shared.entry(forID: entryID),
      let entryDetailViewController
        = EntryTableViewController.loadFromStoryboard(),
      let splitViewController = window?.rootViewController
        as? UISplitViewController else {
      return false
    }

    // 4
    entryDetailViewController.entry = entry
    // 5
    let navController = UINavigationController(
      rootViewController: entryDetailViewController
    )
    splitViewController.showDetailViewController(
      navController,
      sender: self
    )
    return true
  }
}
Drag and drop an entry.
Bney uzm dkis uw onkqr.

Trying it on the Mac

The hard work you put in to make your app support multi-window for iPad has an added bonus: It’ll work seamlessly when you run the app on Mac. Open Xcode, select the My Mac destination and set your team. Then build and run.

Multiple windows running on the Mac.
Xisgizpe kucnoyx qansoyh oj cla Fap.

Key Points

  • Multi-window is a powerful way to be more productive on iPad and users expect to see it on Mac.
  • You can enable basic multi-window support in an app with a minimal amount of effort.
  • Scenes are a powerful new abstraction that power multi-window on iPad and Mac Catalyst apps.
  • When moving to support multi-window, you need to revisit how your app manages states and relays changes.
  • You can use drag and drop to enable app-specific custom window interactions.

Where to Go From Here?

In this chapter, you learned what multi-window support is, why you might want to incorporate it into your app, and how to do just that. You also learned about some of the issues that you might introduce when adopting scenes and how to resolve them. Finally, you learned how to go beyond the OS-provided multi-window support by adding a custom window interaction using drag and NSUserActivity.

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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now