- '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
45 lines
2.0 KiB
Swift
45 lines
2.0 KiB
Swift
//
|
||
// Haptics.swift
|
||
// Keging
|
||
//
|
||
// 진동 패턴 재생 (iOS) — 시스템 바이브(1회 ≈0.5초)를 완료 콜백으로 체이닝한다.
|
||
// ⚠️ 재생 중에 같은 시스템 사운드를 다시 호출하면 무시되므로(실기기 실측 — 겹침·짧은
|
||
// 간격 오프셋이 전부 먹혀 패턴이 똑같아지던 버그) 반드시 완료 후 다음 진동을 쏜다.
|
||
// AudioServices 바이브는 무음 스위치 무관, 오디오 세션이 살아 있으면 백그라운드에서도 울린다.
|
||
//
|
||
|
||
import AudioToolbox
|
||
import Foundation
|
||
|
||
@MainActor
|
||
enum Haptics {
|
||
/// 진행 중 체인 취소용 세대 토큰 — 새 패턴이 시작되면 이전 체인의 남은 진동을 버린다
|
||
/// (수축·이완이 짧을 때 이전 체인이 다음 국면의 첫 진동을 삼키는 것 방지)
|
||
private static var generation = 0
|
||
|
||
static func play(_ pattern: HapticPattern) {
|
||
// 각 진동 앞의 쉼(초) — 첫 진동은 즉시
|
||
let gaps: [TimeInterval]
|
||
switch pattern {
|
||
case .short: gaps = [0] // 톡: 1방
|
||
case .doubleTap: gaps = [0, 0.25] // 톡톡: 완료 후 0.25초 쉬고 1방 더
|
||
case .rattle: gaps = [0, 0.15, 0.15] // 드르륵: 짧은 쉼으로 3연타
|
||
case .long: gaps = [0, 0, 0] // 지잉: 완료 즉시 이어 붙여 긴 진동감
|
||
}
|
||
generation += 1
|
||
playChain(gaps, generation: generation)
|
||
}
|
||
|
||
private static func playChain(_ gaps: [TimeInterval], generation: Int) {
|
||
guard generation == Self.generation, let gap = gaps.first else { return }
|
||
let rest = Array(gaps.dropFirst())
|
||
Task { @MainActor in
|
||
if gap > 0 { try? await Task.sleep(for: .seconds(gap)) }
|
||
guard generation == Self.generation else { return }
|
||
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) { @MainActor @Sendable in
|
||
playChain(rest, generation: generation)
|
||
}
|
||
}
|
||
}
|
||
}
|