MRPF Universal Application Architecture
The app’s architecture is a modular, concurrency-safe design for a universal SwiftUI app using Swift 6 features (async/await, actors for isolation). It decouples UI from data/network/navigation/error logic, emphasizes non-blocking operations, and supports extensibility for new models/endpoints. Key updates incorporate reusable ErrorPublisher instances for both global and scoped error handling, addressing multi-window requirements on macOS (e.g., independent scanner errors per window, shared REST errors across all). AppContext centralizes initialization and propagation, simplifying complex setups across platforms, windows, previews, and backgrounds.
- Decoupling and Data Flow: SwiftUI views (UX) interact only with high-level services/repositories/managers/publishers via @Environment and async calls (e.g., fetch(), startScan(), navigation triggers). Views use @Query for reactive local data from SwiftData, keeping them ignorant of network/caching/navigation/error details. Business logic is offloaded to actors/services/managers.
- Concurrency and Performance: All network/storage ops are async and background-threaded. Actors (e.g., DataRepository, ScannerService) isolate mutable state to prevent data races. URLSession and WebSocket tasks run non-blockingly; SwiftData uses background contexts for writes to avoid UI stalls.
- Navigation Management: NavigationManager (@Observable class or actor) is scene/window-scoped, handling navigation state (e.g., paths/stacks) independently per window on macOS. Created and injected per WindowGroup instance for multi-window isolation.
- Network Clients:
- APIClient (actor): Global, handles REST requests with error throwing.
- WebSocketClient (actor): Global, manages WS connections, sending, and receiving as an AsyncStream. Shared across services.
- Data Management:
- DataRepository (actor): Global facade for REST/WS on models. Handles caching, fetches, subscriptions, and storage. Publishes errors to global ErrorPublisher.
- ScannerService (@Observable actor): Scoped per window/instance (e.g., one per macOS window). Handles scan workflows, streams results, optional storage. Publishes errors to its own scoped ErrorPublisher.
- Storage: SwiftData (ModelContainer) is app-global, initialized in AppContext. Models are @Model-conformed; services use background contexts.
- Error Handling (Reusable/Decoupled): ErrorPublisher is a reusable @MainActor @Observable class managing a list of errors for multi-error support and selective dismissal:
- var errors: [ErrorItem] = [] where ErrorItem = (id: UUID, error: Error, title: String).
- Methods: publish(_ error: Error, title: String), dismiss(id: UUID), dismissAll().
- Global Instance: For app-wide errors (e.g., REST API). Shared across all windows; injected app-wide.
- Scoped Instances: For per-instance errors (e.g., scanner-specific). Each ScannerService has its own ErrorPublisher; injected to the relevant window/view subtree.
- UX Display: Each window’s root view uses a custom ErrorOverlay or .alert that observes both global and local (scanner) publishers, combines their errors into a single list, and provides dismiss buttons (per error or all). This enables:
- Global errors to appear in all windows.
- Scoped errors to appear only in the affected window.
- Concurrent errors shown together; dismiss one without affecting others.
- Central Coordination with AppContext: AppContext (shared instance or singleton) initializes globals (e.g., ModelContainer, APIClient, DataRepository, WebSocketClient, global ErrorPublisher). It provides factories for scoped objects (e.g., createScannerService() -> ScannerService with new ErrorPublisher). Propagation:
- App-wide globals via .environment in App body.
- Scene/window-scoped (e.g., NavigationManager, ScannerService with scoped ErrorPublisher) created/injected in WindowGroup builder for per-window isolation on macOS.
- Handles platform-specifics: Background wakeups for notifications, mock data for Xcode previews, window scene setup on macOS.
- Extensibility: Add models via protocols. New services reuse clients/publishers. AppContext centralizes additions.
- UX Bridge: Views trigger in Task {}, observe streams/status/@Query. No ViewModels. Navigation via scoped manager. Errors via combined overlay for global+scoped. This ensures scalability, multi-window correctness (independent scanner states/errors on macOS), and reusable error handling per 2026 SwiftUI practices.
graph TD
subgraph "App Setup & Coordination"
AppContext[AppContext Class
(init globals: ModelContainer, APIClient, Repository, WebSocketClient, global ErrorPublisher; factories for scoped: ScannerService w/ scoped EP, NavigationManager; handles previews/background)]
AppContext -->|app-wide injection
(e.g., repository, global EP)| App[App Struct]
App -->|scene-specific creation/injection
(e.g., new NavManager, new ScannerService w/ scoped EP per window)| WindowScene[WindowGroup/Scene
(multi-window on macOS)]
AppContext -.->|handles previews/background/windowscene| PreviewsBackground[Previews, Background Wakeups, Window Scenes]
end
subgraph "UX Layer"
WindowScene -->|binds scoped: NavManager, ScannerService, scoped EP| Views[SwiftUI Views (UX)]
Views -->|async calls (e.g., fetch())| DataRepository
Views -->|async calls (e.g., startScan())| ScannerService
Views -->|observes @Query| SwiftData[SwiftData (ModelContainer)]
Views -->|observes status/streams| ScannerService
Views -->|triggers navigation| NavigationManager[NavigationManager
(scoped per-window)]
RootViewPerWindow[Root View per Window] -->|custom ErrorOverlay: combines & shows lists from global + scoped EPs
(dismiss one/all)| ErrorPublisherGlobal[Global ErrorPublisher
(app-wide errors, e.g., REST)]
RootViewPerWindow -->|custom ErrorOverlay| ErrorPublisherScoped[Scoped ErrorPublisher
(per-scanner/window errors)]
end
subgraph "Service Layer (Actors/Managers)"
DataRepository[DataRepository Actor
(global; caching, storage)] -->|uses for REST| APIClient[APIClient Actor
(global)]
DataRepository -->|uses for subscriptions| WebSocketClient[WebSocketClient Actor
(global)]
ScannerService[ScannerService @Observable Actor
(scoped per-window; scan logic)] -->|uses for WS| WebSocketClient
end
subgraph "Network/Storage"
APIClient -.->|network I/O| RemoteREST[Remote REST API]
WebSocketClient -.->|network I/O| RemoteWS[Remote WS Service]
DataRepository -->|background ops| SwiftData
ScannerService -->|optional background store| SwiftData
end
subgraph "Error Handling (Reusable)"
DataRepository -->|publishes (MainActor.run)| ErrorPublisherGlobal
ScannerService -->|publishes| ErrorPublisherScoped
APIClient -->|throws| DataRepository
WebSocketClient -->|throws| ScannerService
end
style AppContext fill:#f9f,stroke:#333
style App fill:#f9f,stroke:#333
style WindowScene fill:#f9f,stroke:#333
style Views fill:#bbf,stroke:#333
style NavigationManager fill:#fd9,stroke:#333
style DataRepository fill:#fd9,stroke:#333
style ScannerService fill:#fd9,stroke:#333
style APIClient fill:#dfd,stroke:#333
style WebSocketClient fill:#dfd,stroke:#333
style SwiftData fill:#9f9,stroke:#333
style ErrorPublisherGlobal fill:#f99,stroke:#333
style ErrorPublisherScoped fill:#f99,stroke:#333
style RemoteREST fill:#ccc,stroke:#333
style RemoteWS fill:#ccc,stroke:#333
style PreviewsBackground fill:#ccc,stroke:#333