// // 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) } } }