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
flowchart LR
Schema["Settings.bundle plists"] --> Preferences["System Preferences window"]
Schema --> App["AppDelegate"]
App -->|"parse recursively + register defaults"| Defaults["UserDefaults.standard"]
Preferences -->|"write preference"| Defaults
Defaults -->|"KVO publisher"| Subscription["AnyCancellable pipeline"]
Subscription -->|"map raw Int"| Colors["BackgroundColors"]
Colors -->|"assign"| View["ViewController.view.backgroundColor"]
Scene["SceneDelegate"] -->|"max window size"| Window["UIWindowScene"]
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
classDiagram
SystemPreferences --> UserDefaults : writes values
AppDelegate --> SettingsBundle : reads schemas
AppDelegate --> UserDefaults : registers defaults
ViewController *-- AnyCancellable : subscriber lifetime
AnyCancellable --> UserDefaultsPublisher : subscription
AnyCancellable --> UIView : assignment target
ViewController *-- BackgroundColors : mapped value
SceneDelegate o-- UIWindow : scene window
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 |