- 세트 엔진(수축→이완×횟수, 중단=미기록) iOS·워치 공용, 완주만 records.json 저장 - 첫 화면 시작 버튼 + 세팅(시간·횟수·진동 패턴)/설정(테마) 시트, 아래 스와이프 통계 - 통계: 하루(시간대별)/주간/월간 꺾은선, 달력 기준↔롤링(지난 7일·30일) 전환 - Live Activity(수축/이완·n/총·카운트다운), 백그라운드 무음 오디오로 타이머·진동 유지 - 단축어 '케겔 운동 시작', 워치 앱(즉시 시작·워치 전용 진동·physical-therapy 세션·기록 폰 병합) - 하루 다님 팔레트 계승(라이트/다크/시스템), 앱 아이콘 SVG→PNG 등록 - 검증: Debug·Release·워치 빌드 그린, 시뮬 QA 10장(26·18.5·워치 46/40mm), 완주·기록 경로 확인 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
52 lines
1.8 KiB
Swift
52 lines
1.8 KiB
Swift
//
|
|
// BackgroundAudioKeeper.swift
|
|
// Keging
|
|
//
|
|
// 운동 중 무음 오디오 루프 재생 — 앱을 나가도 타이머·진동·Live Activity 갱신이 유지되게 한다.
|
|
// (UIBackgroundModes=audio, .mixWithOthers라 음악 재생을 방해하지 않음)
|
|
//
|
|
|
|
import AVFoundation
|
|
|
|
@MainActor
|
|
final class BackgroundAudioKeeper {
|
|
static let shared = BackgroundAudioKeeper()
|
|
|
|
private var player: AVAudioPlayer?
|
|
|
|
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
|
|
player?.volume = 0
|
|
player?.play()
|
|
}
|
|
|
|
func stop() {
|
|
player?.stop()
|
|
player = nil
|
|
try? AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
|
|
}
|
|
|
|
/// 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
|
|
}()
|
|
}
|