mycode/myApp/Keging/IOS/BackgroundAudioKeeper.swift
songyc macbook 3cee5f08af polish(keging): 전면 자가 점검 — 클린룸 빌드 검증·유령 DI 정리·진동 체인 취소·경고 0
- 'cannot find in scope' 판정: 새 DerivedData 클린룸 빌드 3종(Debug/Release/워치) 전부 그린
  → SourceKit 편집기 오탐 확정 (실제 컴파일 에러 0)
- 강제 종료 유령 Live Activity 정리: 운동 중 kill 시 DI가 몇 시간 잔존(시뮬 재현) →
  앱 시작 시 cleanupStaleActivities()로 즉시 소멸(실측 확인)
- 진동 체인 세대 토큰: 짧은 수축·이완에서 이전 패턴 체인이 다음 국면 첫 진동을 삼키는
  엣지 방지, Haptics를 Task 기반으로 재작성해 Swift 6 경고 3건 전부 제거
- 워치에도 -noHealth QA 인자 추가
- 시뮬 실측: DI 백그라운드 갱신(수축 그린→이완 앰버)·포그라운드 복귀 벽시계 재동기화
  (5/20 정확 착지)·유령 DI 정리·주간 통계 검산(11세트/220회)·완주 경로·워치 실행·
  18.5 빌드 그린·전 카탈로그 missing/stale/ASCII~ 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-08-29 23:30:14 +09:00

91 lines
3.6 KiB
Swift

//
// BackgroundAudioKeeper.swift
// Keging
//
// ··Live Activity .
// (UIBackgroundModes=audio, .mixWithOthers )
// ( )· .
//
import AVFoundation
import UIKit
@MainActor
final class BackgroundAudioKeeper {
static let shared = BackgroundAudioKeeper()
private var player: AVAudioPlayer?
private var observers: [NSObjectProtocol] = []
private init() {}
func start() {
guard player == nil else { return }
let session = AVAudioSession.sharedInstance()
try? session.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try? session.setActive(true)
player = try? AVAudioPlayer(data: Self.silenceWAV)
player?.numberOfLoops = -1
// 0.0
player?.volume = 0.1
player?.prepareToPlay()
player?.play()
installObservers()
}
func stop() {
removeObservers()
player?.stop()
player = nil
try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
}
/// ( · )
private func revive() {
guard let player, !player.isPlaying else { return }
try? AVAudioSession.sharedInstance().setActive(true)
player.play()
}
private func installObservers() {
guard observers.isEmpty else { return }
let center = NotificationCenter.default
observers.append(center.addObserver(
forName: AVAudioSession.interruptionNotification, object: nil, queue: .main
) { [weak self] note in
guard let self,
let info = note.userInfo,
let rawType = info[AVAudioSessionInterruptionTypeKey] as? UInt,
AVAudioSession.InterruptionType(rawValue: rawType) == .ended else { return }
Task { @MainActor in self.revive() }
})
for name in [UIApplication.didEnterBackgroundNotification, UIApplication.didBecomeActiveNotification] {
observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
guard let self else { return }
Task { @MainActor in self.revive() }
})
}
}
private func removeObservers() {
observers.forEach { NotificationCenter.default.removeObserver($0) }
observers.removeAll()
}
/// 1 WAV (8kHz 16bit mono)
private static let silenceWAV: Data = {
let sampleRate = 8000
let dataSize = sampleRate * 2
var d = Data()
func append(_ s: String) { d.append(s.data(using: .ascii)!) }
func appendU32(_ v: UInt32) { withUnsafeBytes(of: v.littleEndian) { d.append(contentsOf: $0) } }
func appendU16(_ v: UInt16) { withUnsafeBytes(of: v.littleEndian) { d.append(contentsOf: $0) } }
append("RIFF"); appendU32(UInt32(36 + dataSize)); append("WAVE")
append("fmt "); appendU32(16); appendU16(1); appendU16(1)
appendU32(UInt32(sampleRate)); appendU32(UInt32(sampleRate * 2)); appendU16(2); appendU16(16)
append("data"); appendU32(UInt32(dataSize))
d.append(Data(count: dataSize))
return d
}()
}