Chapter 2.4  ·  Gate 02.4 ·  The App Environment

Settings

User-facing configuration — preferences, feature toggles, anything the user changes and expects to persist — lives in a settings object backed by a key-value store. This subchapter covers that shape: the one model simple enough to be its own manager.

Settings objects

The settings object is its own manager: there’s no separate persistence layer to coordinate, so a setter that writes straight through is the whole implementation.

@Observable @MainActor
final class EditorSettings {
    static let shared = EditorSettings()

    @UserDefault("editorFontSize", defaultValue: 13)
    var fontSize: Int

    @UserDefault("showLineNumbers", defaultValue: true)
    var showLineNumbers: Bool

    private init() { }
}

A view reads it directly — no injection, no protocol, just the shared instance:

struct PreferencesView: View {
    let settings = EditorSettings.shared

    var body: some View {
        Stepper("Font size: \(settings.fontSize)",
                value: Bindable(settings).fontSize, in: 10...24)
    }
}

Two boundaries keep this shape honest. It’s @Observable only because nothing ever writes to it from a background job — the moment background work funnels through a settings object, it stops qualifying and becomes a regular manager-fronted model; Chapter 5 draws that line precisely. And it holds user configuration, not launch configuration — the values a subsystem needs before it can start at all are a configure(_:)d struct, covered in Object Wiring.

Settings get their own full treatment as one shape in the Model layer’s pattern catalog — see Chapter 5.