iOS SDK
The Pulsar iOS SDK provides haptic feedback through three building blocks: Presets, PatternComposer, and RealtimeComposer. All are accessed through the main Pulsar class.
Requirements
Section titled “Requirements”- iOS 13.0+
- Swift 5.9+
Installation
Section titled “Installation”Latest available version: 1.4.0
Add Pulsar as a Swift Package dependency in Xcode:
- Go to File > Add Package Dependencies…
- Enter the repository URL
- Select the Pulsar library product
Or add it to your Package.swift:
dependencies: [ .package(url: "https://github.com/software-mansion-labs/pulsar-ios", from: "1.4.0")]Create a Pulsar instance to access all SDK features:
import Pulsar
let pulsar = Pulsar()Presets
Section titled “Presets”A collection of ready-to-use haptic patterns accessed through pulsar.getPresets().
let presets = pulsar.getPresets()Built-in presets
Section titled “Built-in presets”| Method | Description |
|---|---|
afterglow() | A three-beat phrase that dissolves gently, ideal for soft endings or gradually quieting feedback. |
aftershock() | A firm opening that settles calmly, ideal for transitions needing a strong start and a gentle finish. |
alarm() | Relentless and urgent, best for critical errors or emergencies that require immediate attention. |
anvil() | The full weight of a massive collision, conveys sheer physical force and momentum. |
applause() | A growing wave of appreciation, ideal for celebratory moments or social approval. |
Example
let presets = pulsar.getPresets()
presets.dogBark()System presets
Section titled “System presets”Plays the platform’s platform haptic feedback presets.
| Method | Description |
|---|---|
systemImpactHeavy() | UIImpactFeedbackGenerator.heavy |
systemImpactLight() | UIImpactFeedbackGenerator.light |
systemImpactMedium() | UIImpactFeedbackGenerator.medium |
systemImpactRigid() | UIImpactFeedbackGenerator.rigid |
systemImpactSoft() | UIImpactFeedbackGenerator.soft |
Example
let presets = pulsar.getPresets()
presets.systemImpactHeavy()Playing by name
Section titled “Playing by name”You can also play a preset by its string name using getByName(_:). This returns a Preset? that you call play() on.
Available names:
AfterglowAftershockAlarmAnvilApplauseAscentBalloonPopBarrage
Example
let presets = pulsar.getPresets()
// Direct callpresets.dogBark()
// By namepresets.getByName("Success")?.play()PatternComposer
Section titled “PatternComposer”Composes and plays custom haptic patterns. Get a new instance from pulsar.getPatternComposer(). Each call returns a fresh composer, so you can manage multiple patterns independently.
let composer = pulsar.getPatternComposer()Methods
Section titled “Methods”parsePattern(hapticsData:)
Section titled “parsePattern(hapticsData:)”Parses a PatternData object and prepares it for playback.
func parsePattern(hapticsData: PatternData, fromMs: Double = 0)fromMs starts the pattern that far into its own timeline. The engine can only play a parsed pattern from zero, so Pulsar re-anchors it instead: discrete events before the seek are dropped and the rest rebased, and each continuous envelope is re-anchored on its value at that instant.
parsePatternWithSound(hapticsData:uri:volume:offset:start:duration:)
Section titled “parsePatternWithSound(hapticsData:uri:volume:offset:start:duration:)”Parses a pattern together with a short sound played in sync with the haptics. The sound is registered as a Core Haptics audio event on the same timeline as the pattern, so audio and haptics stay sample-accurate. volume (0–1) and offset (milliseconds, shifting the audio relative to the haptics) are optional.
func parsePatternWithSound( hapticsData: PatternData, uri: String, volume: Float = 1, offset: Double = 0, start: Double = 0, duration: Double = 0, fromMs: Double = 0)uri must resolve to a local file — an absolute path, a file:// URL, or the name of a resource bundled in the app. When no extension is given it defaults to .wav, so a bare name like "beep" resolves beep.wav from the app bundle (add the file to your target’s Copy Bundle Resources). Requires a real device — Core Haptics is unavailable on the simulator — and a missing or unregisterable file degrades gracefully to haptics-only.
start and duration (both in milliseconds) trim the clip: start is where playback begins in the source file, duration is how much of it to play (0 plays to the end). Core Haptics registers an audio resource by URL only, so when either is set the file is sliced to a temporary .caf first — the temp file is cleaned up on the next parse and on dispose.
fromMs is a different thing: it seeks the whole preset, re-anchoring the haptics and advancing into the file so both move together. A preset authored with an audio offset keeps its lead-in until the seek passes it.
playPattern(hapticsData:)
Section titled “playPattern(hapticsData:)”Parses and immediately plays a pattern. Equivalent to calling parsePattern followed by play.
func playPattern(hapticsData: PatternData)play()
Section titled “play()”Plays the previously parsed pattern.
func play()stop()
Section titled “stop()”Stops all playback.
func stop()Example
Section titled “Example”let composer = pulsar.getPatternComposer()
let pattern = PatternData( continuousPattern: ContinuousPattern( amplitude: [ ValuePoint(time: 0, value: 0), ValuePoint(time: 200, value: 1), ValuePoint(time: 400, value: 0), ], frequency: [ ValuePoint(time: 0, value: 0.3), ValuePoint(time: 400, value: 0.8), ] ), discretePattern: [ DiscretePoint(time: 0, amplitude: 1, frequency: 0.5), DiscretePoint(time: 100, amplitude: 0.5, frequency: 0.5), ])
composer.playPattern(hapticsData: pattern)RealtimeComposer
Section titled “RealtimeComposer”Provides real-time haptic control with live amplitude and frequency modulation. Useful for gesture-driven or continuously evolving haptic experiences. Get the shared instance from pulsar.getRealtimeComposer().
let realtime = pulsar.getRealtimeComposer()Methods
Section titled “Methods”set(amplitude:frequency:)
Section titled “set(amplitude:frequency:)”Updates the ongoing haptic with new amplitude and frequency values. Automatically starts playback if it is not already active. Values are clamped to the 0-1 range.
func set(amplitude: Float, frequency: Float)playDiscrete(amplitude:frequency:)
Section titled “playDiscrete(amplitude:frequency:)”Plays a single discrete haptic event.
func playDiscrete(amplitude: Float = 1.0, frequency: Float = 0.5)stop()
Section titled “stop()”Stops the active continuous haptic.
func stop()Properties
Section titled “Properties”isActive
Section titled “isActive”Returns true if a continuous haptic is currently playing.
var isActive: Bool { get }Example
Section titled “Example”let realtime = pulsar.getRealtimeComposer()
// Start a continuous hapticrealtime.set(amplitude: 0.5, frequency: 0.8)
// Update parameters over timerealtime.set(amplitude: 1.0, frequency: 0.3)
// Play a one-off discrete eventrealtime.playDiscrete(amplitude: 0.7, frequency: 0.5)
// Stoprealtime.stop()Preset bundles
Section titled “Preset bundles”A .pulsar bundle exported from Pulsar Studio holds your own presets — each a haptic pattern with
optional synced audio and animation. pulsar-gen turns a bundle into a typed accessor, so every
preset is a real member: a renamed preset becomes a compile error rather than a silent runtime
miss.
Generate the accessor
Section titled “Generate the accessor”Add the .pulsar to your app target (Copy Bundle Resources), then either apply the SwiftPM build
plugin so the accessor regenerates on every build:
.target( name: "MyApp", plugins: [.plugin(name: "PulsarGenPlugin", package: "Pulsar")])…or run the CLI once and commit the result — the practical option for an Xcode app target:
npx pulsar-gen acme-pack.pulsar --target swift --out Sources/MyApp/Load and play
Section titled “Load and play”let pulsar = Pulsar()let bundle = try pulsar.loadBundleSync(AcmePack.descriptor)
bundle.heartbeatV2.play()bundle.explosion.stop()loadBundleAsync is the same load with the file read off the calling thread:
let bundle = try await pulsar.loadBundleAsync(AcmePack.descriptor)The descriptor finds the .pulsar in the app’s main bundle, and its content hash is checked
against the generated types so a stale copy fails loudly. Pass strict: false to skip that.
Each PresetHandle exposes id, name, duration, pattern, hasAudio, hasAnimation, play() / play(fromMs:), stop(), and animation — the Lottie bytes
and timing for your own animation view. Pulsar carries and time-aligns the animation; the app
renders it:
Pass fromMs to start a preset that far into its own timeline — audio and haptics seek together, so a progress bar can scrub it:
bundle.heartbeatV2.play(fromMs: 2500)Every non-zero seek re-parses the preset; playing from the start reuses the cached parse.
The Lottie SDK takes a PresetHandle directly and reads all of that for you — pattern, animation and duration — so you rarely have to unpack it by hand.
if let animation = bundle.heartbeatV2.animation { myLottieView.load(data: animation.data)}Audio authored into a preset is registered as a Core Haptics audio event, so it shares the engine clock with the haptics.
For an id only known at runtime, bundle.get(id) returns a handle or nil. Call bundle.dispose()
to release the native patterns when you are done.
See iOS/PulsarApp for a
working screen.
Settings
Section titled “Settings”Configuration methods available directly on the Pulsar instance.
| Method | Description |
|---|---|
enableHaptics(state: Bool) | Enable or disable all haptic feedback |
enableSound(state: Bool) | Enable or disable audio simulation |
enableCache(state: Bool) | Enable or disable preset caching |
clearCache() | Clear the preset cache |
preloadPresets(presetNames: [String]) | Preload presets by name for faster playback |
stopHaptics() | Stop all currently playing haptics |
shutDownEngine() | Shut down the haptic engine |
isHapticsSupported() -> Bool | Check if the device supports haptics |
isHapticsEnabled: Bool | Returns whether haptic feedback is currently enabled |
hapticCapabilities() -> HapticCapabilities | Returns what the device can render, see HapticCapabilities |
Example
Section titled “Example”let pulsar = Pulsar()
// Preload frequently used presetspulsar.preloadPresets(presetNames: ["Earthquake", "Success"])
// Disable haptics temporarilypulsar.enableHaptics(state: false)
// Check device supportif pulsar.isHapticsSupported() { pulsar.getPresets().success()}PatternData
Section titled “PatternData”Describes a complete haptic pattern with discrete pulses and continuous envelope curves.
class PatternData: NSObject, Codable { let continuousPattern: ContinuousPattern let discretePattern: [DiscretePoint]
init( continuousPattern: ContinuousPattern, discretePattern: [DiscretePoint] )}ContinuousPattern
Section titled “ContinuousPattern”Represents continuous haptic curves for amplitude and frequency.
class ContinuousPattern: NSObject, Codable { let amplitude: [ValuePoint] let frequency: [ValuePoint]
init(amplitude: [ValuePoint], frequency: [ValuePoint])}ValuePoint
Section titled “ValuePoint”A single point in a continuous curve.
class ValuePoint: NSObject, Codable { let time: Double // Milliseconds from pattern start let value: Float // Normalized value (0-1)
init(time: Double, value: Float)}DiscretePoint
Section titled “DiscretePoint”A single discrete haptic event.
class DiscretePoint: NSObject, Codable { let time: Double // Milliseconds from pattern start let amplitude: Float // Intensity (0-1) let frequency: Float // Sharpness (0-1)
init(time: Double, amplitude: Float, frequency: Float)}Use discretePattern for distinct taps and impacts. Use continuousPattern envelopes to shape a sustained haptic over time.
HapticCapabilities
Section titled “HapticCapabilities”What the device’s haptic hardware can render, returned by pulsar.hapticCapabilities().
class HapticCapabilities: NSObject { let hasAmplitudeControl: Bool let hasPrimitiveSupport: Bool let isEnvelopeSupported: Bool let isFrequencyProfileSupported: Bool let minControlPointDurationMillis: Double}Core Haptics exposes a single supportsHaptics capability and the Taptic Engine renders the
full event set uniformly, so hasAmplitudeControl, hasPrimitiveSupport and
isEnvelopeSupported all mirror isHapticsSupported(). isFrequencyProfileSupported is
false (Core Haptics models sharpness rather than exposing a frequency profile) and
minControlPointDurationMillis is 0. The type is shared with the Android SDK, where the
fields carry per-device values.
Preset
Section titled “Preset”The protocol that all preset implementations conform to.
protocol Preset { func play() static func getInstance(haptics: Pulsar) -> Preset static var name: String { get }}