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

Detecting changes in the preferences window

At a glance

Item Summary
Purpose Reflects Mac Catalyst Preferences-window changes in the app immediately by observing UserDefaults with Combine.
App architecture Settings.bundle declares preferences; the app delegate recursively registers defaults; the main controller maps a persisted integer to UI color and retains one reactive subscription.
Main patterns Declarative settings schema, launch-time default registration, reactive binding, enum mapping, and typed UserDefaults facade.
Project style Three Swift files plus a Settings bundle; the system provides the Preferences UI.

Project structure

PreferencesWindowSample/
├── AppDelegate.swift
├── SceneDelegate.swift
├── ViewController.swift
├── Settings.bundle/
│   ├── Root.plist
│   └── additional preference panes
└── Base.lproj/Main.storyboard

Structure observations

  • No settings view controller exists: Mac Catalyst renders the declarative Settings bundle.
  • Registration/parsing is application startup logic in AppDelegate.
  • Typed preference access, value mapping, and UI subscription are colocated in ViewController.swift.

Overall architecture

Reference code

PreferencesWindowSample/ViewController.swift:36 — the controller retains a publisher pipeline that converts preference values into colors and assigns them directly to the view.

var subscriber: AnyCancellable?

override func viewDidLoad() {
    super.viewDidLoad()

    subscriber = UserDefaults.standard
        .publisher(
            for: \.backgroundColorValue,
            options: [.initial, .new]
        )
        .map {
            BackgroundColors(rawValue: $0)?
                .currentColor()
        }
        .assign(
            to: \UIView.backgroundColor,
            on: view
        )
}

Interpretation

The system preferences window and app UI communicate through persistent state, not direct controller callbacks. UserDefaults is the boundary, and Combine turns its KVO-compliant property into a live UI binding.

Ownership and state

Ownership evidence

PreferencesWindowSample/AppDelegate.swift:36 — launch setup builds a defaults dictionary from the bundle and gives it to the shared defaults store.

func registerDefaultPreferenceValues() {
    let preferenceSpecifiers =
        retrieveSettingsBundlePreferenceSpecifiers(
            from: "Root.plist"
        )
    let defaultValuesToRegister =
        parse(preferenceSpecifiers)
    UserDefaults.standard.register(
        defaults: defaultValuesToRegister
    )
}
Owner Object or state Relationship Mutation authority
Settings bundle Preference schema, keys, default values Static app resource Build-time plist configuration
System Preferences UI User choices Framework-generated UI User interaction
UserDefaults.standard Registered defaults and persisted selections Shared process/system store Registration and preferences writes
ViewController Optional AnyCancellable Retains observation for screen lifetime viewDidLoad assignment/replacement
Combine subscription Key-path observation and assignment target Retained by cancellable Publisher events/cancellation
SceneDelegate Storyboard window reference Per-scene lifecycle UIKit and size restriction setup

Class and protocol design

Type or extension Responsibility Depends on
AppDelegate Register defaults and recursively parse settings panes Bundle, plist dictionaries, UserDefaults
ViewController Map preference value and retain reactive UI binding Combine, BackgroundColors
BackgroundColors Give persisted integer values a typed color mapping UIColor
UserDefaults extension Expose keys as KVO-compliant typed properties Objective-C dynamic dispatch
SceneDelegate Restrict Catalyst window maximum size UIWindowScene

There is no app-defined protocol. The abstraction seams are declarative schema, a typed enum, and Combine’s publisher/subscriber contracts.

Access control

Symbol Access Verified effect Why it fits this design
All app types/functions implicit internal Available throughout the app module. The sample is not a reusable framework.
ViewController.subscriber implicit internal var Other module code could cancel/replace it. Only the controller owns it; private would express lifecycle more accurately.
Nested BackgroundColors implicit internal within controller scope Module code can refer to it as ViewController.BackgroundColors. Keeps mapping near its only consumer.
@objc dynamic defaults properties implicit internal Visible to Objective-C/KVO within the module. Dynamic dispatch is required for key-path publishing.
App-delegate parse/retrieve helpers implicit internal Callable across module though used only during launch. private would narrow implementation details.
private / fileprivate / public / private(set) Not used No explicit lexical/public boundary. Simplicity, not deliberate library exposure.

Reference code

PreferencesWindowSample/ViewController.swift:58 — the extension turns stringly stored keys into typed, observable properties without adding a separate preferences service.

extension UserDefaults {
    @objc dynamic var backgroundColorValue: Int {
        integer(forKey: "backgroundColorValue")
    }

    @objc dynamic var someRandomOption: Bool {
        bool(forKey: "someRandomOption")
    }
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Preference schema and default values Settings bundle System Preferences needs declarative metadata.
Recursive child-pane parsing and registration AppDelegate Runs once at application startup before UI consumption.
Persisted-int-to-color mapping BackgroundColors Typed domain mapping, independent of subscription mechanics.
Preference observation and UI assignment ViewController Owns target view and subscription lifetime.
Key strings/KVO facade UserDefaults extension Keeps raw-key access in one typed boundary.
Window size restriction SceneDelegate Per-window Catalyst concern.

Design patterns

Pattern Source evidence Purpose or tradeoff
Declarative settings schema PreferencesWindowSample/AppDelegate.swift:74 Lets the system build Preferences UI from bundled plists.
Default registration PreferencesWindowSample/AppDelegate.swift:37 Ensures reads have a valid baseline without overwriting user values.
Recursive composite traversal PreferencesWindowSample/AppDelegate.swift:46 Collects defaults across root and child panes.
Reactive binding PreferencesWindowSample/ViewController.swift:48 Updates UI on initial and subsequent preference values.
Enum mapping PreferencesWindowSample/ViewController.swift:14 Converts persisted integers into valid UI concepts.
Typed defaults facade PreferencesWindowSample/ViewController.swift:59 Centralizes keys and enables KVO key paths.

Naming conventions

  • BackgroundColors names the value domain, while enum cases mirror settings plist values.
  • backgroundColorValue ends in “Value” because it exposes persisted representation, not UIColor.
  • registerDefaultPreferenceValues and retrieveSettingsBundlePreferenceSpecifiers use explicit lifecycle/data verbs.
  • subscriber is generic; backgroundColorSubscription would describe ownership more precisely.

Architecture takeaways

  • Use Settings.bundle as the contract when the system owns Preferences UI.
  • Register defaults at launch, and keep registration separate from persisted user values.
  • Map persisted primitives into typed app concepts before updating UI.
  • Retain the cancellable at the screen owner so observation has an explicit lifetime.
  • Make subscription and parsing helpers private in production when no cross-type collaboration needs them.

Source map

Source file/resource Relevant symbols
PreferencesWindowSample/AppDelegate.swift Default registration and recursive plist parsing
PreferencesWindowSample/ViewController.swift Typed mapping, defaults facade, Combine binding
PreferencesWindowSample/SceneDelegate.swift Catalyst window restriction
PreferencesWindowSample/Settings.bundle/ System preference schema/defaults