# Core Haptics

For patterns the feedback generators cannot express: shaped intensity and
sharpness, several events in sequence, haptics synchronised with audio, or a
pattern that changes while it plays (a drag that strengthens with distance).

## Contents

1. Engine lifecycle
2. Events and parameters
3. Playing a pattern
4. Parameter curves and dynamic parameters
5. Advanced players
6. AHAP files
7. Pitfalls

## 1. Engine lifecycle

Check support, create the engine once for the screen or feature, set both
handlers before starting, and stop it when it is no longer needed.

```swift
import CoreHaptics
import OSLog

@MainActor
final class PatternPlayer {
    private var engine: CHHapticEngine?
    private let logger = Logger(subsystem: "app.haptics", category: "engine")

    var supportsHaptics: Bool {
        CHHapticEngine.capabilitiesForHardware().supportsHaptics
    }

    func start() throws {
        guard supportsHaptics, engine == nil else { return }
        let engine = try CHHapticEngine()
        engine.stoppedHandler = { @Sendable [logger] reason in
            logger.info("Haptic engine stopped: \(reason.rawValue)")
        }
        engine.resetHandler = { @Sendable [weak self] in
            Task { @MainActor in
                try? self?.engine?.start()
            }
        }
        try engine.start()
        self.engine = engine
    }

    func stop() {
        engine?.stop()
        engine = nil
    }
}
```

- **Stopped handler**: runs when something outside the app stops the engine
  (an audio interruption such as a call, or the app moving to the
  background). It is not called for your own `stop`. Restart before the next
  pattern.
- **Reset handler**: runs after the haptic server recovers from a failure.
  Restart the engine, re-register custom audio resources, and recreate any
  players you held; old players are invalid.
- Do not assume the handlers run on the main thread; hop to the main actor
  before touching main-actor state.
- **Power**: stop the engine when the screen stops playing patterns, or set
  `isAutoShutdownEnabled = true` to let the engine stop itself after about two
  minutes idle and start on demand.
- `playsHapticsOnly` makes the engine ignore audio events; `isMutedForHaptics`
  and `isMutedForAudio` mute one channel.
- The engine is not a singleton. Several can exist; each behaves independently.

## 2. Events and parameters

| Event type | Feels like |
|---|---|
| `.hapticTransient` | A short tap or impulse |
| `.hapticContinuous` | A sustained vibration for a `duration` |
| `.audioContinuous` | A sustained generated tone |
| `.audioCustom` | An audio file registered with `registerAudioResource(_:options:)` |

Event parameters (`CHHapticEvent.ParameterID`), normalised 0 to 1, where 0
and 1 map to the minimum and maximum the hardware supports:

- `.hapticIntensity`: strength.
- `.hapticSharpness`: soft and round (low) to crisp and mechanical (high).
- `.attackTime`, `.decayTime`, `.releaseTime`, `.sustained`: envelope of a
  continuous event.
- Audio: `.audioVolume`, `.audioPitch`, `.audioPan`, `.audioBrightness`.

Match intensity and sharpness to the visual: a crisp snap is short and sharp,
a soft landing is lower in both.

## 3. Playing a pattern

```swift
func playTap(on engine: CHHapticEngine) throws {
    let tap = CHHapticEvent(
        eventType: .hapticTransient,
        parameters: [
            CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
            CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
        ],
        relativeTime: 0
    )
    let pattern = try CHHapticPattern(events: [tap], parameters: [])
    let player = try engine.makePlayer(with: pattern)
    try player.start(atTime: CHHapticTimeImmediate)
}
```

A player (`CHHapticPatternPlayer`) can `start(atTime:)`, `stop(atTime:)`,
`cancel()`, `sendParameters(_:atTime:)` and `scheduleParameterCurve(_:atTime:)`.

## 4. Parameter curves and dynamic parameters

**Curves** schedule a change over time inside the pattern; values move
linearly between control points.

```swift
let rumble = CHHapticEvent(
    eventType: .hapticContinuous,
    parameters: [
        CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
        CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
    ],
    relativeTime: 0,
    duration: 1.0
)
let swell = CHHapticParameterCurve(
    parameterID: .hapticIntensityControl,
    controlPoints: [
        .init(relativeTime: 0, value: 0.3),
        .init(relativeTime: 0.5, value: 0.7),
        .init(relativeTime: 1.0, value: 1.0)
    ],
    relativeTime: 0
)
let pattern = try CHHapticPattern(events: [rumble], parameterCurves: [swell])
```

**Dynamic parameters** (`CHHapticDynamicParameter`) change the whole pattern
right now, from live input: send them with `sendParameters(_:atTime:)` as a
drag moves. They use the `...Control` IDs (`.hapticIntensityControl`,
`.hapticSharpnessControl`, envelope and audio controls), which scale the
event values rather than replace them.

## 5. Advanced players

`makeAdvancedPlayer(with:)` returns a `CHHapticAdvancedPatternPlayer`, which
adds `loopEnabled` and `loopEnd`, `pause(atTime:)`, `resume(atTime:)`,
`seek(toOffset:)`, `playbackRate` and a `completionHandler`. Use it for looping
or scrubbing; a plain player is enough for one-shot patterns.

## 6. AHAP files

AHAP (Apple Haptic and Audio Pattern) is a JSON file holding one pattern.
Ship it as a bundle resource and load it at run time, so designers can tune
the feel without a rebuild.

```json
{
  "Version": 1.0,
  "Pattern": [
    {
      "Event": {
        "Time": 0.0,
        "EventType": "HapticTransient",
        "EventParameters": [
          { "ParameterID": "HapticIntensity", "ParameterValue": 0.9 },
          { "ParameterID": "HapticSharpness", "ParameterValue": 0.6 }
        ]
      }
    },
    {
      "Event": {
        "Time": 0.12,
        "EventType": "HapticContinuous",
        "EventDuration": 0.3,
        "EventParameters": [
          { "ParameterID": "HapticIntensity", "ParameterValue": 0.5 }
        ]
      }
    }
  ]
}
```

- Top-level keys are only `Version` and `Pattern`; `Pattern` is an array of
  entries, each an `Event`, a `Parameter` (dynamic parameter) or a
  `ParameterCurve`.
- Missing keys take defaults, out-of-range values are clamped, unknown keys
  are ignored.
- Play directly with `engine.playPattern(from: url)`, or build a pattern with
  `CHHapticPattern(contentsOf:)` (iOS 16) to make a player you can control.

```swift
func playCheckout(on engine: CHHapticEngine) throws {
    guard let url = Bundle.main.url(forResource: "checkout", withExtension: "ahap") else { return }
    try engine.playPattern(from: url)
}
```

## 7. Pitfalls

- Starting an engine on a device without haptics: check capabilities first and
  fall back to visual or audio feedback.
- Playing after an interruption without restarting: handle the stopped
  handler, restart lazily before the next pattern.
- Keeping players across a reset: recreate them in the reset handler.
- Leaving the engine running on a screen that no longer plays haptics: it
  costs power.
- Long continuous patterns in an app: they dilute meaning and tire the hand.
- Haptics during camera, microphone or motion capture: they can disturb the
  capture.
