name: rn-native-features description: Expo React Native 应用中的原生 iOS 功能。在 Expo 中实现相机、推送通知、触觉反馈、权限、设备传感器或其他原生 API 时使用。
原生特性 (Expo)
权限模式
在使用原生特性前总是请求权限:
import * as Camera from 'expo-camera';
async function requestCameraPermission() {
const { status } = await Camera.requestCameraPermissionsAsync();
if (status !== 'granted') {
// 如果被拒绝,引导用户去设置
Alert.alert(
'需要相机访问权限',
'请在设置中启用相机访问权限以拍照。',
[
{ text: '取消', style: 'cancel' },
{ text: '打开设置', onPress: () => Linking.openSettings() },
]
);
return false;
}
return true;
}
先检查权限状态
import * as Camera from 'expo-camera';
function usePermission() {
const [permission, requestPermission] = Camera.useCameraPermissions();
// permission.granted - 布尔值
// permission.canAskAgain - 如果用户选择了“不再询问”则为 false
// 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>需要相机访问权限</Text>
<Button title="授权" onPress={requestPermission} />
</View>
);
}
const takePicture = async () => {
if (cameraRef.current) {
const photo = await cameraRef.current.takePictureAsync({
quality: 0.8,
base64: false,
exif: false,
});
console.log('照片 URI:', photo.uri);
// photo.uri 是一个本地文件路径
}
};
return (
<View style={{ flex: 1 }}>
<CameraView
ref={cameraRef}
style={{ flex: 1 }}
facing={facing}
>
<View style={styles.controls}>
<Button title="翻转" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} />
<Button title="拍照" 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('推送通知需要物理设备');
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('推送通知权限被拒绝');
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('收到:', 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: "比赛提醒",
body: "您的比赛将在 30 分钟后开始!",
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,
"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()
触觉反馈
import * as Haptics from 'expo-haptics';
// 轻触 - 用于 UI 交互
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
// 中等 - 用于确认
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
// 重触 - 用于重要操作
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
// 成功/错误/警告 - 语义反馈
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
// 选择 - 用于选择器/滚动
Haptics.selectionAsync();
良好的触觉模式
// 按钮按压
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
handlePress();
}}
/>
// 表单提交成功
await submitForm();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// 错误状态
if (error) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
// 选择器/滚动选择
<Picker
onValueChange={(value) => {
Haptics.selectionAsync();
setValue(value);
}}
/>
位置
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,
};
}
// 监听位置变化
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, // 米
},
(loc) => setLocation(loc.coords)
);
})();
return () => subscription?.remove();
}, []);
return location;
}
本地存储
AsyncStorage(简单的键值存储)
import AsyncStorage from '@react-native-async-storage/async-storage';
// 存储
await AsyncStorage.setItem('user_preferences', JSON.stringify(prefs));
// 检索
const prefs = JSON.parse(await AsyncStorage.getItem('user_preferences') || '{}');
// 删除
await AsyncStorage.removeItem('user_preferences');
SecureStore(敏感数据)
import * as SecureStore from 'expo-secure-store';
// 用于令牌、凭证 - 加密存储
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
await SecureStore.deleteItemAsync('auth_token');
应用状态与生命周期
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]);
}
// 使用
useAppState((state) => {
if (state === 'active') {
// 应用进入前台 - 刷新数据
refreshData();
} else if (state === 'background') {
// 应用进入后台 - 保存状态
saveState();
}
});
键盘处理
import { KeyboardAvoidingView, Platform } from 'react-native';
// 包装表单以避免键盘
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<YourForm />
</KeyboardAvoidingView>
// 收起键盘
import { Keyboard } from 'react-native';
Keyboard.dismiss();
// 监听键盘事件
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;
}
常见问题
| 问题 | 解决方案 |
|---|---|
| 相机黑屏 | 检查权限,确保 CameraView 有明确的尺寸 |
| 通知未接收 | 验证物理设备,检查推送令牌注册 |
| 位置不准确 | 使用 Accuracy.High,检查设备位置服务是否启用 |
| 触觉反馈不工作 | 只在物理设备上工作,不在模拟器上 |
| SecureStore 大小限制 | 每项最大 ~2KB,用于令牌不用于大数据 |