# How to Configure Application Preferences in OpenSuperWhisper: A Developer's Guide

> Learn how to configure application preferences in OpenSuperWhisper. This guide details using AppPreferences, @UserDefault, and @OptionalUserDefault for seamless settings management.

- Repository: [Starmel/OpenSuperWhisper](https://github.com/Starmel/OpenSuperWhisper)
- Tags: how-to-guide
- Published: 2026-07-05

---

**OpenSuperWhisper stores all user settings in a singleton `AppPreferences` object that wraps macOS `UserDefaults`, exposing values through `@UserDefault` and `@OptionalUserDefault` property wrappers for automatic persistence and UI binding.**

OpenSuperWhisper is a macOS transcription app that persists user settings using a clean, property-wrapper-based architecture. Whether you are customizing the app through the Settings UI or modifying values programmatically, all configuration flows through the `AppPreferences` singleton defined in [`OpenSuperWhisper/Utils/AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AppPreferences.swift).

## Understanding the Preferences Architecture

The preferences system centers on a single source of truth: the `AppPreferences` class. This singleton manages every user-adjustable setting via type-safe property wrappers that automatically synchronize with `UserDefaults`.

### The AppPreferences Singleton

In [`OpenSuperWhisper/Utils/AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AppPreferences.swift), the `AppPreferences` class creates one shared instance accessible via `static let shared`. All preference properties are defined on this singleton, ensuring every part of the application reads from and writes to the same underlying storage.

```swift
class AppPreferences {
    static let shared = AppPreferences()
    
    @UserDefault(key: "selectedEngine", defaultValue: "whisper")
    var selectedEngine: String
    
    @UserDefault(key: "whisperLanguage", defaultValue: "en")
    var whisperLanguage: String
    
    @OptionalUserDefault(key: "selectedWhisperModelPath")
    var selectedWhisperModelPath: String?
}

```

### Property Wrappers: UserDefault vs OptionalUserDefault

OpenSuperWhisper implements two custom property wrappers to handle persistence:

- **`@UserDefault<T>`** – Stores non-optional values with a default fallback. If the key is absent in `UserDefaults`, it returns the specified `defaultValue`. Implementation is at lines 4-12 of [`AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/AppPreferences.swift).

- **`@OptionalUserDefault<T>`** – Stores optional values that can be `nil`. When the key is missing, it returns `nil` rather than a default. Implementation is at lines 14-22 of [`AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/AppPreferences.swift).

These wrappers automatically call `UserDefaults.standard.object(forKey:)` on read and `set(_:forKey:)` on write, ensuring immediate persistence without manual synchronization.

## Accessing and Modifying Preferences

Any component in the app can read or modify settings by interacting with `AppPreferences.shared`.

### Reading Preferences Programmatically

To read a value, access the property directly on the shared instance:

```swift
let currentEngine = AppPreferences.shared.selectedEngine
let modelPath = AppPreferences.shared.selectedWhisperModelPath
let suppressBlank = AppPreferences.shared.suppressBlankAudio

```

The wrappers guarantee that a value is always available—either the stored value or the default defined in the wrapper declaration.

### Writing Preferences Programmatically

To update a setting, assign a new value to the property. The wrapper automatically persists the change to `UserDefaults`:

```swift
AppPreferences.shared.selectedEngine = "fluidaudio"
AppPreferences.shared.fluidAudioModelVersion = "v2"
AppPreferences.shared.modifierOnlyHotkey = "command"

```

These calls are thread-safe and can be made from anywhere in the application, including background tasks or engine callbacks.

## UI Configuration via SettingsViewModel

The SwiftUI interface does not interact with `AppPreferences` directly. Instead, it uses `SettingsViewModel` (defined in [`OpenSuperWhisper/Settings.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Settings.swift)) as a bridge between the UI and persistent storage.

### How SettingsViewModel Bridges UI and Storage

The view model mirrors the singleton values using `@Published` properties. During initialization (lines 64-86), it copies current values from `AppPreferences.shared` into its own properties:

```swift
class SettingsViewModel: ObservableObject {
    @Published var selectedEngine: String {
        didSet {
            AppPreferences.shared.selectedEngine = selectedEngine
        }
    }
    
    @Published var selectedLanguage: String
    @Published var translateToEnglish: Bool
    
    init() {
        self.selectedEngine = AppPreferences.shared.selectedEngine
        self.selectedLanguage = AppPreferences.shared.whisperLanguage
        self.translateToEnglish = AppPreferences.shared.translateToEnglish
    }
}

```

When a user changes a control in the Settings view, the `didSet` observer immediately writes the value back to `AppPreferences.shared`, ensuring the singleton and `UserDefaults` stay synchronized.

### Handling Side Effects

Some preference changes require immediate system updates. For example, when the transcription engine changes, the view model triggers a reload:

```swift
@Published var selectedEngine: String {
    didSet {
        AppPreferences.shared.selectedEngine = selectedEngine
        Task { @MainActor in
            TranscriptionService.shared.reloadEngine()
        }
    }
}

```

This pattern ensures that configuration changes in the UI propagate instantly to the active transcription backend.

## Preference Migration

OpenSuperWhisper includes a migration strategy for legacy keys. The `migrateOldPreferences()` method (lines 30-35 in [`AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/AppPreferences.swift)) copies values from old keys to new ones when the app launches.

For example, older versions stored the model path under `selectedModelPath`. The migration checks for this key and copies the value to `selectedWhisperModelPath` if the new key is absent:

```swift
private func migrateOldPreferences() {
    if let oldPath = UserDefaults.standard.string(forKey: "selectedModelPath"),
       AppPreferences.shared.selectedWhisperModelPath == nil {
        AppPreferences.shared.selectedWhisperModelPath = oldPath
    }
}

```

This ensures seamless upgrades for users updating from earlier versions without losing their configuration.

## Adding New Preferences to OpenSuperWhisper

To extend the application with a new setting, follow this three-step pattern:

1. **Define the wrapper in [`AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/AppPreferences.swift)**:

```swift
@UserDefault(key: "enableNoiseReduction", defaultValue: false)
var enableNoiseReduction: Bool

```

2. **Expose in `SettingsViewModel`**:

```swift
@Published var enableNoiseReduction: Bool {
    didSet { 
        AppPreferences.shared.enableNoiseReduction = enableNoiseReduction 
    }
}

// In init():
self.enableNoiseReduction = AppPreferences.shared.enableNoiseReduction

```

3. **Bind in [`Settings.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/Settings.swift)**:

```swift
Toggle("Noise Reduction", isOn: $viewModel.enableNoiseReduction)

```

If the new setting replaces a legacy key, add a migration check in `migrateOldPreferences()` to preserve user data across updates.

## Summary

- **Centralized Storage**: All OpenSuperWhisper preferences live in `AppPreferences.shared`, a singleton wrapping `UserDefaults` located in [`OpenSuperWhisper/Utils/AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/OpenSuperWhisper/Utils/AppPreferences.swift).
- **Type Safety**: Custom `@UserDefault` and `@OptionalUserDefault` property wrappers provide type-safe access with automatic persistence.
- **UI Synchronization**: `SettingsViewModel` bridges SwiftUI controls with the singleton, using `@Published` properties that write back to `AppPreferences` on change.
- **Legacy Support**: The `migrateOldPreferences()` method handles key renaming and data migration between app versions.
- **Extensibility**: New settings require only a wrapper property, a view-model publisher, and a UI binding to integrate fully with the existing architecture.

## Frequently Asked Questions

### Where are OpenSuperWhisper preferences stored on macOS?

OpenSuperWhisper persists all settings via the standard macOS `UserDefaults` system, accessed through the `AppPreferences` singleton. The values are stored in the app's sandboxed preferences domain and automatically synchronized by the system. You can access them programmatically via `AppPreferences.shared` from any file that imports the Utils module.

### How do I add a custom setting to OpenSuperWhisper?

To add a custom setting, declare a new property in [`AppPreferences.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/AppPreferences.swift) using either `@UserDefault` (for non-optional values with defaults) or `@OptionalUserDefault` (for optional values). Then expose the value as a `@Published` property in `SettingsViewModel`, initializing it from the singleton and writing back in `didSet`. Finally, add a control in [`Settings.swift`](https://github.com/Starmel/OpenSuperWhisper/blob/main/Settings.swift) bound to the view-model property.

### What happens when I change the transcription engine in OpenSuperWhisper settings?

When the user selects a different engine (Whisper or FluidAudio/Parakeet), the `SettingsViewModel.selectedEngine` setter updates `AppPreferences.shared.selectedEngine` and immediately calls `TranscriptionService.shared.reloadEngine()` on the MainActor. This triggers the app to unload the current transcription backend and initialize the newly selected engine with current preference values.

### How does OpenSuperWhisper handle legacy preference keys?

The `AppPreferences` class includes a `migrateOldPreferences()` method that runs at initialization. It checks for deprecated keys—such as `selectedModelPath`—and copies their values to the current keys (like `selectedWhisperModelPath`) if the new key has no value. This ensures users upgrading from older versions retain their settings without manual intervention.