--- name: sdlc-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). paths: ["**/*.ios.*", "**/*.android.*", "ios/**", "android/**"] --- # Platform-Specific Patterns iOS and Android have real differences. RN abstracts most, but sometimes you need to branch. ## `Platform.OS` ```ts import { 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: ```tsx 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: ```ts 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.Version` Numeric on Android (API level: 28, 30, 33, 34), string on iOS (e.g. `'17.0'`): ```ts if (Platform.OS === 'android' && Platform.Version >= 33) { // Android 13+ specific behavior } if (Platform.OS === 'ios' && parseInt(Platform.Version as string) >= 17) { // iOS 17+ specific } ``` ## 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) ``` ```tsx // import is the same — Metro resolves to the right file 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: ```bash pnpm add expo-camera expo-location expo-notifications ``` ```tsx 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: ```bash pnpm add react-native-camera cd ios && pod install # iOS only — install CocoaPods deps ``` 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: ```tsx 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`: ```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: ```tsx 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) ```tsx // Wrap app once at root import { SafeAreaProvider } from 'react-native-safe-area-context'; export default function App() { return ( ); } // Use SafeAreaView in screens import { SafeAreaView } from 'react-native-safe-area-context'; {/* screen content */} // Or insets for fine control import { useSafeAreaInsets } from 'react-native-safe-area-context'; const insets = useSafeAreaInsets(); ... ``` 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 ```tsx import { KeyboardAvoidingView, Platform } from 'react-native'; {/* form */} ``` For more control, use `react-native-keyboard-controller`: ```tsx import { KeyboardProvider } from 'react-native-keyboard-controller'; // Wrap app, provides better keyboard event APIs and animations. ``` ## Status bar ### Expo ```tsx import { StatusBar } from 'expo-status-bar'; ``` `style`: `'auto'` (matches color scheme), `'light'`, `'dark'`, `'inverted'`. ### Bare ```tsx import { StatusBar } from 'react-native'; ``` `backgroundColor` is Android-only; iOS uses the underlying view's color. ## Pixel ratio and dimensions ```tsx 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. ## 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`.