RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-04-11Advanced

Mastering Native Integration with Rork Max: A Complete iOS/Android Production Guide

A complete guide to integrating iOS/Android native features with Rork Max — from device capabilities and performance tuning to production-ready deployment.

Rork Max229native integrationiOS109Android43advanced implementation

Setup and context

Rork Max is often praised for letting you build complex apps without code — but its real power lies in native platform integration. By connecting to platform-specific features like HealthKit, ARKit, CameraX, and NFC, you can ship high-value apps in days that would be impossible with any other no-code tool.

That said, native integration comes with genuine pitfalls:

  • iOS and Android use completely different APIs and mental models for the same features
  • Asking for device permissions at the wrong moment leads to users denying access permanently
  • Skipping memory leak prevention and version compatibility checks causes crashes in production
  • Misunderstanding Android's threading model hides serious bugs that only surface at scale

This guide walks you through real, working native integrations in Rork Max, complete with code examples and testing strategies. Everything you need to build healthcare apps, AR experiences, and payment apps that actually generate revenue.


Rork Max Native Integration Architecture

There are three main ways to access native capabilities in Rork Max.

① Expo Modules API (Recommended) Expo's module extension API lets you call Swift (iOS) and Kotlin (Android) from TypeScript. Rork Max officially supports this layer for native access. It's more type-safe than raw React Native native modules, with a cleaner async callback design — and it's approachable even if you're new to native development.

② Expo SDK Standard Libraries (Quickest) Packages like expo-camera, expo-health, and expo-nfc already cover most native use cases out of the box. Start here first, and only reach for custom modules when the SDK falls short.

③ Raw React Native Bridge (Advanced) The last resort for APIs that don't exist in the SDK or Expo Modules. This sometimes means diving into Objective-C or C++, and the maintenance burden is high. Keep its footprint in production apps as small as possible.

TypeScript ↔ Native Type Mapping

Type mismatches between TypeScript and Swift/Kotlin are one of the most common sources of native integration bugs. The two languages handle null very differently, and numeric precision differences can cause subtle data corruption.

// TypeScript-side type definition for Expo Modules
export interface HealthDataRecord {
  timestamp: number;   // Unix ms — requires conversion from Swift's TimeInterval
  heartRate: number;   // Swift Double maps cleanly to number
  steps: number;       // Int64 → number can lose precision for very large values
  unit: string;        // Convert Swift's HKUnit.description to string
}
// Swift: corresponding native implementation
@objc(HealthKitModule)
class HealthKitModule: Module {
  func definition() -> ModuleDefinition {
    AsyncFunction("getHeartRate") { (startMs: Double, endMs: Double) -> [String: Any] in
      let start = Date(timeIntervalSince1970: startMs / 1000)
      let end   = Date(timeIntervalSince1970: endMs / 1000)
      // ... HealthKit query
      return ["timestamp": end.timeIntervalSince1970 * 1000,
              "heartRate": latestValue,
              "unit": "bpm"]
    }
  }
}

HealthKit Integration (iOS)

Healthcare is one of the highest-monetization categories on the App Store. With a solid HealthKit integration, you can ship fitness trackers, clinical dashboards, and wellness apps with subscription pricing that's hard to justify with standard data sources.

Permission Timing Drives Revenue

HealthKit permission requests must happen at the exact moment a user understands why access is needed. Asking on launch — before they've seen anything useful — results in denials you can never recover from. The recommended pattern is just-in-time permission requests.

import * as Health from 'expo-health';
import { Alert } from 'react-native';
 
export async function requestHealthPermission(): Promise<boolean> {
  const status = await Health.getPermissionStatusAsync([
    Health.HealthDataType.HEART_RATE,
    Health.HealthDataType.STEP_COUNT,
    Health.HealthDataType.SLEEP_ANALYSIS,
  ]);
 
  if (status === Health.PermissionStatus.GRANTED) return true;
 
  return new Promise((resolve) => {
    Alert.alert(
      'Access Your Health Data',
      'We use heart rate, step count, and sleep data to generate your personalized health report.',
      [
        { text: 'Not Now', onPress: () => resolve(false) },
        {
          text: 'Allow Access',
          onPress: async () => {
            const result = await Health.requestPermissionsAsync([
              Health.HealthDataType.HEART_RATE,
              Health.HealthDataType.STEP_COUNT,
            ]);
            resolve(result.granted);
          },
        },
      ]
    );
  });
}

Background Sync and Battery Efficiency

To receive real-time HealthKit updates, you'll need enableBackgroundDelivery. Match the update frequency to your app's actual needs — over-fetching drains the battery and users will notice.

let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
 
healthStore.enableBackgroundDelivery(
  for: heartRateType,
  frequency: .hourly,   // .immediate for wearable apps; .hourly for most others
  withCompletion: { success, error in
    if let error = error {
      print("Background delivery error: \(error.localizedDescription)")
    }
  }
)

Use .immediate only for apps paired with real-time wearables. Daily report apps can use .daily. For most use cases, .hourly strikes the right balance between freshness and battery impact.


CameraX Integration (Android)

CameraX dramatically simplifies Android camera code compared to Camera2, and it integrates cleanly with Rork Max's Expo module layer.

Real-Time OCR with MLKit

Combining CameraX with Google's MLKit for on-device text recognition opens up high-value business use cases: business card scanning, receipt parsing, QR code reading, and more.

@ExpoMethod
fun startTextRecognition(promise: Promise) {
  val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
 
  val imageAnalysis = ImageAnalysis.Builder()
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
 
  imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
    val mediaImage = imageProxy.image ?: run {
      imageProxy.close()
      return@setAnalyzer
    }
 
    val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
 
    recognizer.process(inputImage)
      .addOnSuccessListener { visionText ->
        val result = visionText.textBlocks.joinToString("\n") { it.text }
        promise.resolve(result)
      }
      .addOnFailureListener { e ->
        promise.reject("OCR_ERROR", e.message, e)
      }
      .addOnCompleteListener {
        imageProxy.close()  // Forgetting this freezes the camera preview
      }
  }
}

Failing to call imageProxy.close() is the single most common cause of frozen camera previews. Always close it inside addOnCompleteListener, not just in the success or failure callbacks.

Handling Device Fragmentation

Android devices vary enormously in camera capability. Settings tuned for a Pixel flagship will cause OOM crashes or laggy previews on mid-range hardware.

fun getOptimalCameraConfig(): CameraConfig {
  val availableMemoryMB = getAvailableMemoryMB()
  val cameraCapabilities = CameraInfo.getCameraCharacteristics()
 
  return when {
    availableMemoryMB > 512 && cameraCapabilities.supportsRaw ->
      CameraConfig(resolution = Size(4032, 3024), enableHDR = true)
    availableMemoryMB > 256 ->
      CameraConfig(resolution = Size(1920, 1080), enableHDR = false)
    else ->
      CameraConfig(resolution = Size(1280, 720), enableHDR = false)
  }
}

AR Integration (ARKit / ARCore)

Augmented reality apps are uniquely easy to showcase with demo videos, which helps enormously with App Store and Google Play featuring. Furniture placement simulators and virtual try-on apps consistently show strong purchase conversion, making them attractive for D2C brand partnerships.

Plane Detection and Object Placement

The most fundamental AR interaction — detecting a horizontal surface and placing a 3D object on it — looks like this in Rork Max:

import { ARView, PlaneDetection } from 'expo-ar';
 
export function ARFurniturePlacement() {
  const [planes, setPlanes] = useState<ARPlane[]>([]);
  const [placedObjects, setPlacedObjects] = useState<PlacedObject[]>([]);
 
  const handlePlaneDetected = useCallback((newPlanes: ARPlane[]) => {
    setPlanes(prev => [...prev, ...newPlanes]);
  }, []);
 
  const handleTap = useCallback((event: ARTapEvent) => {
    const hitResult = event.hitResults.find(r => r.type === 'existingPlane');
    if (!hitResult) return;
 
    setPlacedObjects(prev => [
      ...prev,
      {
        id: Date.now().toString(),
        modelUri: selectedFurnitureModel,
        transform: hitResult.worldTransform,
      },
    ]);
  }, [selectedFurnitureModel]);
 
  return (
    <ARView
      planeDetection={PlaneDetection.Horizontal}
      onPlanesDetected={handlePlaneDetected}
      onTap={handleTap}
    >
      {placedObjects.map(obj => (
        <ARModel key={obj.id} uri={obj.modelUri} transform={obj.transform} />
      ))}
    </ARView>
  );
}

Bridging ARKit and ARCore Differences

A shared utility layer that normalizes platform differences keeps your app logic clean and prevents subtle platform-specific bugs from creeping in.

import { Platform } from 'react-native';
 
export const ARUtils = {
  // ARKit reports plane sizes in cm; ARCore uses meters
  normalizeSize: (rawSize: number): number =>
    Platform.OS === 'ios' ? rawSize / 100 : rawSize,
 
  // Tracking quality thresholds differ between frameworks
  isTrackingReliable: (quality: number): boolean =>
    Platform.OS === 'ios' ? quality > 0.7 : quality > 0.5,
 
  // Ambient light estimation uses different scales
  getLightEstimate: (frame: ARFrame): LightEstimate => {
    if (Platform.OS === 'ios') {
      return { intensity: frame.lightEstimate?.ambientIntensity ?? 1000 };
    } else {
      return { intensity: (frame.lightEstimate?.pixelIntensity ?? 1.0) * 1000 };
    }
  },
};

NFC Payments and Tag Reading

NFC creates powerful offline-to-online bridge scenarios: loyalty cards, venue check-ins, ticket validation, and point-of-sale supplements. If your app connects physical retail to a digital experience, NFC is worth the implementation effort.

Understanding iOS vs. Android NFC Limitations

iOS NFC access is heavily restricted — it's effectively read-only for third-party apps. Android supports both reading and writing. Design around this constraint from the beginning, or you'll face painful rewrites later.

import * as NFC from 'expo-nfc';
import { Platform, Alert } from 'react-native';
 
export async function readNFCTag(): Promise<NFCTagData | null> {
  const isSupported = await NFC.isNFCSupportedAsync();
  if (!isSupported) {
    Alert.alert('Not Supported', 'Your device does not support NFC.');
    return null;
  }
 
  try {
    const tag = await NFC.readTagAsync({
      alertMessage: 'Hold your device near the NFC tag',  // iOS only
      timeout: 10000,
    });
 
    const payload = tag.ndefRecords?.[0]?.payload;
    if (!payload) return null;
 
    // UTF-8 Text Records have a 3-byte language header to skip
    const text = new TextDecoder().decode(payload.slice(3));
    return { rawId: tag.id, text };
 
  } catch (error: unknown) {
    const e = error as { code?: string };
    if (e.code === 'USER_CANCELLED') return null;
    if (e.code === 'TIMEOUT') {
      Alert.alert('Timed Out', 'No NFC tag was detected. Please try again.');
      return null;
    }
    throw error;
  }
}

Permission Management Best Practices

Most native features require OS-level permissions. Poor permission design is one of the leading causes of low App Store ratings — users don't mind granting permissions, but they do mind being asked at the wrong time for the wrong reasons.

Progressive Permission Requests

Never request all permissions upfront. The "just-in-time with context" pattern consistently outperforms upfront requests in both grant rates and user satisfaction.

import * as Permissions from 'expo-permissions';
 
type PermissionContext = {
  feature: string;
  reason: string;
  permission: Permissions.PermissionType;
};
 
export async function requestPermissionWithContext(
  ctx: PermissionContext
): Promise<boolean> {
  const { status } = await Permissions.getAsync(ctx.permission);
 
  if (status === 'granted') return true;
 
  if (status === 'denied') {
    // Once denied, the only path is through Settings
    Alert.alert(
      `Enable ${ctx.feature}`,
      `${ctx.reason}\nPlease enable "${ctx.feature}" in your device Settings.`,
      [
        { text: 'Cancel' },
        { text: 'Open Settings', onPress: () => Linking.openSettings() },
      ]
    );
    return false;
  }
 
  // First-time request with explanation
  const { status: newStatus } = await Permissions.askAsync(ctx.permission);
  return newStatus === 'granted';
}

Memory Leak Prevention and Production Stability

The two most common causes of production crashes in React Native + native module apps are forgotten event listener cleanup and holding large binary data in memory longer than necessary.

Always Clean Up in useEffect

export function HealthMonitor() {
  const subscriptionRef = useRef<HealthSubscription | null>(null);
 
  useEffect(() => {
    let isMounted = true;
 
    async function subscribe() {
      subscriptionRef.current = await Health.watchStepCount(data => {
        if (!isMounted) return;  // Prevent setState after unmount
        setSteps(data.steps);
      });
    }
 
    subscribe();
 
    return () => {
      isMounted = false;
      subscriptionRef.current?.remove();  // This is the leak if you forget it
      subscriptionRef.current = null;
    };
  }, []);
}

Release Large Images and Video Promptly

Keeping camera or AR frames in memory beyond the processing window is a reliable path to OOM crashes.

import * as FileSystem from 'expo-file-system';
 
export async function processAndReleaseImage(uri: string): Promise<ProcessedResult> {
  try {
    const result = await analyzeImage(uri);
    return result;
  } finally {
    // finally ensures cleanup even if analysis throws
    const fileInfo = await FileSystem.getInfoAsync(uri);
    if (fileInfo.exists) {
      await FileSystem.deleteAsync(uri, { idempotent: true });
    }
  }
}

Responding to Memory Pressure

import { AppState, Platform } from 'react-native';
 
export function useMemoryPressureHandler(clearCache: () => void) {
  useEffect(() => {
    if (Platform.OS === 'ios') {
      // iOS fires a dedicated memoryWarning event
      const handler = AppState.addEventListener('memoryWarning' as any, clearCache);
      return () => handler.remove();
    } else {
      // Android: clear cache when the app moves to background
      const handler = AppState.addEventListener('change', (state) => {
        if (state === 'background') clearCache();
      });
      return () => handler.remove();
    }
  }, [clearCache]);
}

Platform-Specific Testing Strategy

Unit tests alone are insufficient for native integrations. The bugs that matter most only show up on real devices, under real conditions.

iOS Testing Checklist

Verify that permission dialogs appear at the right moment — not on launch. Then disable each permission in Settings, return to the app, and confirm the fallback UI behaves correctly. Test background-to-foreground transitions for AR and HealthKit sessions: they should pause gracefully and resume without errors. Finally, use Xcode's "Simulate Memory Warning" to confirm your app releases caches without crashing.

Android Testing Checklist

Run your app on devices with multiple API levels — file access permission behavior changed significantly in Android 10 (API 29). Test Doze mode and battery optimization with adb shell dumpsys deviceidle force-idle to confirm background tasks behave as expected. Pay attention to manufacturer-specific camera behavior: Samsung, Pixel, and Xiaomi devices can differ in frame rate, color correction, and HDR handling.

Mocking Native Modules in Jest

// __mocks__/expo-health.ts
const mockHealthData = {
  steps: 8432,
  heartRate: 72,
  timestamp: Date.now(),
};
 
export const getPermissionStatusAsync = jest.fn().mockResolvedValue('granted');
export const requestPermissionsAsync = jest.fn().mockResolvedValue({ granted: true });
export const getStepCountAsync = jest.fn().mockResolvedValue(mockHealthData.steps);
export const getHeartRateAsync = jest.fn().mockResolvedValue(mockHealthData.heartRate);
export const watchStepCount = jest.fn().mockReturnValue({ remove: jest.fn() });
 
// Example test
describe('HealthMonitor', () => {
  it('shows fallback UI when permission is denied', async () => {
    getPermissionStatusAsync.mockResolvedValueOnce('denied');
    const { getByText } = render(<HealthMonitor />);
    await waitFor(() => {
      expect(getByText('Please enable access in Settings')).toBeTruthy();
    });
  });
});

Wrapping Up

Mastering native integration in Rork Max opens the door to genuinely high-value products:

  • Healthcare: HealthKit-powered dashboards for clinicians or patients ($30–$99/month subscriptions)
  • AR/Visual: ARKit/ARCore furniture placement simulators ($9.99 one-time)
  • Payments: NFC-based loyalty card apps for physical retail (3% transaction fee)

Three core skills to take away from this guide:

  1. Type-safe Bridge design — get TypeScript and Swift/Kotlin types to agree upfront
  2. Understand platform differences — HealthKit and Google Fit are not the same thing
  3. Own your production stability — monitor memory, battery, and crash logs like a professional

Next steps:

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-05-01
Implementing Screen Recording and Broadcast in Rork Max — A Production Guide for ReplayKit and MediaProjection
Integrate iOS ReplayKit and Android MediaProjection into your Rork Max app for gameplay recording, tutorial creation, and live broadcasting—covering the full production pipeline.
Dev Tools2026-04-08
Rork Max + EAS Build Custom Native Module Errors: Complete Fix Guide
Comprehensive guide to fixing EAS Build errors when adding custom native modules to Rork Max apps. Covers Config Plugin creation, iOS Podfile conflicts, Android Gradle errors, Hermes compatibility, and native initialization issues.
Dev Tools2026-07-13
Losing HealthKit Data on Incremental Sync — Designing HKQueryAnchor Persistence
When step or sleep data double-counts or goes missing on incremental HealthKit sync, the root cause is usually HKQueryAnchor persistence. Here is a working Swift design that handles newAnchor and deletedObjects correctly and stays consistent across reinstalls and background updates.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →