① 중단 확인: 하단 시트(confirmationDialog) → 버튼 자리에서 펼쳐지는 인라인 확인
(안내 문구 + 계속하기(그린)/중단하기(레드), scale+opacity 전환)
② 통계: 아래→위 스와이프로 반전, 하단 중앙 손잡이(위 화살표+캡슐), 통통 튀는 스프링
(dampingFraction 0.62), 통계 상단에 닫기 손잡이 추가, 닫기=아래로 스와이프
③ 진동 3종(기본·톡톡·지이이잉)으로 재설계 — 체이닝 간격도 길다는 피드백:
- iOS 포그라운드: Core Haptics 정밀 재생(Shared/HapticEnginePlayer) — 기본 0.25s,
톡톡 0.14s×2 시작 간격 0.28s(요구 0.5s 미만), 지이이잉 1.2s 연속, 세기 1.0
- iOS 백그라운드 폴백: 시스템 바이브 완료 체이닝(1방/2방/3방 즉시 잇기)
- 워치: watchOS SDK에 CoreHaptics 부재(실측) — 강한 시스템 햅틱 매핑
(기본=.notification, 톡톡=.directionUp, 지이이잉=.retry — 내장이라 간격 뭉개짐 없음)
- 구버전 저장 패턴 자동 이관, 기본값 수축=지이이잉·이완=기본
- 신규 QA 인자 -confirmStop, 도움말 문구·번역(en/ja) 갱신, stale 정리(카탈로그 0/0)
- 검증: Debug·Release·워치 빌드 그린, 화면 QA(메인 하단 손잡이·통계 시트·중단 인라인 레드)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
46 lines
1.7 KiB
Swift
46 lines
1.7 KiB
Swift
//
|
||
// Haptics.swift
|
||
// Keging
|
||
//
|
||
// 진동 패턴 재생 (iOS).
|
||
// - 포그라운드: Core Haptics 정밀 패턴(HapticEnginePlayer) — 짧은 간격 연타·긴 연속 진동
|
||
// - 백그라운드: 시스템 바이브 완료 체이닝 폴백. ⚠️재생 중 같은 시스템 사운드 재호출은
|
||
// 무시되므로(실기기 실측) 완료 콜백으로만 잇는다 — 물리적 최소 간격.
|
||
// 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
|
||
}
|
||
playSystemChain(pattern, generation: generation)
|
||
}
|
||
|
||
/// 백그라운드 폴백 — 완료 즉시 잇는 것이 시스템 바이브의 최소 간격
|
||
private static func playSystemChain(_ pattern: HapticPattern, generation: Int) {
|
||
let count = switch pattern {
|
||
case .basic: 1
|
||
case .doubleTap: 2
|
||
case .long: 3 // 이어붙여 긴 진동감
|
||
}
|
||
playChain(remaining: count, 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)
|
||
}
|
||
}
|
||
}
|