Files
voice-cat/clients/apple/iOS/VoiceCatiOS/AudioSessionManager.swift
Talon e26e7db5b1 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.
2026-06-19 02:10:25 +02:00

65 lines
2.3 KiB
Swift

import AVFoundation
import VoiceCatCore
@MainActor
final class AudioSessionManager {
static let shared = AudioSessionManager()
weak var client: VoiceCatClient?
func configure() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, mode: .voiceChat,
options: [.allowBluetooth, .allowBluetoothA2DP,
.defaultToSpeaker, .mixWithOthers])
} catch {
print("[AudioSession] setCategory failed: \(error)")
}
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification, object: nil)
NotificationCenter.default.addObserver(
self, selector: #selector(handleRouteChange),
name: AVAudioSession.routeChangeNotification, object: nil)
}
func activateForStreaming() throws {
try AVAudioSession.sharedInstance().setActive(true, options: [])
}
func deactivateAfterStreaming() {
try? AVAudioSession.sharedInstance().setActive(false,
options: .notifyOthersOnDeactivation)
}
@objc private func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
else { return }
switch type {
case .began:
client?.audioSuspend()
case .ended:
let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
try? AVAudioSession.sharedInstance().setActive(true)
client?.audioResume()
}
@unknown default: break
}
}
@objc private func handleRouteChange(_ notification: Notification) {
NotificationCenter.default.post(name: .voiceCatDeviceListChanged, object: nil)
}
}
extension Notification.Name {
static let voiceCatDeviceListChanged = Notification.Name("cat.voice.deviceListChanged")
}