
A type-safe, FFI-based Flutter plugin that gives you full access to Apple's Core Haptics framework. Create custom vibration patterns, play AHAP files, and deliver tactile experiences that feel native.
โจ What's inside
- ๐ฏ Complete Core Haptics wrapper โ engines, patterns, players, and dynamic parameters
- โก One-liner haptics โ
HapticEngine.success(),HapticEngine.mediumImpact()with zero setup - ๐ AHAP everywhere โ load from JSON strings, files, or Flutter assets
- ๐จ Programmatic patterns โ build haptic sequences with
HapticEvent(no JSON needed!) - ๐ก๏ธ Memory-safe FFI โ automatic cleanup with finalizers, strongly-typed enums
- ๐๏ธ Live parameter control โ adjust intensity and sharpness during playback
- ๐ Interruption handling โ callbacks for audio session changes and resets
- ๐ซ Zero CocoaPods โ Swift Package Manager only, clean and modern
๐ฆ Installation
Add to your pubspec.yaml:
dependencies:
core_haptics: ^latest_version
Then run:
flutter pub get
๐ง Platform Setup
iOS / macOS (Swift Package Manager)
Since this plugin uses SwiftPM instead of CocoaPods, you need to manually link the native module:
Step 1: Open your app in Xcode
example/ios/Runner.xcworkspace (or macos/Runner.xcworkspace)
Step 2: Add the local Swift Package
- File โ Add Package Dependencies
- Click "Add Local..." and navigate to the plugin's
ios/folder - Select
Package.swiftand add it
Step 3: Link to your target
- In your app target's Frameworks and Libraries, add
CoreHapticsFFI - Set to Embed & Sign (iOS) or Do Not Embed (macOS)
Requirements:
- iOS 13.0+ or macOS 11.0+
- Physical device with haptic engine (iPhone 8+ or supported Mac)
Note
SwiftPM gives cleaner builds, better dependency management, and is Apple's recommended approach for modern Swift libraries.
๐ Quick Start
The easiest way to use the plugin is to use the static methods. Instead if you want to use the full API, you can create an engine instance.
One-liner haptics
All static methods automatically check device support and silently do nothing on unsupported devices โ no need to wrap calls in isSupported checks!
import 'package:core_haptics/core_haptics.dart';
// Impact feedback
await HapticEngine.lightImpact();
await HapticEngine.mediumImpact();
await HapticEngine.heavyImpact();
// Notification feedback (not available in Flutter's HapticFeedback!)
await HapticEngine.success();
await HapticEngine.warning();
await HapticEngine.error();
// Selection feedback
await HapticEngine.selection();
Use isSupported when you need to make UI decisions based on haptics availability:
// Show/hide haptics settings based on device capability
final showHapticsToggle = await HapticEngine.isSupported;
For custom patterns with precise timing:
await HapticEngine.play([
HapticEvent(type: HapticEventType.transient, intensity: 0.8, sharpness: 0.5),
HapticEvent(
type: HapticEventType.continuous,
time: Duration(milliseconds: 100),
duration: Duration(seconds: 1),
intensity: 0.5,
),
]);
Advanced usage
For full control over engines, patterns, players, looping, and dynamic parameters, use the HapticEngine API directly.
Basic haptic tap
import 'package:core_haptics/core_haptics.dart';
Future<void> playSimpleTap() async {
// Create and start the engine
final engine = await HapticEngine.create();
await engine.start();
// Load an AHAP pattern (JSON string)
final pattern = await engine.loadPatternFromAhap('''
{
"Version": 1,
"Pattern": [{
"Event": {
"EventType": "HapticTransient",
"Time": 0,
"EventParameters": [
{"ParameterID": "HapticIntensity", "ParameterValue": 0.8},
{"ParameterID": "HapticSharpness", "ParameterValue": 0.5}
]
}
}]
}
''');
// Play it
final player = await engine.createPlayer(pattern);
await player.play();
// Cleanup
await player.dispose();
await pattern.dispose();
await engine.dispose();
}
Programmatic patterns
final pattern = await engine.loadPatternFromEvents([
// Sharp tap at start
const HapticEvent(
type: HapticEventType.transient,
time: Duration.zero,
intensity: 1.0,
sharpness: 0.8,
),
// Continuous rumble after 300ms
const HapticEvent(
type: HapticEventType.continuous,
time: Duration(milliseconds: 300),
duration: Duration(seconds: 2),
intensity: 0.6,
sharpness: 0.3,
),
]);
Load from Flutter assets
// 1. Add AHAP file to pubspec.yaml assets
// 2. Load it:
final pattern = await engine.loadPatternFromAsset('assets/haptics/my_pattern.ahap');
Handle interruptions
final engine = await HapticEngine.create(
onEvent: (event, message) {
switch (event) {
HapticEngineEvent.interrupted => print('โ ๏ธ Haptics interrupted: $message');
HapticEngineEvent.restarted => print('โ
Haptics resumed');
}
},
);
๐ฏ API Reference
HapticEngine
Your main entry point. Provides both static one-liner methods and full engine control.
Static methods (uses native UIFeedbackGenerator, auto-checks device support):
// Impact feedback (silently no-ops on unsupported devices)
await HapticEngine.lightImpact();
await HapticEngine.mediumImpact();
await HapticEngine.heavyImpact();
await HapticEngine.softImpact();
await HapticEngine.rigidImpact();
// Notification feedback
await HapticEngine.success();
await HapticEngine.warning();
await HapticEngine.error();
// Selection feedback
await HapticEngine.selection();
// Custom patterns
await HapticEngine.play(eventList);
// Check device support (for UI decisions)
if (await HapticEngine.isSupported) { ... }
Instance API (for full control):
// Create
final engine = await HapticEngine.create(onEvent: callback);
// Start/stop
await engine.start();
await engine.stop();
// Load patterns
final p1 = await engine.loadPatternFromAhap(jsonString);
final p2 = await engine.loadPatternFromFile(path);
final p3 = await engine.loadPatternFromAsset('assets/pattern.ahap');
final p4 = await engine.loadPatternFromEvents(eventList);
// Create players
final player = await engine.createPlayer(pattern);
// Cleanup
await engine.dispose();
HapticPlayer
Controls playback of a haptic pattern.
await player.play(atTime: 0);
await player.stop(atTime: 0);
// Looping (not available on all platforms)
await player.setLoop(enabled: true, loopStart: 0, loopEnd: 2.0);
// Dynamic parameter updates (during playback)
await player.setParameter(HapticParameterId.hapticIntensity, 0.9, atTime: 0);
await player.dispose();
HapticEvent
Programmatically define haptic events.
const HapticEvent({
required HapticEventType type, // transient or continuous
Duration time = Duration.zero, // when to fire (relative to pattern start)
Duration? duration, // required for continuous events
double? intensity, // 0.0 to 1.0
double? sharpness, // 0.0 to 1.0
});
Error Handling
All errors throw HapticsException:
try {
await engine.start();
} on HapticsException catch (e) {
print('Error: ${e.code} - ${e.message}');
// e.code is a HapticsErrorCode enum
}
Error codes:
| Code | Description |
|---|---|
notSupported |
Device doesn't support haptics |
engine |
Engine failed to start |
invalidArgument |
Bad pattern or parameter |
decode |
Invalid AHAP JSON |
io |
File not found |
runtime |
Playback issue |
๐งช Testing
Dart unit tests:
flutter test
Uses mocked FFI bridge for ~90% API coverage.
Swift native tests:
cd ios && swift test
Tests the Core Haptics bridge (skips on devices without haptics).
๐ Troubleshooting
"Cannot find symbol 'chffi_engine_create'"
The SwiftPM package isn't linked. Go back to Platform Setup and ensure CoreHapticsFFI is added to your app target's frameworks.
"Device does not support haptics"
Core Haptics requires an iPhone 8+ or newer Mac with Taptic Engine. Simulators don't support haptics.
"HapticsException: runtime (-4820)"
Tried to send a parameter update to a player that isn't actively playing. Ensure the pattern is playing before calling setParameter.
๐ Learn More
๐ License
See LICENSE for details.
๐ Contributing
Issues and PRs welcome! This plugin maintains a 1:1 mapping with Core Haptics APIs, so contributions should align with Apple's framework design.