Skip to content

7.5. Swift

7.5.1. Snapshot

  • Install: .package(url: "https://github.com/thekaveh/VMx.git", from: "3.24.0")
  • Current source: Swift 3.26.0 implementing spec 3.25.0
  • Publication status: 3.24.0 is public through the immutable v3.24.0 SwiftPM tag and matching swift-v3.24.0 GitHub Release.
  • Reactive primitive: Combine
  • Naming idiom: camelCase
  • Hub delivery: callbacks run outside the state condition; ordinary concurrent producers remain synchronous and nested cross-hub sends cannot deadlock
  • Notification replay: pending subscribers are registered atomically, then attached outside the hub lock before CurrentValueSubject replays state
  • Modal completion: BasicModalVM atomically registers waiters and accepts only the first concurrent dismissal or disposal result

7.5.2. What To Reach For

Swift is the right fit when you want the same VMx lifecycle and conformance surface in a SwiftPM package, with SwiftUI adapters kept outside the core library boundary.

Use .product(name: "VMx", package: "vmx") in the consuming target. The lowercase package value is SwiftPM's canonical identity for the repository; the imported module and product remain VMx.

7.5.3. Serviced Collections

ServicedObservableCollection<T> publishes locally through Combine, then to an optional external hub:

let notes = ServicedObservableCollection<Note>(hub: hub)
let changes = notes.collectionChanged.sink { message in render(message) }

notes.append(first)
notes.append(second)
notes.replace(at: 0, with: revised)  // setAt remains available
try notes.move(from: 0, to: notes.count - 1)
notes.replaceAll(serverSnapshot)     // one Reset

Value removal is available when T: Equatable, removes the first match, and returns false when absent. removeAt and replace retain Swift's established array-precondition bounds behavior; move instead throws the catchable VMCollectionIndexError. Equal-index move and empty clear are no-ops. The caller owns both the Combine cancellable and every stored item.

Use KeyedServicedObservableCollection<Key,T> for captured-key access while retaining the same ordered message contract:

let notesByID = KeyedServicedObservableCollection<String, Note>(
    keyOf: { $0.id },
    hub: hub
)
try notesByID.append(first)
let note = notesByID.get(first.id)
let added = try notesByID.upsert(revised) // false: Replace at stable position
let removed = notesByID.delete(first.id)

containsKey tests membership. The projector is throwing, so append, replacement, whole-list replacement, and upsert are throwing and atomic. Captured membership keys do not follow mutable properties; indexed replacement or delete-then-add rekeys explicitly. The same mutated instance can occupy its old and newly projected memberships. Duplicate/projector failure preserves state and emits nothing. Lookup and target discovery are expected O(1), append is amortized O(1), and ordered middle shifts remain O(n). Local Combine delivery stays immediate when an external hub transaction defers only hub publication. Items remain caller-owned; the keyed type adds no batch or VM lifecycle interface.

7.5.4. Imperative Engine Bridge

The Equatable overload of subscribeValue uses ==; the isEqual: overload accepts custom equality without an Equatable constraint. Both return AnyCancellable:

import Combine
import VMx

let exposureSubscription: AnyCancellable = try subscribeValue(
    cameraVM,
    selector: { $0.model.exposure },
    callback: { exposure, _ in
        material.uniforms.exposure.value = exposure
    },
    fireImmediately: true
)

// Host adapter disposal:
exposureSubscription.cancel()

The callback receives (current, previous); immediate delivery uses the initial value for both. The host adapter owns the cancellable, and the selector reevaluates after every property message from this fixed VM rather than on every render frame.

7.5.5. Pointers

7.5.6. Current Example Coverage

  • SwiftUI flagship: examples/swift/notes-showcase/

The Swift flavor is at full library parity. Its current example surface is narrower than the other languages, but the flagship README points to the same cross-flavor scenario contract and parity matrix.

7.5.7. Coverage

The coverage floor job in .github/workflows/swift.yml runs the library tests once with code coverage on macos-15 and the default Xcode. It fails when line, region, or function coverage falls below the Swift floors in tools/coverage-floors.json:

swift test --package-path langs/swift --enable-code-coverage \
  -Xswiftc -Xllvm -Xswiftc -instrprof-atomic-counter-update-all
python3 tools/check-coverage-floor.py --flavor swift \
  --report "$(swift test --package-path langs/swift --show-codecov-path)"

The denominator is langs/swift/Sources/VMx/ only. Test files, the bundled JSON resources, and build output are not counted, so a change that touches only generated or resource files cannot fail the gate. Removing a test that exercised library code lowers the figure and can. A failure names each source file that gained uncovered lines since the recorded baseline, with that file's unexecuted line ranges as llvm-cov show marks them. The job uploads the llvm-cov export and a provenance file that names the commit, toolchain, and measured figures. Instrumentation stays in that job's debug build; the release builds, platform builds, and packages never see it.

The coverage build updates its counters atomically. Coverage counts the code after a guard as the function's entries minus its early returns, and plain counter updates can lose entries when a test calls one function from many threads at once, as DISP-003 does with NotificationHub.dispose(). A lost entry can make a region that ran read as unexecuted and fail the floor without a code change (#513).

Coverage says which lines ran, not whether a test would catch a wrong result. It does not replace the conformance assertions.