# Managing Application Preferences in OpenSuperWhisper: A Swift Best Practices Guide

> Master OpenSuperWhisper application preferences with this Swift guide. Discover type-safe `AppPreferences`, property wrappers, schema migration, and NotificationCenter for seamless UI updates.

- Repository: [Starmel/OpenSuperWhisper](https://github.com/Starmel/OpenSuperWhisper)
- Tags: best-practices
- Published: 2026-07-05

---

**OpenSuperWhisper centralizes all user settings in a type-safe `AppPreferences` singleton that uses property wrappers for compile-time safety, automatic migration for schema changes, and `NotificationCenter` for decoupled UI updates.**

Managing application preferences effectively is crucial for maintaining clean architecture in macOS apps. In OpenSuperWhisper, all user-configurable settings are handled through a centralized singleton defined in [`OpenSuperWhisper/Utils/AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AppPreferences.swift), eliminating string-typed keys and scattered state. This approach demonstrates modern Swift patterns for managing application preferences that ensure consistency across the Settings UI, transcription engines, and global hotkey handlers.

## Centralizing State with a Singleton Pattern

The foundation of preference management in OpenSuperWhisper is the `AppPreferences` singleton. Rather than accessing `UserDefaults` directly throughout the codebase, every component reads from and writes to `AppPreferences.shared`, creating a single source of truth for all configuration state.

### Single Source of Truth

All settings are ultimately stored in `UserDefaults`, but the singleton acts as the exclusive gateway. This guarantees that every part of the application—from the transcription engine to the settings panel—reads identical values. For example, [`WhisperEngine.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/WhisperEngine.swift) and [`FluidAudioEngine.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/FluidAudioEngine.swift) both query `AppPreferences.shared` to locate model files, ensuring engine selection remains synchronized.

### Type Safety Through Property Wrappers

Preference keys are never raw strings scattered through the code. Instead, OpenSuperWhisper implements custom property wrappers (`@UserDefault` and `@OptionalUserDefault`) that map Swift types to storage keys at compile time.

```swift
// Reading a preference returns the correct type immediately
let language = AppPreferences.shared.whisperLanguage   // "en" by default

// Writing updates UserDefaults automatically through the wrapper
AppPreferences.shared.translateToEnglish = true

```

This design prevents type mismatches and eliminates the need for manual casting or key management throughout the application.

## Handling Schema Changes and Default Values

### Automatic Migration

When the preference schema changes, `AppPreferences.init()` calls `migrateOldPreferences()` to lazily migrate legacy keys. This ensures existing users retain their settings after updates without requiring manual intervention or data loss.

### Consistent Defaults

Every property declares a sensible default value (e.g., `translateToEnglish: false`, `temperature: 0.0`). This guarantees the app functions out-of-the-box and makes unit testing deterministic by ensuring predictable initial states.

## Encapsulating Complex Logic

### Derived Preferences

Some preferences depend on others, and OpenSuperWhisper encapsulates this logic within the preferences class rather than duplicating it in UI or engine layers. For example, `selectedModelPath` forwards to `selectedWhisperModelPath` when the engine is set to *whisper*.

```swift
// UI code remains agnostic about which underlying key to query
if let modelPath = AppPreferences.shared.selectedModelPath {
    // Use the path for transcription regardless of engine type
}

```

This abstraction prevents view controllers and engine implementations from needing to know about the internal preference schema.

## Decoupling UI with Notifications

Changes that affect the UI—such as language changes—are broadcast through `NotificationCenter` using constants defined in `OpenSuperWhisper/Utils/NotificationName+App.swift`. Consumers subscribe only to notifications they care about, keeping the preference store free of UI code.

```swift
NotificationCenter.default.addObserver(
    forName: .appPreferencesLanguageChanged,
    object: nil,
    queue: .main
) { _ in
    // Reload UI strings for the new language
    self.reloadLocalizedStrings()
}

```

## Integration Points Across the Application

### Settings UI

The preferences user interface in [`Settings.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/Settings.swift) reads and writes properties directly through the singleton:

```swift
AppPreferences.shared.selectedEngine = newEngine

```

### Transcription Engines

Both [`WhisperEngine.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/WhisperEngine.swift) and [`FluidAudioEngine.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/FluidAudioEngine.swift) query specific preferences to locate their respective resources. The Whisper engine accesses `AppPreferences.shared.selectedWhisperModelPath`, while the FluidAudio engine checks `AppPreferences.shared.fluidAudioModelVersion`.

### Hotkey Management

[`ShortcutManager.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/ShortcutManager.swift) pulls configuration values directly from the singleton to register global shortcuts:

```swift
let modifierHotkey = AppPreferences.shared.modifierOnlyHotkey
let mouseHotkey = AppPreferences.shared.mouseButtonHotkey

```

## Testing Strategy

The singleton can be reset in tests by clearing `UserDefaults` and reinstantiating `AppPreferences.shared`. Since all preferences are value types with explicit defaults, assertions remain trivial and deterministic.

```swift
func testLanguagePreference() {
    // Reset defaults for a clean test environment
    UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
    
    // Initialize the shared instance (migration runs automatically)
    let prefs = AppPreferences.shared
    XCTAssertEqual(prefs.whisperLanguage, "en")   // default
    
    prefs.whisperLanguage = "ja"
    XCTAssertEqual(prefs.whisperLanguage, "ja")
}

```

## Summary

- Use a **singleton** (`AppPreferences.shared`) as the single source of truth for all settings, defined in [`OpenSuperWhisper/Utils/AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AppPreferences.swift)
- Implement **property wrappers** (`@UserDefault`, `@OptionalUserDefault`) to ensure compile-time type safety and eliminate string-typed keys
- Provide **default values** for every preference to guarantee out-of-the-box functionality and deterministic testing
- Handle **schema migrations** lazily in the initializer via `migrateOldPreferences()` to preserve user data across updates
- Encapsulate **derived values** within the preferences class to prevent logic duplication across UI and engine layers
- Use **NotificationCenter** (via `NotificationName+App.swift`) to broadcast changes without coupling the storage layer to UI code
- Access preferences directly in **engines**, **UI**, and **hotkey managers** via the shared singleton to maintain consistency

## Frequently Asked Questions

### How does OpenSuperWhisper ensure type safety when storing preferences?

It uses custom Swift property wrappers (`@UserDefault` and `@OptionalUserDefault`) that map specific Swift types to `UserDefaults` keys. This prevents type mismatches at runtime and eliminates the need for raw string keys scattered throughout the codebase, as all access flows through the typed properties of `AppPreferences.shared`.

### Where does the app handle migration of old preference keys?

Migration occurs lazily inside `AppPreferences.init()` via the `migrateOldPreferences()` method. This approach ensures existing users retain their settings when the schema changes, running only once when the singleton first initializes rather than on every launch.

### How do UI components know when a preference has changed?

UI components observe specific notifications broadcast via `NotificationCenter` using constants defined in `OpenSuperWhisper/Utils/NotificationName+App.swift`. For example, language changes trigger `.appPreferencesLanguageChanged`, allowing views to reload without the preference store knowing about specific UI implementation details.

### Can the AppPreferences singleton be tested independently?

Yes. Tests can reset the singleton by clearing `UserDefaults.standard` for the app bundle identifier and then re-instantiating `AppPreferences.shared`. Since all preferences are value types with explicit defaults, the test environment remains deterministic and isolated from other test cases.