jpskill.com
🛠️ 開発・MCP コミュニティ

rn-native-features

Native iOS features in Expo React Native apps. Use when implementing camera, push notifications, haptics, permissions, device sensors, or other native APIs in Expo.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o rn-native-features.zip https://jpskill.com/download/17857.zip && unzip -o rn-native-features.zip && rm rn-native-features.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17857.zip -OutFile "$d\rn-native-features.zip"; Expand-Archive "$d\rn-native-features.zip" -DestinationPath $d -Force; ri "$d\rn-native-features.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して rn-native-features.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → rn-native-features フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

ネイティブ機能 (Expo)

パーミッションのパターン

ネイティブ機能を使用する前に、必ずパーミッションをリクエストしてください。

import * as Camera from 'expo-camera';

async function requestCameraPermission() {
  const { status } = await Camera.requestCameraPermissionsAsync();

  if (status !== 'granted') {
    // 拒否された場合、ユーザーを設定に誘導する
    Alert.alert(
      'Camera Access Required',
      'Please enable camera access in Settings to take photos.',
      [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Open Settings', onPress: () => Linking.openSettings() },
      ]
    );
    return false;
  }
  return true;
}

最初にパーミッションの状態を確認する

import * as Camera from 'expo-camera';

function usePermission() {
  const [permission, requestPermission] = Camera.useCameraPermissions();

  // permission.granted - boolean
  // permission.canAskAgain - false if user selected "Don't ask again"
  // permission.status - 'granted' | 'denied' | 'undetermined'

  return { permission, requestPermission };
}

カメラ

写真撮影機能付きの基本的なカメラ

import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRef, useState } from 'react';

export function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [facing, setFacing] = useState<'front' | 'back'>('back');
  const cameraRef = useRef<CameraView>(null);

  if (!permission) return <View />;

  if (!permission.granted) {
    return (
      <View>
        <Text>Camera access is required</Text>
        <Button title="Grant Permission" onPress={requestPermission} />
      </View>
    );
  }

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync({
        quality: 0.8,
        base64: false,
        exif: false,
      });
      console.log('Photo URI:', photo.uri);
      // photo.uri はローカルファイルパスです
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView 
        ref={cameraRef}
        style={{ flex: 1 }} 
        facing={facing}
      >
        <View style={styles.controls}>
          <Button title="Flip" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
          <Button title="Take Photo" onPress={takePicture} />
        </View>
      </CameraView>
    </View>
  );
}

イメージピッカー (ギャラリー + カメラ)

多くの場合、フルカメラ UI よりも簡単です。

import * as ImagePicker from 'expo-image-picker';

async function pickImage() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

async function takePhoto() {
  const permission = await ImagePicker.requestCameraPermissionsAsync();
  if (!permission.granted) return;

  const result = await ImagePicker.launchCameraAsync({
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

プッシュ通知

expo-notifications でのセットアップ

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';

// アプリがフォアグラウンドにあるときの通知の表示方法を設定します
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

async function registerForPushNotifications() {
  if (!Device.isDevice) {
    console.log('Push notifications require a physical device');
    return null;
  }

  // 既存のパーミッションを確認する
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  // 未確定の場合、リクエストする
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    console.log('Push notification permission denied');
    return null;
  }

  // Expo プッシュトークンを取得する
  const token = await Notifications.getExpoPushTokenAsync({
    projectId: 'your-expo-project-id', // app.json から
  });

  return token.data; // "ExponentPushToken[xxxx]"
}

受信通知の処理

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';

export function useNotificationHandler() {
  const notificationListener = useRef<Notifications.Subscription>();
  const responseListener = useRef<Notifications.Subscription>();

  useEffect(() => {
    // アプリがフォアグラウンドにある間に通知を受信した場合
    notificationListener.current = Notifications.addNotificationReceivedListener(
      (notification) => {
        console.log('Received:', notification.request.content);
      }
    );

    // ユーザーが通知をタップした場合
    responseListener.current = Notifications.addNotificationResponseReceivedListener(
      (response) => {
        const data = response.notification.request.content.data;
        // 通知データに基づいてナビゲートする
        if (data.screen) {
          router.push(data.screen);
        }
      }
    );

    return () => {
      notificationListener.current?.remove();
      responseListener.current?.remove();
    };
  }, []);
}

テスト通知をローカルで送信する

async function sendTestNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "Match Reminder",
      body: "Your match starts in 30 minutes!",
      data: { screen: '/match/123' },
    },
    trigger: { seconds: 5 },
  });
}

バックエンドから送信する (Expo Push API)


# FastAPI の例
import httpx

async def send_push_notification(
    expo_push_token: str, 
    title: str, 
    body: str, 
    data: dict = None
):
    message = {
        "to": expo_push_token,


(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Native Features (Expo)

Permissions Pattern

Always request permissions before using native features:

import * as Camera from 'expo-camera';

async function requestCameraPermission() {
  const { status } = await Camera.requestCameraPermissionsAsync();

  if (status !== 'granted') {
    // Guide user to settings if denied
    Alert.alert(
      'Camera Access Required',
      'Please enable camera access in Settings to take photos.',
      [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Open Settings', onPress: () => Linking.openSettings() },
      ]
    );
    return false;
  }
  return true;
}

Check permission status first

import * as Camera from 'expo-camera';

function usePermission() {
  const [permission, requestPermission] = Camera.useCameraPermissions();

  // permission.granted - boolean
  // permission.canAskAgain - false if user selected "Don't ask again"
  // permission.status - 'granted' | 'denied' | 'undetermined'

  return { permission, requestPermission };
}

Camera

Basic Camera with Photo Capture

import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRef, useState } from 'react';

export function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [facing, setFacing] = useState<'front' | 'back'>('back');
  const cameraRef = useRef<CameraView>(null);

  if (!permission) return <View />;

  if (!permission.granted) {
    return (
      <View>
        <Text>Camera access is required</Text>
        <Button title="Grant Permission" onPress={requestPermission} />
      </View>
    );
  }

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync({
        quality: 0.8,
        base64: false,
        exif: false,
      });
      console.log('Photo URI:', photo.uri);
      // photo.uri is a local file path
    }
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView 
        ref={cameraRef}
        style={{ flex: 1 }} 
        facing={facing}
      >
        <View style={styles.controls}>
          <Button title="Flip" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
          <Button title="Take Photo" onPress={takePicture} />
        </View>
      </CameraView>
    </View>
  );
}

Image Picker (Gallery + Camera)

Often simpler than full camera UI:

import * as ImagePicker from 'expo-image-picker';

async function pickImage() {
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

async function takePhoto() {
  const permission = await ImagePicker.requestCameraPermissionsAsync();
  if (!permission.granted) return;

  const result = await ImagePicker.launchCameraAsync({
    allowsEditing: true,
    aspect: [1, 1],
    quality: 0.8,
  });

  if (!result.canceled) {
    return result.assets[0].uri;
  }
}

Push Notifications

Setup with expo-notifications

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';

// Configure how notifications appear when app is foregrounded
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

async function registerForPushNotifications() {
  if (!Device.isDevice) {
    console.log('Push notifications require a physical device');
    return null;
  }

  // Check existing permission
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  // Request if not determined
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    console.log('Push notification permission denied');
    return null;
  }

  // Get Expo push token
  const token = await Notifications.getExpoPushTokenAsync({
    projectId: 'your-expo-project-id', // From app.json
  });

  return token.data; // "ExponentPushToken[xxxx]"
}

Handle incoming notifications

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';

export function useNotificationHandler() {
  const notificationListener = useRef<Notifications.Subscription>();
  const responseListener = useRef<Notifications.Subscription>();

  useEffect(() => {
    // Notification received while app is foregrounded
    notificationListener.current = Notifications.addNotificationReceivedListener(
      (notification) => {
        console.log('Received:', notification.request.content);
      }
    );

    // User tapped on notification
    responseListener.current = Notifications.addNotificationResponseReceivedListener(
      (response) => {
        const data = response.notification.request.content.data;
        // Navigate based on notification data
        if (data.screen) {
          router.push(data.screen);
        }
      }
    );

    return () => {
      notificationListener.current?.remove();
      responseListener.current?.remove();
    };
  }, []);
}

Send test notification locally

async function sendTestNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "Match Reminder",
      body: "Your match starts in 30 minutes!",
      data: { screen: '/match/123' },
    },
    trigger: { seconds: 5 },
  });
}

Send from backend (Expo Push API)

# FastAPI example
import httpx

async def send_push_notification(
    expo_push_token: str, 
    title: str, 
    body: str, 
    data: dict = None
):
    message = {
        "to": expo_push_token,
        "title": title,
        "body": body,
        "data": data or {},
    }

    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://exp.host/--/api/v2/push/send",
            json=message,
            headers={"Content-Type": "application/json"},
        )
        return response.json()

Haptic Feedback

import * as Haptics from 'expo-haptics';

// Light tap - for UI interactions
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

// Medium - for confirmations
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);

// Heavy - for significant actions
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);

// Success/Error/Warning - semantic feedback
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);

// Selection - for picker/scroll
Haptics.selectionAsync();

Good haptic patterns

// Button press
<Pressable 
  onPress={() => {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
    handlePress();
  }}
/>

// Form submission success
await submitForm();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);

// Error state
if (error) {
  Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}

// Picker/Scroll selection
<Picker 
  onValueChange={(value) => {
    Haptics.selectionAsync();
    setValue(value);
  }}
/>

Location

import * as Location from 'expo-location';

async function getCurrentLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') {
    return null;
  }

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced,
  });

  return {
    latitude: location.coords.latitude,
    longitude: location.coords.longitude,
  };
}

// Watch location changes
function useLocationTracking() {
  const [location, setLocation] = useState(null);

  useEffect(() => {
    let subscription: Location.LocationSubscription;

    (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (status !== 'granted') return;

      subscription = await Location.watchPositionAsync(
        {
          accuracy: Location.Accuracy.High,
          distanceInterval: 10, // meters
        },
        (loc) => setLocation(loc.coords)
      );
    })();

    return () => subscription?.remove();
  }, []);

  return location;
}

Local Storage

AsyncStorage (simple key-value)

import AsyncStorage from '@react-native-async-storage/async-storage';

// Store
await AsyncStorage.setItem('user_preferences', JSON.stringify(prefs));

// Retrieve
const prefs = JSON.parse(await AsyncStorage.getItem('user_preferences') || '{}');

// Remove
await AsyncStorage.removeItem('user_preferences');

SecureStore (sensitive data)

import * as SecureStore from 'expo-secure-store';

// For tokens, credentials - encrypted storage
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');

App State & Lifecycle

import { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';

function useAppState(callback: (state: AppStateStatus) => void) {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', callback);
    return () => subscription.remove();
  }, [callback]);
}

// Usage
useAppState((state) => {
  if (state === 'active') {
    // App came to foreground - refresh data
    refreshData();
  } else if (state === 'background') {
    // App going to background - save state
    saveState();
  }
});

Keyboard Handling

import { KeyboardAvoidingView, Platform } from 'react-native';

// Wrap forms to avoid keyboard
<KeyboardAvoidingView 
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  style={{ flex: 1 }}
>
  <YourForm />
</KeyboardAvoidingView>

// Dismiss keyboard
import { Keyboard } from 'react-native';
Keyboard.dismiss();

// Listen to keyboard events
import { useEffect } from 'react';
import { Keyboard } from 'react-native';

function useKeyboardVisible() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const showSub = Keyboard.addListener('keyboardDidShow', () => setVisible(true));
    const hideSub = Keyboard.addListener('keyboardDidHide', () => setVisible(false));
    return () => {
      showSub.remove();
      hideSub.remove();
    };
  }, []);

  return visible;
}

Common Issues

Issue Solution
Camera black screen Check permissions, ensure CameraView has explicit dimensions
Notifications not received Verify physical device, check push token registration
Location inaccurate Use Accuracy.High, check device location services enabled
Haptics not working Only works on physical device, not simulator
SecureStore size limit Max ~2KB per item, use for tokens not large data