| name | rn-platform-specific |
| description | 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).
|
Platform-Specific Patterns
iOS and Android have real differences. RN abstracts most, but sometimes you need to branch.
Platform.OS
import { Platform } from 'react-native';
console.log(Platform.OS);
if (Platform.OS === 'ios') {
}
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.select
Declarative 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: {},
}),
},
});
Each key returns a value; Platform.select picks the matching one.
Platform.Version
Numeric on Android (API level: 28, 30, 33, 34), string on iOS (e.g. '17.0'):
if (Platform.OS === 'android' && Platform.Version >= 33) {
}
if (Platform.OS === 'ios' && parseInt(Platform.Version as string) >= 17) {
}
File extensions
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 { DatePicker } from './components/DatePicker';
Use file extensions when:
- The two platforms need genuinely different markup.
- Native modules differ per platform.
- Otherwise, prefer
Platform.select or inline conditionals — easier to read in one file.
Native modules
Expo SDK (managed)
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.
Bare RN (autolinking)
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
After install:
- Run
npm run ios / npm run android to rebuild with new native module.
- Restart Metro (
npm start --reset-cache if cached).
Some packages need additional native config (manifest entries, Info.plist keys). Check package README.
Custom native modules
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.
Permissions
Expo
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"]
}
}
}
Bare
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.
Safe area
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)
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<RootNavigator />
</SafeAreaProvider>
);
}
import { SafeAreaView } from 'react-native-safe-area-context';
<SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
{/* screen content */}
</SafeAreaView>
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).
Keyboard handling
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';
Status bar
Expo
import { StatusBar } from 'expo-status-bar';
<StatusBar style="auto" />
style: 'auto' (matches color scheme), 'light', 'dark', 'inverted'.
Bare
import { StatusBar } from 'react-native';
<StatusBar barStyle="dark-content" backgroundColor="#fff" translucent={false} />
backgroundColor is Android-only; iOS uses the underlying view's color.
Pixel ratio and dimensions
import { Dimensions, useWindowDimensions, PixelRatio } from 'react-native';
const { width, height } = Dimensions.get('window');
const { width, height } = useWindowDimensions();
const px = PixelRatio.getPixelSizeForLayoutSize(50);
Use useWindowDimensions for layouts that adapt to orientation.
Common platform pitfalls
| 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 |
Anti-patterns
- ❌ Forking entire screens for
Platform.OS === 'ios' when 90% of code is shared — use inline conditionals or .ios.tsx/.android.tsx only for genuinely different markup.
- ❌ Ignoring safe areas — content clipped by notch/home indicator.
- ❌ Calling
NativeModules.X directly without Platform check — module may not exist on the other platform = crash.
- ❌ Using web-only positioning (
position: 'fixed').
- ❌ Hardcoding pixel values without considering pixel ratio (use density-independent units; RN's "px" already handles this).
- ❌ Forgetting to declare permission usage strings in
app.json / Info.plist — App Store / Play Store rejection.
- ❌ Assuming Android back button works without handling —
BackHandler.addEventListener('hardwareBackPress', ...).
- ❌ Mixing the deprecated
SafeAreaView from react-native with react-native-safe-area-context.