- CHHapticEngine을 공유 AVAudioSession에 부착(기본 생성 시 독자 오디오 정책이 백그라운드 무음 루프를 끊던 것이 DI 멈춤 원인) + 오디오 키퍼 하드닝(루트 변경· 미디어 서버 리셋·ensureAlive) + 운동 중 2초 하트비트(오디오·엔진·LA 자가 치유) - Live Activity: 실제 phaseStart 사용·동일 상태 no-op·staleDate 8초·isStale 흐림 - 워치 세팅 동기화 3중화: 변경 push + reachability 재푸시 + 워치 앱 열릴 때 sendMessage pull - 완주 진동 신설: 폰 2.4초 연속(백그라운드 6방 체인, 오디오 정지 4초 지연), 워치 retry×2+success - 통계 하루 탭 맨 위 '오늘의 타임라인'(24시간 축 점 그래프, 시각 라벨·지금 룰) - 폰 칼로리 공식을 애플워치 '기타' 운동 규칙(빠르게 걷기 상당)에 정합: MET 4.0+강도×0.15 - 도움말 4건 갱신·en/ja 번역·CFBundleName 보충, CLAUDE.md 현행화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
53 lines
2.1 KiB
Swift
53 lines
2.1 KiB
Swift
//
|
||
// Haptics.swift
|
||
// Keging
|
||
//
|
||
// 진동 패턴 재생 (iOS).
|
||
// - 포그라운드: Core Haptics 정밀 패턴(HapticEnginePlayer) — 짧은 간격 연타·긴 연속 진동
|
||
// - 백그라운드: 시스템 바이브 완료 체이닝 폴백. iOS는 백그라운드 커스텀 햅틱을 막아두어
|
||
// (Core Haptics 포그라운드 전용) 시스템 진동 연타가 공개 API의 한계 — 질감이 조금 다르다.
|
||
// ⚠️재생 중 같은 시스템 사운드 재호출은 무시되므로(실기기 실측) 완료 콜백으로만 잇는다.
|
||
// AudioServices 바이브는 무음 스위치 무관, 오디오 세션이 살아 있으면 백그라운드에서도 울린다.
|
||
//
|
||
|
||
import AudioToolbox
|
||
import UIKit
|
||
|
||
@MainActor
|
||
enum Haptics {
|
||
/// 진행 중 체인 취소용 세대 토큰 — 새 패턴이 시작되면 이전 체인의 남은 진동을 버린다
|
||
private static var generation = 0
|
||
|
||
static func play(_ pattern: HapticPattern) {
|
||
generation += 1
|
||
if UIApplication.shared.applicationState == .active,
|
||
HapticEnginePlayer.shared.play(pattern) {
|
||
return
|
||
}
|
||
let count = switch pattern {
|
||
case .basic: 1
|
||
case .doubleTap: 2
|
||
case .long: 3 // 이어붙여 긴 진동감
|
||
}
|
||
playChain(remaining: count, generation: generation)
|
||
}
|
||
|
||
/// 완주 축하 진동 — 아주 긴 "우우우우웅" (백그라운드는 6방 연속 체인으로 근사)
|
||
static func playFinish() {
|
||
generation += 1
|
||
if UIApplication.shared.applicationState == .active,
|
||
HapticEnginePlayer.shared.playFinish() {
|
||
return
|
||
}
|
||
playChain(remaining: 6, generation: generation)
|
||
}
|
||
|
||
/// 백그라운드 폴백 — 완료 즉시 잇는 것이 시스템 바이브의 최소 간격
|
||
private static func playChain(remaining: Int, generation: Int) {
|
||
guard remaining > 0, generation == Self.generation else { return }
|
||
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) { @MainActor @Sendable in
|
||
playChain(remaining: remaining - 1, generation: generation)
|
||
}
|
||
}
|
||
}
|