From react-native-plugin
iOS / Android platform-specific code in React Native: Platform.OS, Platform.select, .ios.tsx / .android.tsx file extensions, native modules, permissions, safe area handling, status bar. Use this skill to: - Branch code at runtime via Platform.OS / Platform.select. - Use file extensions for whole-component swaps. - Link and use native modules (Expo SDK or autolinked bare). - Handle permissions across platforms. - Configure status bar and safe area correctly. Do NOT use this skill for: - General project structure (see rn-conventions). - Navigation (see rn-navigation). - Storage (see rn-state-and-storage). - Testing (see rn-testing).
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-plugin:rn-platform-specificThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
iOS and Android have real differences. RN abstracts most, but sometimes you need to branch.
iOS and Android have real differences. RN abstracts most, but sometimes you need to branch.
Platform.OSimport { Platform } from 'react-native';
console.log(Platform.OS); // 'ios' | 'android' | 'web' | 'windows' | 'macos'
if (Platform.OS === 'ios') {
// iOS-only logic
}
Platform.OS is set at runtime by RN. Use for small branches:
const elevation = Platform.OS === 'android' ? { elevation: 4 } : { shadowOpacity: 0.1 };
Don't fork entire components for 5 lines of difference — use Platform.select or inline conditionals.
Platform.selectDeclarative platform branching:
const styles = StyleSheet.create({
card: {
padding: 16,
backgroundColor: '#fff',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
default: {}, // web, windows, macos
}),
},
});
Each key returns a value; Platform.select picks the matching one.
Platform.VersionNumeric on Android (API level: 28, 30, 33, 34), string on iOS (e.g. '17.0'):
if (Platform.OS === 'android' && Platform.Version >= 33) {
// Android 13+ specific behavior
}
if (Platform.OS === 'ios' && parseInt(Platform.Version as string) >= 17) {
// iOS 17+ specific
}
For whole-component swaps, Metro bundler picks files based on extension:
src/
├── components/
│ ├── DatePicker.tsx # default for all platforms
│ ├── DatePicker.ios.tsx # iOS-only override
│ ├── DatePicker.android.tsx # Android-only override
│ ├── DatePicker.native.tsx # native (iOS + Android, NOT web)
│ └── DatePicker.web.tsx # web (RN Web)
// import is the same — Metro resolves to the right file
import { DatePicker } from './components/DatePicker';
Use file extensions when:
Platform.select or inline conditionals — easier to read in one file.Pre-installed, no native linking needed. Just install the JS package:
pnpm add expo-camera expo-location expo-notifications
import * as Camera from 'expo-camera';
const [permission, requestPermission] = Camera.useCameraPermissions();
For custom native code in managed workflow, you need to migrate to dev-client or eject to bare.
RN 0.60+ has autolinking — installing a package via npm/yarn/pnpm wires it up automatically:
pnpm add react-native-camera
cd ios && pod install # iOS only — install CocoaPods deps
After install:
npm run ios / npm run android to rebuild with new native module.npm start --reset-cache if cached).Some packages need additional native config (manifest entries, Info.plist keys). Check package README.
Bare: write Objective-C/Swift (iOS) and Java/Kotlin (Android) modules. Beyond the scope of this skill.
Expo: write a config plugin that injects native code via the prebuild step. See expo-build-properties and withDangerousMod examples.
Each feature-specific package handles its own permission flow:
import * as Camera from 'expo-camera';
const [permission, requestPermission] = Camera.useCameraPermissions();
if (!permission?.granted) {
await requestPermission();
}
import * as Location from 'expo-location';
const { status } = await Location.requestForegroundPermissionsAsync();
Declare permission descriptions in app.json:
{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to access your camera",
"NSLocationWhenInUseUsageDescription": "Allow location access for map features"
}
},
"android": {
"permissions": ["CAMERA", "ACCESS_FINE_LOCATION"]
}
}
}
Use react-native-permissions for unified API:
import { check, request, RESULTS, PERMISSIONS } from 'react-native-permissions';
const status = await check(PERMISSIONS.IOS.CAMERA);
if (status !== RESULTS.GRANTED) {
await request(PERMISSIONS.IOS.CAMERA);
}
Configure Info.plist (iOS) and AndroidManifest.xml (Android) with the right keys / permissions.
The notch (iPhone X+) and rounded corners (iPad/iPhone) eat into screen real estate. iOS has the home indicator; Android may have on-screen nav bar or gesture area.
react-native-safe-area-context (preferred)// Wrap app once at root
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<RootNavigator />
</SafeAreaProvider>
);
}
// Use SafeAreaView in screens
import { SafeAreaView } from 'react-native-safe-area-context';
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
{/* screen content */}
</SafeAreaView>
// Or insets for fine control
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const insets = useSafeAreaInsets();
<View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}>...</View>
The RN-built-in SafeAreaView from react-native is deprecated — don't use it.
edges prop: which edges to apply safe area padding. Often ['top'] for screens with bottom tab nav (tab nav handles bottom safe area).
import { KeyboardAvoidingView, Platform } from 'react-native';
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
keyboardVerticalOffset={64} // header height
>
{/* form */}
</KeyboardAvoidingView>
For more control, use react-native-keyboard-controller:
import { KeyboardProvider } from 'react-native-keyboard-controller';
// Wrap app, provides better keyboard event APIs and animations.
import { StatusBar } from 'expo-status-bar';
<StatusBar style="auto" />
style: 'auto' (matches color scheme), 'light', 'dark', 'inverted'.
import { StatusBar } from 'react-native';
<StatusBar barStyle="dark-content" backgroundColor="#fff" translucent={false} />
backgroundColor is Android-only; iOS uses the underlying view's color.
import { Dimensions, useWindowDimensions, PixelRatio } from 'react-native';
// One-time read (doesn't update on rotation)
const { width, height } = Dimensions.get('window');
// Reactive — updates on rotation/resize
const { width, height } = useWindowDimensions();
// For native conversions (rare)
const px = PixelRatio.getPixelSizeForLayoutSize(50);
Use useWindowDimensions for layouts that adapt to orientation.
| Issue | Platforms | Fix |
|---|---|---|
| Shadows differ | iOS uses shadowColor/Offset/Opacity; Android uses elevation | Platform.select |
| Status bar overlaps content | iOS by default doesn't; Android translucent does | Set translucent={false} or wrap in SafeAreaView |
| Back button | Android has hardware back; iOS doesn't | BackHandler (Android) for custom logic; React Navigation handles automatically |
| Date/time picker | iOS shows wheel; Android shows native dialog | @react-native-community/datetimepicker handles both |
| Keyboard appearance | iOS animates over content; Android may resize layout | KeyboardAvoidingView with behavior per platform |
| Linking external apps | iOS has stricter URL scheme rules | Linking.canOpenURL before openURL; declare allowed schemes in LSApplicationQueriesSchemes (iOS) |
| Push notifications | Different APNs (iOS) vs FCM (Android) tokens | Use expo-notifications or react-native-firebase for unified API |
| Notch / Dynamic Island (iOS) | Only iOS | SafeAreaView handles automatically |
Platform.OS === 'ios' when 90% of code is shared — use inline conditionals or .ios.tsx/.android.tsx only for genuinely different markup.NativeModules.X directly without Platform check — module may not exist on the other platform = crash.position: 'fixed').app.json / Info.plist — App Store / Play Store rejection.BackHandler.addEventListener('hardwareBackPress', ...).SafeAreaView from react-native with react-native-safe-area-context.npx claudepluginhub aratkruglik/claude-sdlc --plugin react-native-pluginHandles platform-specific code in React Native for iOS and Android using Platform API, platform files, components, native modules, and cross-platform best practices.
Builds, optimizes, and debugs cross-platform mobile apps with React Native and Expo. Covers navigation, native modules, FlatList performance, and platform-specific code.
Builds, optimizes, and debugs cross-platform mobile apps with React Native and Expo. Handles navigation, native modules, FlatList performance, and platform-specific code.