① 중단 확인: 하단 시트(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
62 lines
2.1 KiB
Swift
62 lines
2.1 KiB
Swift
//
|
|
// HapticEnginePlayer.swift
|
|
// Keging
|
|
//
|
|
// Core Haptics 정밀 패턴 재생 — 아이폰 전용·포그라운드 전용 (watchOS SDK엔 CoreHaptics 없음).
|
|
// 시스템 바이브로는 불가능한 짧은 간격의 연타와 1초 이상의 연속 진동을 만든다.
|
|
// 엔진이 백그라운드 등에서 죽어 있으면 1회 재생성 후 재시도, 그래도 실패면 false
|
|
// (호출 측이 시스템 진동으로 폴백).
|
|
//
|
|
|
|
#if canImport(CoreHaptics)
|
|
import CoreHaptics
|
|
|
|
@MainActor
|
|
final class HapticEnginePlayer {
|
|
static let shared = HapticEnginePlayer()
|
|
|
|
private var engine: CHHapticEngine?
|
|
|
|
private init() {}
|
|
|
|
@discardableResult
|
|
func play(_ pattern: HapticPattern) -> Bool {
|
|
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return false }
|
|
if attempt(pattern) { return true }
|
|
engine = nil
|
|
return attempt(pattern)
|
|
}
|
|
|
|
private func attempt(_ pattern: HapticPattern) -> Bool {
|
|
do {
|
|
let engine = try currentEngine()
|
|
let events = pattern.hapticEvents.map { spec in
|
|
CHHapticEvent(
|
|
eventType: .hapticContinuous,
|
|
parameters: [
|
|
CHHapticEventParameter(parameterID: .hapticIntensity, value: spec.intensity),
|
|
CHHapticEventParameter(parameterID: .hapticSharpness, value: spec.sharpness),
|
|
],
|
|
relativeTime: spec.time,
|
|
duration: spec.duration
|
|
)
|
|
}
|
|
let player = try engine.makePlayer(with: try CHHapticPattern(events: events, parameters: []))
|
|
try engine.start()
|
|
try player.start(atTime: CHHapticTimeImmediate)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func currentEngine() throws -> CHHapticEngine {
|
|
if let engine { return engine }
|
|
let newEngine = try CHHapticEngine()
|
|
newEngine.isAutoShutdownEnabled = true
|
|
engine = newEngine
|
|
return newEngine
|
|
}
|
|
}
|
|
#endif
|