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:
@@ -136,11 +136,7 @@ Design notes:
|
||||
|
||||
### Per-platform binding notes
|
||||
|
||||
- **Swift / Apple.** Import the C ABI via a module map; SwiftUI on top. On **iOS** the app
|
||||
must still own `AVAudioSession` (category `.playAndRecord`, `.voiceChat` mode), request
|
||||
mic permission, and handle interruptions/route changes — the core exposes hooks
|
||||
(`vc_audio_suspend`/`vc_audio_resume`) the Swift layer calls from `AVAudioSession`
|
||||
notifications. Background voice and VoIP push (CallKit/PushKit) are a later milestone.
|
||||
- **Swift / Apple.** Import the C ABI via a **module map** (`module VoiceCatC { header "voicecat.h" }`) staged into the XCFramework headers by `clients/apple/scripts/build-xcframework.sh` — Swift gets a clean `import VoiceCatC` with all C enums/structs/functions available directly (no manual redeclaration, unlike the C# P/Invoke layer). A **Swift wrapper** (`VoiceCatCore` package at `clients/apple/`) provides Swift-idiomatic types (`VoiceCatResult`, `VoiceCatEvent`, `Channel`, `User`, etc.) on top, mirroring the C# `VoiceCat.Interop` layer. Callbacks use `@convention(c)` closures (plain C function pointers, not ARC-managed closures) + `Unmanaged.passUnretained(self)` as the `user` context (the Swift analog of C#'s `[UnmanagedCallersOnly]` + `GCHandle`). Events are delivered on `@MainActor` via a coalesced `DispatchQueue.main` drain (one async block scheduled at a time) — the Swift analog of C#'s `Channel<VoiceCatEvent>` + 30ms WinForms Timer pump. `deinit` calls `vc_client_destroy` (joins all threads) then frees native CString config storage (the core stores raw pointers, doesn't copy). **macOS UI: AppKit** (chosen over SwiftUI for the most mature VoiceOver accessibility story — same rationale as the Windows client's WinForms choice); **iOS UI: SwiftUI** (narrower control surface, sufficient VoiceOver support). On **iOS** the app must still own `AVAudioSession` (category `.playAndRecord`, `.voiceChat` mode), request mic permission, and handle interruptions/route changes — the core exposes hooks (`vc_audio_suspend`/`vc_audio_resume`) the Swift layer calls from `AVAudioSession` notifications (these hooks are deferred until the iOS client milestone to keep the ABI stable). Background voice and VoIP push (CallKit/PushKit) are a later milestone. The XCFramework carries a **fat static library** (`libvoicecat-fat.a`) bundling `libvoicecat.a` + all vcpkg static deps so the Swift Package links a single self-contained `.a` per slice.
|
||||
- **iOS screen / system-audio sharing** is supported via a **ReplayKit Broadcast Upload
|
||||
Extension** (the same mechanism Discord uses; triggered from Control Center's screen-record
|
||||
button via `RPSystemBroadcastPickerView`). The extension receives
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ explicit resampling (speexdsp/libsamplerate) is only needed when a device can't
|
||||
|
||||
| Concern | Choice | Notes |
|
||||
|---------|--------|-------|
|
||||
| Language | **Swift 5.9+** | Direct **Swift↔C++ interop** available, but we bind through the C ABI for parity with Windows. |
|
||||
| UI | **SwiftUI** | Single UI codebase for macOS + iOS where practical; AppKit/UIKit shims as needed. |
|
||||
| Audio session (iOS) | **AVAudioSession** | App owns category `.playAndRecord` + `.voiceChat` mode, mic permission, interruption/route-change handling; calls `vc_audio_suspend/resume` on the core. macOS uses CoreAudio via the core directly. |
|
||||
| Packaging | Swift Package + Xcode project | Core shipped as an XCFramework (device + simulator + macOS slices). |
|
||||
| Language | **Swift 5.9+** | Direct **Swift↔C interop** — the C ABI (`voicecat.h`) is imported as a Clang module (`import VoiceCatC`) via a module map in the XCFramework headers; no manual struct/function redeclaration (unlike the C# P/Invoke layer). A Swift wrapper (`VoiceCatCore` package) provides Swift-idiomatic types on top. |
|
||||
| UI — macOS | **AppKit** | Chosen over SwiftUI for the most mature, granular **VoiceOver** accessibility story (per-control `accessibilityLabel`/`accessibilityHelp`/`accessibilityRole`, `NSAccessibility.post(.announcement)` for live announcements) — the same rationale that drove the Windows client to WinForms over WinUI 3 for screen-reader (NVDA/JAWS/Narrator) UIA support (resolved decision in `docs/roadmap.md`). macOS 14 (Sonoma) deployment target. |
|
||||
| UI — iOS | **SwiftUI** | iOS has a narrower control surface (no channel-tree moderation, etc.) and SwiftUI's VoiceOver support is sufficient; revisit if gaps emerge. |
|
||||
| Shared core | **VoiceCatCore** Swift Package | One Swift library wrapping the C ABI, consumed by both the macOS AppKit app and the iOS SwiftUI app. Mirrors the C# `VoiceCat.Interop` layer. Events delivered on `@MainActor` via a coalesced `DispatchQueue.main` drain (the Swift analog of C#'s `Channel<VoiceCatEvent>` + 30ms WinForms Timer pump). |
|
||||
| Audio session (iOS) | **AVAudioSession** | App owns category `.playAndRecord` + `.voiceChat` mode, mic permission, interruption/route-change handling; calls `vc_audio_suspend/resume` on the core. macOS uses CoreAudio via the core directly. (`vc_audio_suspend`/`vc_audio_resume` ABI hooks are deferred until the iOS client milestone — keep ABI stable.) |
|
||||
| Packaging | Swift Package + Xcode project | Core shipped as an **XCFramework** binary target — a fat static library (`libvoicecat-fat.a`) bundling `libvoicecat.a` + all vcpkg static deps (protobuf/mbedtls/sodium/opus/sqlite3/spdlog/asio), so the Swift Package links a single self-contained `.a` per slice. macOS slice validated; iOS device + sim slices are scaffolding. |
|
||||
| Future | CallKit / PushKit | For background VoIP + incoming-call UX on iOS. Post-v1. |
|
||||
|
||||
### Windows — C# (shipped in M4, 2026-06-17)
|
||||
|
||||
Reference in New Issue
Block a user