Library / Press & hold

Hold-to-confirm with rising tension

A "hold 1.5 s to confirm" button: vibration ramps up while holding, success fires at completion.

0ms 140ms 280ms 420ms 560ms
Feel
A continuous buzz that grows from faint to strong, resolved by a success pattern. Releasing early cuts it instantly.
APIs
CHHapticContinuousEventCHHapticParameterCurveUINotificationFeedbackGenerator
Minimum OS
iOS 13+
Raw .md
Core Haptics + SwiftUI
import SwiftUI
import CoreHaptics

struct HoldToConfirmButton: View {
    let duration: TimeInterval = 1.5
    let onConfirm: () -> Void

    @State private var engine: CHHapticEngine?
    @State private var player: CHHapticAdvancedPatternPlayer?
    @State private var progress: CGFloat = 0
    @State private var holding = false

    var body: some View {
        Text("Hold to confirm")
            .font(.headline).foregroundStyle(.white)
            .frame(maxWidth: .infinity).padding()
            .background(
                GeometryReader { geo in
                    ZStack(alignment: .leading) {
                        Color.red.opacity(0.4)
                        Color.red.frame(width: geo.size.width * progress)
                    }
                }, in: .rect(cornerRadius: 14)
            )
            .onLongPressGesture(
                minimumDuration: duration,
                perform: confirm,
                onPressingChanged: pressingChanged
            )
            .onAppear(perform: setupEngine)
    }

    private func setupEngine() {
        guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
        engine = try? CHHapticEngine()
        try? engine?.start()
    }

    private func pressingChanged(_ pressing: Bool) {
        holding = pressing
        if pressing {
            startRamp()
            withAnimation(.linear(duration: duration)) { progress = 1 }
        } else {
            try? player?.stop(atTime: CHHapticTimeImmediate) // cancel = cut
            withAnimation(.easeOut(duration: 0.2)) { progress = 0 }
        }
    }

    private func startRamp() {
        guard let engine else { return }
        // Continuous event whose intensity climbs 0.1 → 1.0 over the hold
        let event = CHHapticEvent(
            eventType: .hapticContinuous,
            parameters: [
                CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
                CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4),
            ],
            relativeTime: 0,
            duration: duration
        )
        let curve = CHHapticParameterCurve(
            parameterID: .hapticIntensityControl,
            controlPoints: [
                .init(relativeTime: 0, value: 0.1),
                .init(relativeTime: duration, value: 1.0),
            ],
            relativeTime: 0
        )
        if let pattern = try? CHHapticPattern(events: [event],
                                              parameterCurves: [curve]) {
            player = try? engine.makeAdvancedPlayer(with: pattern)
            try? player?.start(atTime: CHHapticTimeImmediate)
        }
    }

    private func confirm() {
        UINotificationFeedbackGenerator().notificationOccurred(.success)
        onConfirm()
    }
}
Copied