# FluidVoice App Architecture: A Deep Dive into the macOS SwiftUI Structure

> Explore the FluidVoice app architecture a three layer SwiftUI design. Learn how heavy audio and AI services are separated from the UI to ensure stability and responsiveness.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: architecture
- Published: 2026-06-30

---

**FluidVoice uses a three-layer SwiftUI architecture built around a lazy-initialized service container that separates heavy audio and AI services from the UI layer to prevent crashes and maintain responsiveness.**

FluidVoice is a macOS voice transcription application developed by altic-dev. Its architecture follows a concentric layer design that isolates resource-intensive services—such as automatic speech recognition (ASR) and model loading—from the SwiftUI view hierarchy. Understanding the FluidVoice app architecture reveals how modern macOS apps can handle real-time audio processing while maintaining a fluid user interface.

## Three-Layer Architecture Overview

The codebase is organized into three distinct layers, each with specific responsibilities:

| Layer | Responsibility | Primary Types | Key Files |
|------|----------------|---------------|-----------|
| **Entry & UI** | Process initialization, window hierarchy, main UI rendering, and menu-bar hosting | `FluidApp`, `ContentView`, `WelcomeView` | [`Sources/Fluid/fluidApp.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/fluidApp.swift), [`Sources/Fluid/ContentView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/ContentView.swift) |
| **Service Container** | Long-lived service management, lazy initialization, and UI-ready gating | `AppServices`, `ASRService`, `MenuBarManager` | [`Sources/Fluid/Services/AppServices.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/AppServices.swift) |
| **Domain & Infrastructure** | Transcription engines, AI enhancement, model management, and persistence | `TranscriptionProvider`, `WhisperProvider`, `LocalAPIServer` | [`Sources/Fluid/Services/TranscriptionProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/TranscriptionProvider.swift), [`Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift) |

This separation ensures that heavyweight components like neural network models and audio capture engines do not interfere with SwiftUI's render cycle.

## Entry Point and Application Lifecycle

### The @main Entry in fluidApp.swift

The application launches through the `FluidApp` struct in [`Sources/Fluid/fluidApp.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/fluidApp.swift). This entry point creates two critical `@StateObject` instances: `MenuBarManager` for the status-bar icon and `AppServices` for the shared service container.

```swift
@main
struct FluidApp: App {
    @StateObject private var menuBarManager = MenuBarManager()
    @StateObject private var appServices: AppServices
    @ObservedObject private var settings = SettingsStore.shared
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    init() {
        _appServices = StateObject(wrappedValue: AppServices.shared)
    }

    var body: some Scene {
        WindowGroup(id: "main") {
            AdaptiveAppTheme(accent: self.settings.accentColor) {
                ContentView()
                    .environmentObject(self.menuBarManager)
                    .environmentObject(self.appServices)
            }
        }
        .defaultSize(width: 1000, height: 700)
    }
}

```

The `ContentView` receives both objects via `environmentObject`, making them accessible throughout the view hierarchy.

### Early Boot Logic in AppDelegate.swift

`AppDelegate` handles initialization that must occur before any SwiftUI view appears. Located in [`Sources/Fluid/AppDelegate.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/AppDelegate.swift), it initializes the `FileLogger`, detects login-item launches, boots the `SettingsStore`, and starts the `LocalAPIServer`. It also manages window presentation through staggered retries to guarantee UI readiness.

## The Service Container Pattern

### AppServices Singleton Implementation

`AppServices` is a `@MainActor` singleton defined in [`Sources/Fluid/Services/AppServices.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/AppServices.swift). It acts as a dependency injection container for heavy-weight components.

The class uses **lazy initialization** to defer expensive object creation until explicitly needed:

```swift
@MainActor
final class AppServices: ObservableObject {
    static let shared = AppServices()
    @Published private(set) var isUIReady = false

    private var _audioObserver: AudioHardwareObserver?
    var audioObserver: AudioHardwareObserver {
        if _audioObserver == nil {
            _audioObserver = AudioHardwareObserver()
        }
        return _audioObserver!
    }

    private var _asr: ASRService?
    var asr: ASRService {
        if _asr == nil {
            _asr = ASRService()
        }
        return _asr!
    }

    func signalUIReady() { isUIReady = true }
}

```

### UI-Ready Gating Mechanism

The container implements a **UI-ready flag** to prevent service initialization during SwiftUI's initial render. The `isUIReady` property is set by `ContentView.onAppear` after a short delay. Only then does `initializeServicesIfNeeded()` trigger the lazy getters, instantiating `AudioHardwareObserver`, `ASRService`, and `GlobalHotkeyManager`.

This pattern prevents `EXC_BAD_ACCESS` crashes that can occur when heavy Objective-C runtime metadata is loaded during SwiftUI's type resolution phase.

## UI Layer and View Hierarchy

### ContentView as the Root Controller

`ContentView` serves as the root SwiftUI view in [`Sources/Fluid/ContentView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/ContentView.swift). It accesses lazily-initialized services through the environment:

```swift
@EnvironmentObject private var appServices: AppServices
private var asr: ASRService { appServices.asr }
private var audioObserver: AudioHardwareObserver { appServices.audioObserver }

```

The view creates per-feature view models such as `CommandModeService` and `RewriteModeService`, managing recording modes (dictate, command, prompt) and driving overlay logic.

### MenuBarManager and NotchOverlayManager

`MenuBarManager` (in [`Sources/Fluid/Services/MenuBarManager.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/MenuBarManager.swift)) builds the status-item icon and tracks recording state. It publishes navigation requests through `requestedNavigationDestination` that `ContentView` observes.

The manager coordinates the **notch overlay**—a live transcription bubble positioned around the MacBook notch. It shows the overlay when `ASRService` starts recording and hides it when recording stops, unless AI post-processing remains active.

`NotchOverlayManager` (in [`Sources/Fluid/Services/NotchOverlayManager.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/NotchOverlayManager.swift)) renders this overlay, receiving text updates from `MenuBarManager` and audio level data from `ASRService` through Combine publishers.

## Transcription and AI Provider Stack

### TranscriptionProvider Protocol

At the domain layer, the `TranscriptionProvider` protocol (in [`Sources/Fluid/Services/TranscriptionProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/TranscriptionProvider.swift)) abstracts all speech-to-text backends. It defines uniform methods for model management and transcription:

- `prepare()` and `modelsExistOnDisk()` for cache management
- `transcribeStreaming()` for real-time results
- `transcribeFinal()` for high-quality offline processing

### Concrete Provider Implementations

The architecture supports multiple providers through protocol conformance:

- **WhisperProvider**: OpenAI Whisper model integration
- **NemotronProvider**: NVIDIA Nemotron AI enhancement
- **ParakeetRealtimeProvider**: Real-time streaming ASR

Each provider returns `ASRTranscriptionResult` containing text and confidence scores. The `ASRService` exposes Combine publishers like `partialTranscription` and `audioLevelPublisher` that feed UI elements without blocking the main thread.

## Supporting Infrastructure

### Local API Server

`LocalAPIServer` (in [`Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/LocalAPI/LocalAPIServer.swift)) runs an in-process HTTP server exposing internal endpoints. Controllers such as `InferenceAPIController`, `HistoryAPIController`, and `DictionaryAPIController` handle history export and dictionary management.

### Persistence and Settings

`SettingsStore` (in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift)) persists user preferences via `UserDefaults` and Keychain (for API keys). Related stores including `TranscriptionHistoryStore` and `ChatHistoryStore` provide on-disk caching for audio history and model metadata.

## Practical Code Examples

### Accessing the Service Container from Any View

```swift
struct ExampleView: View {
    @EnvironmentObject private var appServices: AppServices

    var body: some View {
        VStack {
            Text(appServices.asr.isRunning ? "Listening…" : "Idle")
            Button("Start") { appServices.asr.start() }
            Button("Stop")  { appServices.asr.stop() }
        }
    }
}

```

This pattern leverages the `AppServices` singleton provided through the SwiftUI environment.

### Subscribing to Live Transcription Streams

```swift
struct LivePreview: View {
    @EnvironmentObject private var appServices: AppServices
    @State private var transcript = ""

    var body: some View {
        Text(transcript)
            .onReceive(appServices.asr.$partialTranscription) { newText in
                transcript = newText
            }
    }
}

```

The `$partialTranscription` publisher emits real-time transcription updates from the ASR engine.

### Triggering Menu-Bar Navigation Programmatically

```swift
// From any view model or view:
MenuBarManager.shared.requestedNavigationDestination = .preferences

```

`ContentView` observes this property to present the appropriate destination.

## Summary

- **Three-layer architecture** separates UI, services, and domain logic to maintain clean dependencies and testability.
- **Lazy initialization** in `AppServices` prevents SwiftUI crashes by deferring heavy object creation until after the UI renders.
- **Protocol-driven design** via `TranscriptionProvider` enables swapping ASR engines without modifying view code.
- **Combine publishers** facilitate real-time updates between audio services and UI components like the notch overlay.
- **Local API server** provides extensibility for automation and third-party integrations.

## Frequently Asked Questions

### What design pattern does FluidVoice use for dependency injection?

FluidVoice uses a **singleton service container pattern** via the `AppServices` class. Rather than injecting dependencies through initializer parameters, the app exposes a `@MainActor` singleton that views access through SwiftUI's `environmentObject`. This approach avoids the type-metadata crashes common in early SwiftUI while maintaining global access to heavy services like `ASRService` and `AudioHardwareObserver`.

### How does FluidVoice prevent UI freezes during model loading?

The architecture implements **UI-ready gating** in `AppServices`. Heavy services are marked as lazy properties that only instantiate after `ContentView` sets `isUIReady = true` in its `onAppear` handler. This ensures that resource-intensive initialization—such as loading neural network models into memory—occurs after the window is fully rendered and responsive.

### Can FluidVoice support multiple transcription engines simultaneously?

Yes. The `TranscriptionProvider` protocol abstracts the transcription interface, allowing the app to host multiple providers such as `WhisperProvider`, `NemotronProvider`, and `ParakeetRealtimeProvider`. The `ASRService` can switch between providers based on user settings or network availability without requiring changes to the UI layer, thanks to the uniform `ASRTranscriptionResult` return type.

### Where does FluidVoice store user preferences and sensitive data?

User preferences reside in `SettingsStore`, which persists data to `UserDefaults` for general settings and the macOS Keychain for sensitive information like API keys. Additional stores such as `TranscriptionHistoryStore` handle on-disk caching of audio history and chat logs, ensuring data persists across app launches while remaining accessible to the local HTTP API server.