How Vorssaint-Utils Separates SwiftUI UI from Business Logic: A Clean Architecture Deep Dive

Vorssaint-Utils implements a clean-architecture approach where SwiftUI views bind to observable runtime objects that delegate all business operations to service-oriented singletons, ensuring UI components remain pure declarative compositions without direct system API calls.

Vorssaint-Utils demonstrates how to keep SwiftUI UI separated from business logic by using a runtime coordinator pattern that isolates platform-specific code in service layers. The repository structures its codebase so that view files handle only presentation and user interaction while functional behavior lives in dedicated service classes. This separation makes the UI layer testable, maintainable, and free from complex platform API dependencies.

The Architectural Pattern: Observable Objects as Bridges

The separation strategy centers on intermediary observable objects that act as the single source of truth between the user interface and business operations. Rather than embedding logic directly in views, Vorssaint-Utils uses lightweight model objects that publish state changes SwiftUI can observe.

Centralized State Management with FeatureRuntime

In Sources/Vorssaint/App/FeatureRuntime.swift, the FeatureRuntime class serves as the primary bridge between UI and logic. This singleton exposes a @Published property that notifies views of state changes while encapsulating all mutation logic:

final class FeatureRuntime: ObservableObject {
    static let shared = FeatureRuntime()
    @Published private(set) var revision = 0
    
    // Called from UI
    func replaceAvailable(with selected: Set<AppFeature>, enabling keys: [String] = []) {
        // Persist selection
        keys.forEach { UserDefaults.standard.set(true, forKey: $0) }
        // Apply bindings (services) without UI references
        selected.forEach { Self.bindings[$0]?() }
        finishAvailabilityChange()
    }
}

When a view modifies a setting, it calls methods on FeatureRuntime.shared, which updates UserDefaults and executes the appropriate service bindings. The UI never instantiates services directly or interacts with system APIs.

UI Bindings Through Property Wrappers

Views subscribe to the runtime using standard SwiftUI property wrappers. Components declare dependencies through @ObservedObject, @AppStorage, and @State, binding to objects like FeatureRuntime and L10n (localization). This ensures that when the runtime updates its revision property, the UI automatically re-renders without the view containing any business logic.

Decoupling Through Feature Bindings

Vorssaint-Utils achieves loose coupling through a static dictionary of bindings that maps features to their corresponding service actions. This mechanism prevents UI components from importing or referencing concrete service implementations.

The Bindings Map Architecture

At the end of FeatureRuntime.swift, a private static dictionary links each AppFeature enum case to a closure that invokes the appropriate service:

private static let bindings: [AppFeature: () -> Void] = [
    .windowMaximizer: { WindowMaximizer.shared.syncWithPreferences() },
    .scrollInverter:   { ScrollInverter.shared.syncWithPreferences() },
    // …
]

When replaceAvailable(...) or setAvailable(...) is called from the UI, the runtime iterates through the selected features and executes their corresponding bindings. The UI remains agnostic about which services exist or how they function—it simply triggers high-level runtime methods.

Service-Oriented Business Logic

All functional behavior resides in Sources/Vorssaint/Services/, where each service operates as a singleton encapsulating specific platform capabilities. This directory contains concrete implementations for window management, scroll inversion, auto-quit functionality, and other system-level features.

Service Implementation Pattern

Every service implements a consistent interface centered on syncWithPreferences(). This method reads the current model state from UserDefaults and performs platform-specific work, such as interacting with WindowServer, AX (Accessibility APIs), or other system frameworks. The UI layer never touches these APIs directly.

For example, WindowMaximizer.swift, ScrollInverter.swift, and AutoQuitService.swift each follow this pattern:

    1. Expose a shared singleton instance
    1. Implement syncWithPreferences() to read current settings
    1. Execute platform-specific operations based on the persisted state

Concrete Service Examples

  • WindowMaximizer: Handles window management and sizing operations through private system APIs
  • ScrollInverter: Manages scroll direction preferences and hardware interaction
  • AutoQuitService: Implements automatic application termination logic

Services read their initial configuration during FeatureRuntime.syncAtLaunch(), ensuring the UI and logic stay synchronized without tight coupling.

Pure Declarative SwiftUI Views

The UI layer in Sources/Vorssaint/UI/ contains only layout code, local state management, and calls to the runtime. Views are pure compositions that delegate all action handling to FeatureRuntime.

Onboarding Flow Implementation

In OnboardingView.swift, the "Continue" button demonstrates the separation principle. When clicked, it delegates the business logic to FeatureRuntime and manages only the local UI state:

// OnboardingView – UI only
Button(primaryButtonTitle) {
    // Business logic delegated to FeatureRuntime
    FeatureRuntime.shared.replaceAvailable(
        with: selectedFeatures,
        enabling: selectedPreset?.enableKeys ?? [])
    // UI state update
    withAnimation(.easeInOut(duration: 0.2)) { index += 1 }
}

The view does not instantiate WindowMaximizer or modify UserDefaults directly. It simply informs the runtime of the user's selection and animates the transition to the next step.

Files like MenuPanel/QuickTogglesSection.swift maintain this pattern, containing only declarative layout structures and binding to published properties from the runtime. Complex gesture controls in WindowGestureControls.swift similarly delegate their actions through the runtime coordinator rather than implementing logic internally.

Handling Persistence and Permissions

The separation extends to data persistence and system permission management, ensuring these concerns remain outside the view layer.

UserDefaults Abstraction

UI-initiated changes are persisted through FeatureRuntime, which writes the appropriate keys to UserDefaults. Services read these keys on launch via FeatureRuntime.syncAtLaunch(). This creates a unidirectional flow: UI → Runtime → Persistence → Services, preventing views from directly accessing the standard user defaults interface.

Permission UI Decoupling

The onboarding permission screen in SelectedPermissionsStep.swift builds its list from features' onboardingPermissions property but delegates permission changes to the runtime. The permission UI is purely presentational; the logic that reacts to permission changes lives in the services that depend on them. When a user toggles a permission, FeatureRuntime syncs the state, triggering the appropriate service binding without the view knowing which service requires that permission.

Summary

Vorssaint-Utils demonstrates a robust pattern for separating SwiftUI UI from business logic through these key architectural decisions:

  • Observable singletons (FeatureRuntime) act as the sole conduit between views and services, publishing state changes via @Published properties
  • Static binding dictionaries map feature flags to service methods without exposing service details to the UI layer
  • Service-oriented architecture isolates platform-specific code (window management, scrolling, auto-quit) in testable singletons with consistent syncWithPreferences() interfaces
  • Pure declarative views contain only layout and animation code, delegating all business operations to the runtime coordinator
  • Decoupled persistence ensures UserDefaults access happens exclusively in the runtime layer, not in views or services directly

Frequently Asked Questions

How does FeatureRuntime communicate state changes to SwiftUI views?

FeatureRuntime inherits from ObservableObject and exposes a @Published private(set) var revision property. When business logic updates occur, the runtime increments this revision, automatically triggering view re-renders for any SwiftUI components observing the singleton via @ObservedObject. This mechanism allows the UI to reflect state changes without containing logic about what caused those changes.

Why doesn't Vorssaint-Utils use SwiftUI's @Environment for dependency injection?

The project opts for a singleton runtime coordinator pattern rather than environment injection to maintain a clear, explicit dependency graph. By using FeatureRuntime.shared as a centralized entry point, the codebase ensures that all UI components reference the same state authority and that service lifecycle management happens in one location. This approach simplifies testing by allowing developers to mock the runtime or individual services without configuring the SwiftUI environment hierarchy.

How does the service layer handle platform-specific APIs without exposing them to the UI?

Services like WindowMaximizer and ScrollInverter encapsulate all interactions with system frameworks (e.g., WindowServer, AX Accessibility APIs) within their syncWithPreferences() methods. These singletons are marked as internal or private to the service layer, and views never import service modules directly. The UI communicates only through the FeatureRuntime API, which forwards calls to the appropriate service via the static bindings dictionary, maintaining strict boundaries between presentation and platform code.

What pattern does Vorssaint-Utils use to test business logic independently of SwiftUI views?

The architecture enables unit testing of services in isolation since they are pure Swift classes without SwiftUI dependencies. Developers can instantiate service singletons directly, manipulate UserDefaults state, and verify that syncWithPreferences() produces the correct side effects. The UI layer can be tested using preview providers and mock runtime objects, ensuring that view logic (layout, animations, navigation) remains separate from business rule validation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →