feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients

Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.

Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.

Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
  libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
  (libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
  stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
  runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.

VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
  Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
  Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
  (10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
  Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
  [UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
  all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
  delivery on main queue via coalesced DispatchQueue.main drain).

Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
  testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
  static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
  lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
  bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.

Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
This commit is contained in:
2026-06-18 14:20:38 +02:00
parent b2af1a3001
commit b4766d2f24
16 changed files with 2019 additions and 61 deletions

View File

@@ -51,13 +51,24 @@ exists from M1 so the protocol can be exercised long before any GUI.
- Focus-scoped PTT (documented limitation — no system-wide hook in v1).
- Admin/moderation UI **out of scope** — needs server-side dispatch first (M5).
**macOS (Swift/SwiftUI) — pending:**
**macOS (Swift/AppKit) — in progress:**
- Same feature set as Windows over the same C ABI (now stable and complete).
- AVAudioSession, mic permission, interruption/route-change handling.
- **Shared Swift core (`VoiceCatCore` package) ✓ complete 2026-06-18** — wraps all 38 C ABI
functions; 6/6 XCTest smoke tests pass against a real server (connect → TOFU → auth →
channels → moderation → admin CRUD → per-stream recv controls). See
`clients/apple/README.md`.
- **UI: AppKit** (not SwiftUI) — chosen for the most mature VoiceOver accessibility story
(per-control `accessibilityLabel`/`accessibilityHelp`/`accessibilityRole`,
`NSAccessibility.post(.announcement)` for live announcements). Same rationale as the
Windows client's WinForms-over-WinUI-3 decision (see §2 below). macOS 14 (Sonoma)
deployment target.
- AVAudioSession not needed on macOS (CoreAudio via the core directly).
**iOS (Swift) — pending:**
**iOS (Swift/SwiftUI) — pending:**
- SwiftUI app consuming the same `VoiceCatCore` package.
- AVAudioSession, mic permission, foreground voice.
- ReplayKit broadcast extension for `SCREEN_AUDIO`.
- `vc_audio_suspend`/`vc_audio_resume` ABI hooks (deferred until this milestone).
**Exit:** non-technical user installs a client, saves a server, and joins.
@@ -120,6 +131,17 @@ Settled and reflected throughout the docs:
Reason: a low-level keyboard hook requires escalated permissions, risks AV flagging, and is
disproportionate complexity for a v1 client. Documented in the UI as a known limitation.
Can be revisited for v2 if users request it. (clients/windows/VoiceCat.App/Forms/MainForm.cs)
- **macOS client UI framework (2026-06-18):** **AppKit**, not SwiftUI. Reason: AppKit has the
most mature, granular **VoiceOver** accessibility story on macOS — per-control
`accessibilityLabel`/`accessibilityHelp`/`accessibilityRole`, `NSAccessibility.post(.announcement)`
for curated live announcements, and decades of real-world screen-reader usage. This is the
same rationale that drove the Windows client to WinForms over WinUI 3 (screen-reader
support is the deciding factor). SwiftUI's VoiceOver support has improved but still has
gaps in complex AppKit-bridged control surfaces (outline views, data-table column
headers, live-region announcements). iOS stays SwiftUI — its control surface is narrower
and SwiftUI's VoiceOver support is sufficient there. This overrides the earlier
"SwiftUI for both macOS + iOS" mention in `docs/tech-stack.md §2` and `docs/architecture.md
§4`. macOS 14 (Sonoma) deployment target. (clients/apple/)
## 3. Open questions