react-native-tickle

repository·main·Indexed 19 days ago

https://github.com/renegades-studio/react-native-tickle

A React Native library for iOS that provides AHAP-style haptics using Core Haptics. Built on Nitro Modules and designed to be UI-thread friendly (worklet-ready), it supports transient and continuous haptic patterns, real-time continuous players for gesture-based input, and standard system haptics including impact, notification, and selection feedback.

Tokens
6.3K
Snippets
22
Records
27
Agent score
65%

What's inside @renegades/react-native-tickle

  1. How haptic patterns work: Transient, Continuous, and Continuous Players

    main

    The library provides three distinct ways to trigger haptics depending on your use case:

    1. Transient: Instant "click/tap" events. These have no duration and are triggered at a specific point in time.
    2. Continuous (pattern): Time-based patterns defined ahead of time. You provide events (which include a duration) and optionally curves (which provide automation/modulation over time).
    3. Continuous player (real-time): Used for unpredictable input like gesture positions or scroll velocity. You create a player once, then use a start → update (many times) → stop lifecycle.

    Note on Events vs. Curves:

    • Events define the structure (what happens and when).
    • Curves define the modulation (how parameters like intensity or sharpness evolve over time).
  2. Understand the limitation of parameter curves on haptic events

    main

    In this library (via CoreHaptics), CHHapticParameterCurve values act as pattern-level multipliers rather than per-event modifiers. If you define an intensity or sharpness curve, it will multiply the intensity/sharpness of all events playing at that specific moment, including transient events.

    The Problem: If a transient event has a base intensity of 1.0 but an active intensity curve has a value of 0.3 at that same timestamp, the resulting effective intensity will be 0.3 (1.0 × 0.3). This makes transients feel weaker than intended if they occur during a continuous event with active curves.

  3. Workaround for transients being affected by continuous event curves

    main

    To prevent curves from affecting transient events, play the continuous and transient events in separate startHaptic() calls. This creates isolated patterns/players so that curves from one call do not multiply the intensity of events in the other.

    Note: The library automatically resets control values to 1.0 at the end of each continuous event. Therefore, transients occurring after a continuous event has finished are not affected by its curves. The limitation only applies to transients occurring during a continuous event with active curves.

    // Continuous with curves
    startHaptic(continuousEvents, curves);
    
    // Transients without curves (separate pattern)
    startHaptic(transientEvents, []);
  4. Initialize haptics with HapticProvider

    main

    The recommended way to use the library is to wrap your application in the HapticProvider. This component initializes the haptic engine and automatically destroys it when the app moves to the background.

    import { HapticProvider } from '@renegades/react-native-tickle';
    
    export function App() {
      return <HapticProvider>{/* {Rest of your app} */}</HapticProvider>;
    }
  5. Reload the application

    main

    If you need to perform a full reload to reset the app state, use the following platform-specific methods:

    • Android: Press the <kbd>R</kbd> key twice, or open the Dev Menu via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS) and select "Reload".
    • iOS: Press <kbd>R</kbd> in the iOS Simulator.
  6. Build and run the iOS app

    main

    For iOS, you must first install CocoaPods dependencies. If this is your first time or you have updated native dependencies, run bundle exec pod install. Then, use the following command to build and launch the application on an iOS simulator or connected device.

    # Install CocoaPods dependencies
    bundle exec pod install
    
    # Using npm
    npm run ios
    
    # OR using Yarn
    yarn ios
  7. Configure Metro for Monorepo Support

    main

    When using this project within a monorepo, the metro.config.js must be configured to watch the monorepo root and resolve node_modules from both the project and the monorepo levels.

    Key configuration steps include:

    1. Setting watchFolders to include the monorepoRoot.
    2. Configuring resolver.nodeModulesPaths to prioritize the local projectRoot/node_modules before checking the monorepoRoot/node_modules.
    3. Enabling transformer.unstable_allowRequireContext to ensure imports from the monorepo are correctly transformed.
    4. Setting resolver.disableHierarchicalLookup to true to force specific module resolution.
    5. Disabling resolver.unstable_enablePackageExports to avoid compatibility issues with React Native 0.79+.
    const { getDefaultConfig } = require('expo/metro-config');
    const path = require('path');
    
    const projectRoot = __dirname;
    const monorepoRoot = path.resolve(projectRoot, '..');
    
    const config = getDefaultConfig(projectRoot);
    
    // Watch all files in the monorepo
    config.watchFolders = [monorepoRoot];
    
    // Resolve with project modules first, then monorepo modules
    config.resolver.nodeModulesPaths = [
      path.resolve(projectRoot, 'node_modules'),
      path.resolve(monorepoRoot, 'node_modules'),
    ];
    
    // Force resolving nested modules
    config.resolver.disableHierarchicalLookup = true;
    
    // Disable package exports for RN 0.79+ compatibility
    config.resolver.unstable_enablePackageExports = false;
    
    // Allow transformation for monorepo imports
    config.transformer.unstable_allowRequireContext = true;
    
    module.exports = config;
  8. Stop all running haptics

    main

    To prevent haptics from playing when a user navigates away or a component unmounts, call stopAllHaptics(). This is especially useful in navigation listeners (like beforeRemove in Expo Router) or useEffect cleanup functions.

    import { stopAllHaptics } from '@renegades/react-native-tickle';
    import { useEffect } from 'react';
    
    export function SomeScreen() {
      // Stop haptics when screen unmounts
      useEffect(() => () => stopAllHaptics(), []);
      return null;
    }
  9. Use real-time continuous haptics with useContinuousPlayer

    main

    For haptics that respond to real-time data (like gestures), use the useContinuousPlayer hook. This provides start, stop, and update methods to control a player instance by ID.

    import { useContinuousPlayer } from '@renegades/react-native-tickle';
    
    function MyComponent() {
      // playerId, initialIntensity, initialSharpness
      const { start, stop, update } = useContinuousPlayer('my-player', 1.0, 0.5);
    
      const gesture = Gesture.Pan()
        .onBegin(() => {
          start();
        })
        .onUpdate((e) => {
          // Update intensity/sharpness based on gesture
          update(e.translationY / 100, 0.5);
        })
        .onEnd(() => {
          stop();
        });
    }