# How to Handle Configuration Changes in FluidVoice: A Complete Guide to SettingsStore

> Learn to manage configuration changes in FluidVoice using SettingsStore. This guide explains type-safe access, ObservableObject publishing, and NotificationCenter broadcasting for seamless UI updates.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: how-to-guide
- Published: 2026-07-08

---

**FluidVoice handles configuration changes through a centralized `SettingsStore` singleton that wraps `UserDefaults` with type-safe accessors, publishes changes via `ObservableObject`, and broadcasts updates through `NotificationCenter` to keep the UI synchronized.**

All user-tunable preferences in the [altic-dev/FluidVoice](https://github.com/altic-dev/FluidVoice) repository flow through a single architectural entry point. This design ensures that whether you are migrating legacy settings, updating prompt profiles, or reacting to system-wide changes, the configuration layer remains consistent and reactive across the entire application.

## The SettingsStore Architecture

The configuration system is built around **`SettingsStore`**, a singleton defined in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift). This class encapsulates `UserDefaults.standard` (initialized at line 25) and exposes a strongly-typed API that prevents direct dictionary access scattered throughout the codebase.

The architecture separates concerns into four distinct layers:

- **Persistence Layer**: Handles raw `UserDefaults` reads and writes. Complex objects like `dictationPromptProfiles` and `appPromptBindings` are stored as JSON-encoded blobs using `JSONEncoder` and `JSONDecoder`.
- **Domain Model Layer**: Types such as **`PromptMode`**, **`DictationPromptProfile`**, **`AppPromptBinding`**, and **`DictationPromptSelection`** enforce correct data shapes. For example, `PromptMode` normalizes legacy string values (`"write"`, `"rewrite"`) to the canonical `.edit` case (lines 75-99).
- **Business Logic Layer**: Methods like **`effectivePromptBody(for:)`**, **`promptResolution(for:)`**, and **`renderDictationUserMessage`** interpret raw settings into runtime-ready values. The `effectivePromptBody` method uses `promptResolution` to combine hidden base prompts with user overrides (lines 1154-1159).
- **UI Binding Layer**: SwiftUI views observe `SettingsStore.shared` and react to `objectWillChange` emissions. When users interact with controls, the view calls typed setters such as `setDictationPromptSelection(_:for:)`.

## Reading and Writing Basic Configuration

For primitive values, `SettingsStore` provides direct access to the underlying `UserDefaults` while maintaining type safety. You should never access `UserDefaults` directly outside of this class.

**Reading a Boolean flag:**

```swift
let isDebugEnabled = SettingsStore.shared.defaults.bool(forKey: "EnableDebugLogs")

```

**Creating a SwiftUI binding:**

```swift
Toggle("Enable Debug Logs", isOn: Binding(
    get: { SettingsStore.shared.defaults.bool(forKey: "EnableDebugLogs") },
    set: { SettingsStore.shared.defaults.set($0, forKey: "EnableDebugLogs") }
))

```

When setting values, the store handles validation automatically. All string setters trim whitespace and convert empty strings to `nil`, preventing configuration pollution from accidental spaces.

## Managing Complex Configuration Objects

FluidVoice stores sophisticated domain objects—such as custom dictation prompts and per-app bindings—as JSON blobs. The `SettingsStore` handles serialization transparently.

**Creating a new prompt profile:**

```swift
let newProfile = SettingsStore.DictationPromptProfile(
    name: "Creative Writing",
    prompt: SettingsStore.defaultPromptBodyText(for: .dictate) + "\n\nBe more expressive.",
    mode: .dictate,
    includeContext: true
)

var profiles = SettingsStore.shared.dictationPromptProfiles
profiles.append(newProfile)
SettingsStore.shared.dictationPromptProfiles = profiles

```

**Updating per-app bindings:**

To associate a specific prompt with a particular application, use the **`upsertAppPromptBinding(for:appBundleID:appName:promptID:)`** method (lines 704-740). This normalizes the bundle ID, updates the in-memory array, and persists the entire collection back to `UserDefaults`:

```swift
SettingsStore.shared.upsertAppPromptBinding(
    for: .dictate,
    appBundleID: "com.example.myapp",
    appName: "My App",
    promptID: "custom-prompt-123"
)

```

## Migrating Legacy Configuration Data

The `SettingsStore` initializer runs several migration helpers to ensure backwards compatibility. When handling configuration changes in older versions of the app, the system automatically converts deprecated keys to their modern equivalents.

Key migration methods include:
- **`migrateLegacyDictationAIPreferenceIfNeeded`**: Converts old boolean flags to new selection enums.
- **`normalizePromptSelectionsIfNeeded`**: Ensures all persisted prompt selections match current valid states.

The store also maintains compatibility aliases for renamed keys. For instance, `selectedWritePromptID` and `defaultWritePromptOverride` forward to their canonical counterparts, allowing external code to reference either key without data loss.

## Subscribing to Configuration Changes

FluidVoice provides two mechanisms for reacting to configuration changes: SwiftUI’s `ObservableObject` protocol for view-level updates, and `NotificationCenter` for broader system reactions.

**SwiftUI Observation:**

Views automatically refresh when `SettingsStore` emits `objectWillChange`. The `AISettingsView+AdvancedSettings.swift` file demonstrates this pattern—toggling “Rewrite Mode linked to global” writes directly to `UserDefaults` and triggers immediate UI updates (line 1561).

**Global Notification Subscription:**

For components that need to respond to changes originating outside the `SettingsStore` (such as system `UserDefaults` modifications), subscribe to `UserDefaults.didChangeNotification`:

```swift
// In NotchContentViews.swift (line 81) and BottomOverlayView.swift
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
    viewModel.refreshFromSettings()
}

```

This dual-channel approach ensures that **any** change—whether programmatic or user-initiated—propagates instantly to every subscriber.

## Handling Edge Cases and Validation

The `SettingsStore` implements several safeguards to maintain configuration integrity:

- **Whitespace trimming**: All text-based setters convert empty or whitespace-only strings to `nil`.
- **Nil vs. empty distinction**: The `defaultPromptOverride(for:)` getter distinguishes between “not set” and “explicitly cleared,” allowing the UI to display built-in defaults while respecting user intent.
- **Legacy value normalization**: The `PromptMode` enum initializer handles legacy string values, ensuring that older configurations map correctly to current behavior.

## Summary

- **`SettingsStore`** in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) serves as the single source of truth for all configuration, wrapping `UserDefaults.standard` with type-safe accessors.
- Complex objects are JSON-encoded automatically, while primitives map directly to `UserDefaults` keys.
- **Migration methods** like `migrateLegacyDictationAIPreferenceIfNeeded` handle version upgrades without data loss.
- **SwiftUI views** observe `SettingsStore.shared` via `ObservableObject`, while background components listen to `UserDefaults.didChangeNotification` for changes.
- **Validation** occurs at the persistence layer, with automatic string trimming and legacy key aliasing.

## Frequently Asked Questions

### How does FluidVoice persist complex configuration objects?

FluidVoice stores complex objects like `DictationPromptProfile` and `AppPromptBinding` as JSON-encoded strings in `UserDefaults`. The `SettingsStore` uses `JSONEncoder` to serialize these structs before writing them, and `JSONDecoder` to reconstruct them on access. This approach maintains type safety while leveraging the simple key-value storage of `UserDefaults`.

### What triggers UI updates when settings change in FluidVoice?

UI updates trigger through two mechanisms. First, `SettingsStore` conforms to `ObservableObject` and calls `objectWillChange.send()` after modifying values, causing SwiftUI views to recompute. Second, the system broadcasts `UserDefaults.didChangeNotification` via `NotificationCenter`, which views like [`NotchContentViews.swift`](https://github.com/altic-dev/FluidVoice/blob/main/NotchContentViews.swift) and [`BottomOverlayView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/BottomOverlayView.swift) subscribe to for instantaneous refresh.

### How does FluidVoice handle legacy configuration keys?

The `SettingsStore` initializer runs migration helpers that detect and convert old keys. For example, the `normalizePromptSelectionsIfNeeded` method ensures legacy prompt selections align with current enums, while properties like `selectedWritePromptID` maintain aliases that forward to new canonical keys. This ensures backwards compatibility without requiring manual user intervention.

### Can I use SettingsStore outside of SwiftUI views?

Yes. While SwiftUI views commonly reference `SettingsStore.shared`, any class or function can import the store and call its methods. For non-SwiftUI components, subscribe to `NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)` to observe changes, or poll the typed getters directly when the app becomes active.