Skip to content

Demo: exercise ReliaBLE from a background-actor SwiftData stack #20

Description

@itsniper

Goal

Refactor the Demo app's SwiftData layer to run on a background actor rather than @MainActor, so that the Demo itself exercises ReliaBLE across actor boundaries. Catches @MainActor-creep regressions in the library at PR-time and doubles as a concrete pattern for library consumers to copy.

Context

See the investigation report at docs/investigations/swift6-concurrency-audit-2026-05-13.md, Preventive Measures section. Quoting the maintainer's note:

I was trying to keep the Demo app simple, but I think this is a case where it would make sense to implement background SwiftData in the Demo. Both to catch regressions and demonstrate the ability to library consumers.

Today CentralViewModel runs on @MainActor and performs SwiftData inserts/upserts directly in Combine .sink closures. After the library refactor (#10 and its sub-issues), the manager will be a nonisolated Sendable type with AsyncStream event surfaces. The Demo can exercise this by:

  • Holding the ReliaBLEManager reference on a @MainActor view-model (typical SwiftUI path).
  • Routing SwiftData writes through a background actor (@ModelActor or a custom isolated ModelContext wrapper).
  • Driving the BLE events from .task { for await … in manager.peripheralDiscoveries } and hopping into the SwiftData actor for persistence.

This is also a useful demonstration for library consumers building real-world apps where BLE event volume warrants off-main persistence.

Deliverable

  1. Introduce a @ModelActor (or equivalent) wrapper around the SwiftData ModelContainer in the Demo. Suggested name: DeviceStoreActor.
  2. Route all writes (peripheral upserts, discovery-event inserts) through this actor.
  3. Keep reads on @MainActor for SwiftUI binding (or move to @Observable actor-snapshot pattern if desired).
  4. The Demo's discovery pipeline becomes:
    .task {
        for await event in manager.peripheralDiscoveries {
            await store.upsert(event)   // background actor
        }
    }
  5. Validate that the existing UX (CentralView, PeripheralView) keeps working — no main-thread checker complaints, no UI stutters.

Acceptance criteria

  • Demo builds clean under Swift 6 strict concurrency.
  • All SwiftData writes are observable on a non-main thread (verify via Xcode's Main Thread Checker or Thread.isMainThread asserts).
  • UX is unchanged for the user.
  • No @MainActor annotation creep into ReliaBLEManager or its public types is needed to make the Demo compile (this is the regression guard).
  • A small README / inline comment in the Demo points future consumers at this as the canonical "off-main persistence" pattern.

Not in scope

  • Adding a background-actor test to the library's own test target — that's covered by the smoke test in Step 5.

References

  • Investigation report: docs/investigations/swift6-concurrency-audit-2026-05-13.md
  • Parent: Update for Swift Concurrency in Swift 6 #10
  • Depends on: Step 4 issue (façade Sendable-ization) — but the SwiftData refactor itself can land earlier if it's structured to use .receive(on: DispatchQueue.global()) bridging on the Combine surface for now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions