core_haptics 0.0.4
core_haptics: ^0.0.4 copied to clipboard
Type-safe Core Haptics FFI plugin (iOS & macOS) via SwiftPM.

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.