feat(ios): ship iOS SwiftUI client (VoiceCatiOS)
Full SwiftUI app at clients/apple/iOS/VoiceCatiOS.xcodeproj:
- 24 Swift source files: AppState + SessionState (@Observable @MainActor),
AudioSessionManager (AVAudioSession owner + interruption/route handling),
ServerListStore/SavedServer (App Group container + Keychain sharing),
and 14 SwiftUI views covering the full feature set
- NavigationSplitView on iPad, TabView on iPhone (horizontalSizeClass)
- Channel tree via OutlineGroup, user list with context menu admin actions
- PTT via DragGesture(minimumDistance: 0) + @GestureState
- onEvent closures hop to MainActor via Task { @MainActor in ... }
- App Group: group.cat.voice.VoiceCat (shared with future ReplayKit extension)
C ABI: add vc_audio_suspend / vc_audio_resume (AudioEngine::suspend/resume)
called by AudioSessionManager on AVAudioSession interruption events.
XCFramework: add ios-arm64 and ios-arm64-simulator slices to build-xcframework.sh;
Package.swift gains .iOS(.v17) platform; CMakePresets.json adds apple-ios /
apple-ios-sim presets with arm64-ios / arm64-ios-simulator vcpkg triplets.
Verified: xcodebuild -target VoiceCatiOS -sdk iphonesimulator26.5 BUILD SUCCEEDED.
This commit is contained in:
94
clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift
Normal file
94
clients/apple/iOS/VoiceCatiOS/Views/SettingsView.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
import SwiftUI
|
||||
import VoiceCatCore
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(AppState.self) private var appState
|
||||
@Bindable var session: SessionState
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Voice input section
|
||||
Section("Voice Input") {
|
||||
Picker("Input Mode", selection: Binding(
|
||||
get: { session.voiceState.inputMode },
|
||||
set: { session.setInputMode($0) }
|
||||
)) {
|
||||
Text("Voice Activation").tag(VoiceCatInputMode.voiceActivation)
|
||||
Text("Push to Talk").tag(VoiceCatInputMode.pushToTalk)
|
||||
Text("Always On").tag(VoiceCatInputMode.alwaysOn)
|
||||
}
|
||||
.accessibilityLabel("Voice input mode")
|
||||
|
||||
if session.voiceState.inputMode == .voiceActivation {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("VAD Threshold: \(String(format: "%.3f", session.voiceState.vadThreshold))")
|
||||
.font(.caption)
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(session.voiceState.vadThreshold) },
|
||||
set: { session.setVadThreshold(Float($0)) }
|
||||
),
|
||||
in: 0.001...0.1, step: 0.001
|
||||
)
|
||||
.accessibilityLabel("Voice activation threshold")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio device section
|
||||
if !session.devices.isEmpty {
|
||||
Section("Input Device") {
|
||||
Picker("Microphone", selection: Binding(
|
||||
get: { session.voiceState.currentDeviceId ?? "" },
|
||||
set: { id in
|
||||
session.voiceState.currentDeviceId = id.isEmpty ? nil : id
|
||||
if session.voiceState.localStreamId != 0 {
|
||||
session.client.setInputDevice(
|
||||
streamId: session.voiceState.localStreamId,
|
||||
deviceId: id.isEmpty ? nil : id)
|
||||
}
|
||||
}
|
||||
)) {
|
||||
Text("Default").tag("")
|
||||
ForEach(session.devices) { dev in
|
||||
Text(dev.name).tag(dev.id)
|
||||
}
|
||||
}
|
||||
.accessibilityLabel("Microphone selection")
|
||||
}
|
||||
}
|
||||
|
||||
// Admin section
|
||||
if session.permissions.canAdminAccounts || session.permissions.isAdmin {
|
||||
Section("Administration") {
|
||||
NavigationLink("Manage Accounts") {
|
||||
AccountsView(session: session)
|
||||
}
|
||||
.accessibilityLabel("Manage server accounts")
|
||||
}
|
||||
}
|
||||
|
||||
// Server info
|
||||
Section("Server") {
|
||||
Button(role: .destructive) {
|
||||
session.stopMicStream()
|
||||
appState.disconnect()
|
||||
} label: {
|
||||
Label("Disconnect", systemImage: "phone.down")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.accessibilityLabel("Disconnect from server")
|
||||
}
|
||||
|
||||
Section("About") {
|
||||
Text(VoiceCatClient.versionString)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.accessibilityLabel("Version: \(VoiceCatClient.versionString)")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user