Library / Pickers, sliders & steppers

Rotating dial / ratchet

A rotary knob (volume, timer) that clicks per step, with sharper clicks at faster spin — Digital-Crown style.

0ms 100ms 200ms 300ms 400ms
Feel
Ratchet clicks whose sharpness rises with rotation speed, so fast spins feel mechanical and precise.
APIs
CHHapticEvent(.hapticTransient)RotateGesture / DragGesture
Minimum OS
iOS 13+
Raw .md
Core Haptics + SwiftUI
import SwiftUI
import CoreHaptics

struct RatchetDial: View {
    @State private var angle: Angle = .zero
    @State private var lastStep = 0
    @State private var lastTime = Date()
    @State private var engine: CHHapticEngine?
    let degreesPerStep = 15.0

    var body: some View {
        Circle()
            .fill(.gray.gradient)
            .overlay(alignment: .top) {
                Capsule().fill(.white).frame(width: 6, height: 24).padding(8)
            }
            .frame(width: 180, height: 180)
            .rotationEffect(angle)
            .gesture(
                RotateGesture()
                    .onChanged { value in
                        angle = value.rotation
                        let step = Int(value.rotation.degrees / degreesPerStep)
                        if step != lastStep {
                            // Sharper click when spinning fast
                            let dt = Date().timeIntervalSince(lastTime)
                            let sharpness = Float(min(1, 0.3 + 0.1 / max(dt, 0.02)))
                            click(sharpness: sharpness)
                            lastStep = step
                            lastTime = Date()
                        }
                    }
            )
            .onAppear {
                guard CHHapticEngine.capabilitiesForHardware().supportsHaptics
                else { return }
                engine = try? CHHapticEngine()
                try? engine?.start()
            }
    }

    private func click(sharpness: Float) {
        guard let engine else { return }
        let event = CHHapticEvent(eventType: .hapticTransient, parameters: [
            CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
            CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness),
        ], relativeTime: 0)
        if let pattern = try? CHHapticPattern(events: [event], parameters: []),
           let player = try? engine.makePlayer(with: pattern) {
            try? player.start(atTime: CHHapticTimeImmediate)
        }
    }
}
Copied