Platform-Specific Code Skill
This skill covers patterns for handling iOS and Android differences.
When to Use
Use this skill when:
-
Writing platform-specific UI
-
Handling platform APIs differently
-
Creating platform-specific files
-
Styling for each platform
Core Principle
WRITE ONCE, ADAPT WHERE NEEDED - Share code where possible, diverge only when necessary.
Platform Detection
import { Platform } from 'react-native';
// Basic detection if (Platform.OS === 'ios') { // iOS-specific code } else if (Platform.OS === 'android') { // Android-specific code }
// Platform.select for values const styles = { container: { paddingTop: Platform.select({ ios: 20, android: 0, }), }, };
// With default value const shadowStyle = Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, }, android: { elevation: 5, }, default: {}, });
Platform-Specific Files
// File structure components/ ├── Button.tsx // Shared code ├── Button.ios.tsx // iOS-specific ├── Button.android.tsx // Android-specific
// Button.ios.tsx import { TouchableOpacity, Text } from 'react-native';
export function Button({ onPress, children }) { return ( <TouchableOpacity onPress={onPress} style={{ paddingVertical: 12, paddingHorizontal: 24 }} > <Text>{children}</Text> </TouchableOpacity> ); }
// Button.android.tsx import { Pressable, Text } from 'react-native';
export function Button({ onPress, children }) { return ( <Pressable onPress={onPress} android_ripple={{ color: 'rgba(0,0,0,0.1)' }} style={{ paddingVertical: 12, paddingHorizontal: 24 }} > <Text>{children}</Text> </Pressable> ); }
// Usage - automatically picks correct file import { Button } from './Button';
Platform-Specific Styling
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({ container: { flex: 1, // Platform-specific values ...Platform.select({ ios: { paddingTop: 44, // iOS notch }, android: { paddingTop: 24, // Android status bar }, }), }, shadow: Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, }, android: { elevation: 4, }, }), });
NativeWind Platform Classes
// Using NativeWind with platform prefixes <View className="ios:pt-12 android:pt-6"> <Text>Platform-specific padding</Text> </View>
<View className="ios:shadow-lg android:elevation-4"> <Text>Platform-specific shadows</Text> </View>
Safe Area Handling
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
// Using SafeAreaView function Screen() { return ( <SafeAreaView style={{ flex: 1 }}> <Content /> </SafeAreaView> ); }
// Using hook for fine control function Header() { const insets = useSafeAreaInsets();
return ( <View style={{ paddingTop: insets.top }}> <Text>Header</Text> </View> ); }
// Platform-specific safe area function PlatformHeader() { const insets = useSafeAreaInsets();
return ( <View style={{ paddingTop: Platform.select({ ios: insets.top, android: insets.top + 8, // Extra padding on Android }), }} > <Text>Header</Text> </View> ); }
Status Bar
import { StatusBar, Platform } from 'react-native';
function App() { return ( <> <StatusBar barStyle={Platform.select({ ios: 'dark-content', android: 'light-content', })} backgroundColor={Platform.OS === 'android' ? '#ffffff' : undefined} translucent={Platform.OS === 'android'} /> <Content /> </> ); }
Platform-Specific Navigation
import { Platform } from 'react-native'; import { Stack } from 'expo-router';
function StackLayout() { return ( <Stack screenOptions={{ headerStyle: { backgroundColor: '#fff', }, // iOS-specific ...(Platform.OS === 'ios' && { headerLargeTitle: true, headerTransparent: true, headerBlurEffect: 'regular', }), // Android-specific ...(Platform.OS === 'android' && { animation: 'slide_from_right', }), }} > <Stack.Screen name="index" /> </Stack> ); }
Platform-Specific Haptics
import * as Haptics from 'expo-haptics'; import { Platform, Vibration } from 'react-native';
async function triggerFeedback() { if (Platform.OS === 'ios') { await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } else { Vibration.vibrate(50); } }
// Selection feedback async function selectionFeedback() { if (Platform.OS === 'ios') { await Haptics.selectionAsync(); } // Android handles selection feedback automatically }
Platform-Specific Keyboards
import { Platform, KeyboardAvoidingView } from 'react-native';
function FormScreen() { return ( <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={Platform.select({ ios: 88, // Header height android: 0, })} style={{ flex: 1 }} > <Form /> </KeyboardAvoidingView> ); }
Platform-Specific Permissions
import * as ImagePicker from 'expo-image-picker'; import { Platform } from 'react-native';
async function requestCameraPermission() { if (Platform.OS === 'ios') { const { status } = await ImagePicker.requestCameraPermissionsAsync(); return status === 'granted'; } else { // Android handles permissions differently const { status } = await ImagePicker.requestCameraPermissionsAsync(); return status === 'granted'; } }
Platform-Specific Links
import { Linking, Platform } from 'react-native';
function openSettings() { if (Platform.OS === 'ios') { Linking.openURL('app-settings:'); } else { Linking.openSettings(); } }
function openMaps(latitude: number, longitude: number) {
const url = Platform.select({
ios: maps:0,0?q=${latitude},${longitude},
android: geo:0,0?q=${latitude},${longitude},
});
if (url) { Linking.openURL(url); } }
function openPhone(phoneNumber: string) {
const url = Platform.select({
ios: telprompt:${phoneNumber},
android: tel:${phoneNumber},
});
if (url) { Linking.openURL(url); } }
Platform-Specific Components
import { Platform, Pressable, TouchableOpacity } from 'react-native';
// Use Pressable with ripple on Android function PlatformButton({ onPress, children, style }) { if (Platform.OS === 'android') { return ( <Pressable onPress={onPress} android_ripple={{ color: 'rgba(0,0,0,0.1)' }} style={style} > {children} </Pressable> ); }
return ( <TouchableOpacity onPress={onPress} style={style}> {children} </TouchableOpacity> ); }
Platform-Specific Fonts
import { Platform } from 'react-native';
const fontFamily = Platform.select({ ios: 'System', android: 'Roboto', });
// With custom fonts const customFont = Platform.select({ ios: 'SF Pro Display', android: 'sans-serif-medium', });
Version Checking
import { Platform } from 'react-native';
// Check platform version const isIOS15OrLater = Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 15; const isAndroid12OrLater = Platform.OS === 'android' && Platform.Version >= 31;
// Conditional features if (isIOS15OrLater) { // Use iOS 15+ features }
Platform Constants
import { Platform } from 'react-native';
// iOS specific if (Platform.OS === 'ios') { console.log('iOS Version:', Platform.Version); // e.g., "17.0" console.log('Is iPad:', Platform.isPad); console.log('Is TV:', Platform.isTV); }
// Android specific if (Platform.OS === 'android') { console.log('API Level:', Platform.Version); // e.g., 34 }
Testing Platform Code
import { Platform } from 'react-native';
// Mock Platform in tests jest.mock('react-native/Libraries/Utilities/Platform', () => ({ OS: 'ios', select: jest.fn((obj) => obj.ios), }));
// Or for Android jest.mock('react-native/Libraries/Utilities/Platform', () => ({ OS: 'android', select: jest.fn((obj) => obj.android), Version: 31, }));
Notes
-
Use platform-specific files for large differences
-
Use Platform.select for simple value differences
-
Test on both platforms regularly
-
Consider using design system that handles differences
-
Document platform-specific behavior
-
Use Expo's cross-platform APIs when available