Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Building and improving your app with Mac Catalyst

At a glance

Item Summary
Purpose Evolves an iPad trip-planning app into a multiwindow Mac Catalyst app with native toolbars, sharing, printing, menus, and restoration.
App architecture A three-column split-view coordinator owns browser state; protocol-composed values are type-erased for heterogeneous UI; scene delegates adapt state and Mac-only chrome.
Main patterns Split-view coordinator, protocol composition/type erasure, observable selection model, scene restoration, responder-chain commands, and platform adapters.
Project style UIKit MVC-style app organized by Model, Item Detail, Printing, and Utility Extensions feature folders.

Project structure

Trip Planner/
├── AppDelegate.swift
├── SceneDelegate.swift
├── BrowserSplitViewController.swift
├── SidebarViewController.swift
├── SupplementaryViewController.swift
├── Model/
│   ├── DataModel.swift
│   ├── ModelItemsCollection.swift
│   └── Sample Data/SampleData.swift
├── Item Detail/
│   ├── DetailSceneDelegate.swift
│   ├── DetailViewController.swift
│   ├── LocatedItemDetailViewController.swift
│   └── RewardsProgramDetailViewController.swift
├── Printing/ItemPrintPageRenderer.swift
└── Utility Extensions/

Structure observations

  • Domain values and selection state live outside the view controllers.
  • The main browser and item-detail windows have separate scene delegates and restoration paths.
  • Mac Catalyst code is localized behind targetEnvironment(macCatalyst) rather than forked into a second app.

Overall architecture

Reference code

Trip Planner/BrowserSplitViewController.swift:31 — the split controller installs its column shells, observes one selection collection, and derives visible columns from that state.

setViewController(emptyNavigationController(), for: .secondary)
setViewController(emptyNavigationController(), for: .supplementary)

supplementarySelectionChangeObserver = NotificationCenter.default.addObserver(
    forName: .modelItemsCollectionDidChange,
    object: selectedItemsCollection,
    queue: nil
) { [weak self] _ in
    self?.updateDueToSelectedItemsChange()
}

updateDueToSelectedItemsChange()
updateDueToDetailItemChange()

Interpretation

BrowserSplitViewController is the high-level feature coordinator, not AppDelegate. It translates selection/detail state into columns, titles, printing targets, and a browser-state notification consumed by the scene’s restoration adapter.

Ownership and state

Ownership evidence

Trip Planner/Model/DataModel.swift:108AnyModelItem retains one private protocol value and exposes a uniform identity/name plus optional capabilities.

class AnyModelItem: NSObject, ModelItem {
    typealias ItemID = ItemIdentifierProviding.ItemID

    private let wrapped: ModelItem
    var itemID: ItemID { wrapped.itemID }
    var name: String { wrapped.name }

    var imageName: String? { (wrapped as? ImageProviding)?.imageName }
    var location: CLLocation? { (wrapped as? LocationProviding)?.location }
    var iconName: String? { (wrapped as? IconNameProviding)?.iconName }

    init(_ modelItem: ModelItem) {
        wrapped = modelItem
    }
}
Owner Object or state Relationship Mutation authority
BrowserSplitViewController selectedItemsCollection and detailItem Owns browser navigation state Selection collection operations and coordinator setter
ModelItemsCollection Ordered modelItems Owns mutable selection and posts changes add, remove, or restoration assignment
SceneDelegate Current NSUserActivity and Catalyst toolbar model Owns per-main-window integration state Browser-state observer and scene callbacks
DetailSceneDelegate Item activity and detail-window toolbar Owns per-detail-window state Scene configuration/restoration
DetailViewController One type-erased item and one chosen child controller Retains immutable input; creates child Private child-installation logic
AnyModelItem Wrapped ModelItem plus UI-facing favorite flag Retains erased domain value Wrapper initialization and local mutation

Class and protocol design

Type or protocol Responsibility Depends on or composes
Model Root access to countries and reward programs Concrete sample data
ModelItem Common identity and display name ItemIdentifierProviding
ImageProviding / LocationProviding / IconNameProviding Small optional domain capabilities Composed only where a model supports them
LocatedItem Alias for ModelItem & ImageProviding & LocationProviding Protocol composition
AnyModelItem Type-erase heterogeneous model items for collections and controllers Private ModelItem plus conditional capability casts
BrowserSplitViewController Coordinate primary, supplementary, and secondary columns Selection model, detail container, printing
DetailViewController Choose a concrete detail child for the erased item Located/reward capability views
Scene delegates Translate scene activities and Mac chrome into app state NSUserActivity, Catalyst toolbar delegates

This is protocol-oriented design with a pragmatic UIKit bridge: domain features are expressed as narrow protocols, while AnyModelItem provides the concrete, homogeneous reference type required by collection identity, Objective-C equality, and controller APIs.

Access control

Symbol Access Verified effect Why it fits this design
selectedItemsCollection private(set) Other app types can read/use the stable collection, but only the split controller can replace it. Preserves one selection identity for observers and injected consumers.
Selection observer and column-update helpers private Only the coordinator can run its derived-view synchronization. Prevents bypassing the state-to-columns flow.
AnyModelItem.wrapped private let Callers use the uniform/capability surface, not the erased value directly. Enforces type-erasure boundary.
DetailViewController.modelItem and child installer private The item’s interpretation remains inside the container. Keeps child-selection rules cohesive.
SceneDelegate.currentUserActivity private Only scene restoration code replaces the current activity. Avoids competing state snapshots.
Toolbar model properties/classes private and Catalyst-conditional Hidden from iOS code and other source files. Treats AppKit toolbar delegates as platform adapters.
SidebarViewController.dataSource fileprivate private(set) Same-file helpers can read it; mutation stays on the declaring type. Supports same-file snapshot helpers without broad module mutation.
Most app types implicit internal Available within the app target only. This sample is an app, not a reusable public framework.

Reference code

Trip Planner/BrowserSplitViewController.swift:10 — the coordinator exposes its current selection but guards replacement and synchronization internals.

class BrowserSplitViewController: UISplitViewController {
    private(set) var selectedItemsCollection = ModelItemsCollection()
    private var supplementarySelectionChangeObserver: Any?

    var selectedItems: [AnyModelItem] {
        selectedItemsCollection.modelItems
    }

    var detailItem: AnyModelItem? {
        didSet { updateDueToDetailItemChange() }
    }
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Column selection, detail navigation, titles, print routing BrowserSplitViewController One owner sees all three split-view columns and the responder chain.
Ordered selection mutations and change notification ModelItemsCollection Selection is reusable state, independent of one view.
Heterogeneous model capabilities and erasure DataModel.swift Domain shape should not depend on view-controller branching.
Concrete detail selection and child containment DetailViewController The detail container owns presentation context and child lifetime.
Main/detail window restoration SceneDelegate / DetailSceneDelegate NSUserActivity belongs to scene lifecycle boundaries.
Printing layout ItemPrintPageRenderer Printing rules remain out of navigation controllers.
Native Mac toolbar construction Private toolbar model types AppKit integration stays isolated and conditional.

Design patterns

Pattern Source evidence Purpose or tradeoff
Split-view coordinator Trip Planner/BrowserSplitViewController.swift:10 Centralizes relationships among three columns without putting domain data in the app delegate.
Observable selection model Trip Planner/Model/ModelItemsCollection.swift:11 Preserves ordered selection and notifies multiple UI/state consumers.
Protocol composition Trip Planner/Model/DataModel.swift:17 Models declare only capabilities they actually support.
Type erasure Trip Planner/Model/DataModel.swift:108 Makes heterogeneous values usable in homogeneous UIKit collections.
Container controller Trip Planner/Item Detail/DetailViewController.swift:67 Chooses and owns the specialized detail child.
Scene restoration adapter Trip Planner/SceneDelegate.swift:61 Persists stable item IDs rather than view-controller instances.
Responder-chain command routing Trip Planner/BrowserSplitViewController.swift:126 Sends Print to one focused detail or the aggregate browser.
Conditional platform adapter Trip Planner/SceneDelegate.swift:112 Adds native Mac chrome without contaminating shared domain logic.

Naming conventions

  • UI owners use role suffixes: BrowserSplitViewController, SidebarViewController, DetailSceneDelegate, and ItemPrintPageRenderer.
  • Capability protocols use “Providing”; the common entity role uses ModelItem.
  • AnyModelItem clearly signals type erasure; ModelItemsCollection signals ordered mutable selection rather than a repository.
  • State-transition methods are explicit: updateDueToSelectedItemsChange, restoreSelections, configure(with:), and updateUserActivity.

Architecture takeaways

  • Use a split-view coordinator as the owner of cross-column navigation state.
  • Model heterogeneous domain values with small capabilities, then type-erase only at the UIKit collection boundary.
  • Restore windows from stable model identifiers, not serialized UI objects.
  • Keep Catalyst-only toolbar adapters private and conditionally compiled while sharing the domain/navigation core.
  • Use private(set) to preserve the identity of observable state that collaborators still need to read and mutate through its API.

Source map

Source file Relevant symbols
Trip Planner/BrowserSplitViewController.swift Three-column coordination, printing, browser state
Trip Planner/Model/DataModel.swift Capability protocols, model values, AnyModelItem
Trip Planner/Model/ModelItemsCollection.swift Ordered selection owner and notification
Trip Planner/SceneDelegate.swift Main scene restoration and Catalyst toolbar
Trip Planner/Item Detail/DetailSceneDelegate.swift Item-window restoration and toolbar
Trip Planner/Item Detail/DetailViewController.swift Type-directed child containment
Trip Planner/Printing/ItemPrintPageRenderer.swift Print filtering and page rendering
Trip Planner/SidebarViewController.swift Hierarchical list and selection input