Accessing iOS-specific APIs not available in React Native
Scope: Native modules, bridging Swift/Objective-C, iOS APIs, TurboModules, native UI components Lines: ~350 Last Updated: 2025-10-18
Activate this skill when:
Bridge Pattern:
TurboModules (New Architecture):
Communication Flow:
JavaScript → Bridge → Native Module → iOS API
iOS API → Native Module → Bridge → JavaScript
Bridging Header (Objective-C interop):
@objc Attribute:
Push vs Pull:
Use Cases:
// ios/MyModule.swift
import Foundation
import React
@objc(MyModule)
class MyModule: NSObject {
// MARK: - React Native Export
@objc
static func requiresMainQueueSetup() -> Bool {
return false // Run on background thread
}
@objc
func greet(_ name: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
let greeting = "Hello, \(name)!"
resolver(greeting)
}
@objc
func calculateSum(_ a: NSNumber, b: NSNumber, callback: @escaping RCTResponseSenderBlock) {
let sum = a.doubleValue + b.doubleValue
callback([NSNull(), sum]) // [error, result] pattern
}
@objc
func getDeviceInfo() -> NSDictionary {
return [
"model": UIDevice.current.model,
"systemVersion": UIDevice.current.systemVersion,
"name": UIDevice.current.name
]
}
}
// ios/MyModule.m (Bridging file)
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(MyModule, NSObject)
RCT_EXTERN_METHOD(greet:(NSString *)name
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(calculateSum:(nonnull NSNumber *)a
b:(nonnull NSNumber *)b
callback:(RCTResponseSenderBlock)callback)
RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(getDeviceInfo)
@end
// src/modules/MyModule.ts
import { NativeModules } from 'react-native';
interface MyModuleInterface {
greet(name: string): Promise<string>;
calculateSum(a: number, b: number, callback: (error: any, result: number) => void): void;
getDeviceInfo(): { model: string; systemVersion: string; name: string };
}
const { MyModule } = NativeModules;
export default MyModule as MyModuleInterface;
Benefits:
// ios/SensorModule.swift
import Foundation
import CoreMotion
import React
@objc(SensorModule)
class SensorModule: RCTEventEmitter {
private var motionManager: CMMotionManager?
private var isObserving = false
override init() {
super.init()
motionManager = CMMotionManager()
}
// MARK: - React Native Export
override static func requiresMainQueueSetup() -> Bool {
return false
}
override func supportedEvents() -> [String]! {
return ["onAccelerometerData", "onDeviceMotion"]
}
@objc
func startAccelerometer() {
guard let manager = motionManager, !isObserving else { return }
isObserving = true
manager.accelerometerUpdateInterval = 0.1
manager.startAccelerometerUpdates(to: .main) { [weak self] (data, error) in
guard let data = data, error == nil else { return }
self?.sendEvent(withName: "onAccelerometerData", body: [
"x": data.acceleration.x,
"y": data.acceleration.y,
"z": data.acceleration.z,
"timestamp": Date().timeIntervalSince1970
])
}
}
@objc
func stopAccelerometer() {
motionManager?.stopAccelerometerUpdates()
isObserving = false
}
override func stopObserving() {
stopAccelerometer()
}
}
// ios/SensorModule.m
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface RCT_EXTERN_MODULE(SensorModule, RCTEventEmitter)
RCT_EXTERN_METHOD(startAccelerometer)
RCT_EXTERN_METHOD(stopAccelerometer)
@end
// src/modules/SensorModule.ts
import { NativeEventEmitter, NativeModules } from 'react-native';
const { SensorModule } = NativeModules;
const sensorEmitter = new NativeEventEmitter(SensorModule);
export interface AccelerometerData {
x: number;
y: number;
z: number;
timestamp: number;
}
export const startAccelerometer = () => {
SensorModule.startAccelerometer();
};
export const stopAccelerometer = () => {
SensorModule.stopAccelerometer();
};
export const subscribeToAccelerometer = (
callback: (data: AccelerometerData) => void
) => {
const subscription = sensorEmitter.addListener('onAccelerometerData', callback);
return () => subscription.remove();
};
When to use:
// ios/CircularProgressView.swift
import UIKit
class CircularProgressView: UIView {
private var progressLayer = CAShapeLayer()
private var trackLayer = CAShapeLayer()
@objc var progress: CGFloat = 0 {
didSet {
updateProgress()
}
}
@objc var progressColor: UIColor = .systemBlue {
didSet {
progressLayer.strokeColor = progressColor.cgColor
}
}
@objc var trackColor: UIColor = .systemGray5 {
didSet {
trackLayer.strokeColor = trackColor.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
private func setupLayers() {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - 10
let circularPath = UIBezierPath(
arcCenter: center,
radius: radius,
startAngle: -.pi / 2,
endAngle: 3 * .pi / 2,
clockwise: true
)
// Track layer
trackLayer.path = circularPath.cgPath
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.strokeColor = trackColor.cgColor
trackLayer.lineWidth = 10
trackLayer.lineCap = .round
// Progress layer
progressLayer.path = circularPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = progressColor.cgColor
progressLayer.lineWidth = 10
progressLayer.lineCap = .round
progressLayer.strokeEnd = 0
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
}
private func updateProgress() {
CATransaction.begin()
CATransaction.setAnimationDuration(0.3)
progressLayer.strokeEnd = progress
CATransaction.commit()
}
}
// ios/CircularProgressViewManager.swift
import React
@objc(CircularProgressViewManager)
class CircularProgressViewManager: RCTViewManager {
override static func requiresMainQueueSetup() -> Bool {
return true // UI components need main thread
}
override func view() -> UIView! {
return CircularProgressView()
}
}
// ios/CircularProgressViewManager.m
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(CircularProgressViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(progressColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(trackColor, UIColor)
@end
// src/components/CircularProgress.tsx
import React from 'react';
import { requireNativeComponent, ViewProps } from 'react-native';
interface CircularProgressProps extends ViewProps {
progress: number; // 0.0 - 1.0
progressColor?: string;
trackColor?: string;
}
const NativeCircularProgress = requireNativeComponent<CircularProgressProps>(
'CircularProgressView'
);
export const CircularProgress: React.FC<CircularProgressProps> = (props) => {
return <NativeCircularProgress {...props} />;
};
Benefits:
// ios/NetworkModule.swift
import Foundation
import React
@objc(NetworkModule)
class NetworkModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
@objc
func fetchUser(_ userId: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
guard let url = URL(string: "https://api.example.com/users/\(userId)") else {
rejecter("INVALID_URL", "Invalid user ID", nil)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
// Handle network error
if let error = error {
rejecter("NETWORK_ERROR", error.localizedDescription, error)
return
}
// Handle HTTP error
guard let httpResponse = response as? HTTPURLResponse else {
rejecter("INVALID_RESPONSE", "Invalid response type", nil)
return
}
guard (200...299).contains(httpResponse.statusCode) else {
rejecter("HTTP_ERROR", "Status code: \(httpResponse.statusCode)", nil)
return
}
// Parse JSON
guard let data = data else {
rejecter("NO_DATA", "No data received", nil)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
resolver(json)
} catch {
rejecter("PARSE_ERROR", "Failed to parse JSON", error)
}
}.resume()
}
}
// src/modules/NetworkModule.ts
import { NativeModules } from 'react-native';
interface User {
id: string;
name: string;
email: string;
}
class NetworkModuleWrapper {
async fetchUser(userId: string): Promise<User> {
try {
const user = await NativeModules.NetworkModule.fetchUser(userId);
return user as User;
} catch (error: any) {
// Error codes from native module
switch (error.code) {
case 'INVALID_URL':
throw new Error('Invalid user ID format');
case 'NETWORK_ERROR':
throw new Error('Network connection failed');
case 'HTTP_ERROR':
throw new Error(`Server error: ${error.message}`);
case 'PARSE_ERROR':
throw new Error('Invalid response format');
default:
throw error;
}
}
}
}
export default new NetworkModuleWrapper();
When to use:
// ios/ModernAsyncModule.swift
import Foundation
import React
@objc(ModernAsyncModule)
class ModernAsyncModule: NSObject {
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
@objc
func performAsyncWork(_ input: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
Task {
do {
let result = try await processData(input)
resolver(result)
} catch {
rejecter("ASYNC_ERROR", error.localizedDescription, error)
}
}
}
private func processData(_ input: String) async throws -> [String: Any] {
// Simulate async work
try await Task.sleep(nanoseconds: 1_000_000_000)
// Use structured concurrency
async let part1 = fetchPart1(input)
async let part2 = fetchPart2(input)
let results = try await [part1, part2]
return [
"input": input,
"results": results,
"timestamp": Date().timeIntervalSince1970
]
}
private func fetchPart1(_ input: String) async throws -> String {
try await Task.sleep(nanoseconds: 500_000_000)
return "Part1: \(input)"
}
private func fetchPart2(_ input: String) async throws -> String {
try await Task.sleep(nanoseconds: 500_000_000)
return "Part2: \(input)"
}
}
Benefits:
// ios/LegacyModule.h
#import <React/RCTBridgeModule.h>
@interface LegacyModule : NSObject <RCTBridgeModule>
@end
// ios/LegacyModule.m
#import "LegacyModule.h"
@implementation LegacyModule
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(addNumbers:(NSInteger)a
b:(NSInteger)b
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSInteger sum = a + b;
resolve(@(sum));
}
RCT_EXPORT_METHOD(performCallback:(RCTResponseSenderBlock)callback)
{
callback(@[[NSNull null], @"Success"]);
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
@end
When to use:
JavaScript Type | Objective-C/Swift Type | Notes
-------------------|------------------------------|------------------
number | NSNumber / Double / Int | Auto-converted
string | NSString / String | UTF-8 encoded
boolean | NSNumber / Bool | true/false
null/undefined | NSNull / nil | Check for NSNull
Array | NSArray / [Any] | Ordered collection
Object | NSDictionary / [String: Any] | Key-value pairs
Function | RCTResponseSenderBlock | Callback
Promise | RCTPromise(Resolve/Reject) | Async result
RCT_EXPORT_MODULE() // Export module with class name
RCT_EXPORT_MODULE(CustomName) // Export with custom name
RCT_EXPORT_METHOD(methodName:...) // Export async method
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD() // Export synchronous method
RCT_EXPORT_VIEW_PROPERTY(name, type) // Export view property
✅ DO: Use promises for async operations
✅ DO: Validate input parameters
✅ DO: Return descriptive error codes
✅ DO: Run on background thread when possible
✅ DO: Use type-safe TypeScript interfaces
❌ DON'T: Block the main thread with heavy operations
❌ DON'T: Return functions or callbacks from native
❌ DON'T: Store references to JavaScript objects
❌ DON'T: Use synchronous methods for slow operations
❌ DON'T: Forget to handle errors properly
❌ Synchronous heavy operations: Blocking main thread with slow native code ✅ Use promises and background threads: requiresMainQueueSetup() -> false
❌ Over-bridging: Calling native for simple logic better done in JS ✅ Use native only for platform APIs, performance-critical code, or third-party SDKs
❌ No error handling: Native errors crash the app ✅ Always use try-catch and reject promises with descriptive error codes
❌ Storing JavaScript callbacks: Memory leaks and crashes ✅ Use event emitters for continuous updates, promises for one-time results
❌ Large data serialization: Sending MB of data across bridge ✅ Use file paths or shared memory, send only metadata
❌ Main thread for async work: UI freezes during operations ✅ Use background threads for network, file I/O, computations
❌ Missing TypeScript types: Untyped native module usage ✅ Create interfaces for all native modules with full type safety
❌ Not cleaning up resources: Memory leaks from sensors, timers, etc. ✅ Implement stopObserving and cleanup in dealloc/deinit
react-native-setup.md - Project configuration for native modulesreact-native-performance.md - Optimizing bridge communicationswift-concurrency.md - Modern Swift async patternsios-networking.md - URLSession and network best practicesswiftui-architecture.md - iOS app structure and patternsios-testing.md - Testing native modulesLast Updated: 2025-10-18 Format Version: 1.0 (Atomic)