React Native iOS发布/TestFlight模式下NSInvalidArgumentException报错
问题描述
基于React Native CLI构建、集成Firebase实现用户认证的应用,iOS调试模式下运行正常,但通过TestFlight部署或切换为Release方案运行时,在Widget页面(使用react-native-drag-sort库)出现以下错误:
NSInvalidArgumentException: -[__NSDictionaryM doubleValue]: unrecognized selector sent to instance 0x2807e6c40
已完成的排查操作:
- 确认应用在调试模式下无异常
- 将Release模式的优化设置为None
Widget页面代码如下:
import React, {createRef, useState, useEffect, useRef} from 'react'; import { Text, TouchableOpacity, StyleSheet, View, Animated, Vibration, SafeAreaView, Image, ScrollView, } from 'react-native'; import styles from './styles'; import {useFocusEffect} from '@react-navigation/native'; import {calendarWidgetList} from '../../constants/general'; import {AnySizeDragSortableView} from 'react-native-drag-sort'; import LoadingIndicator from '../loadingindicator/LoadingIndicator'; import { deleteWidget, fetchWidgets, getWidgetsFromFirebase, uploadWidgetsToFirebase, } from '../../utils/function'; import Images from '../../constants/images'; import {useNavigation} from '@react-navigation/native'; import {COLORS} from '../../constants'; export default Draggable = ({userId, addedWidgets}) => { const navigation = useNavigation(); const sortableViewRef = useRef(); const [widgets, setWidgets] = useState(addedWidgets); const [isLoading, setLoading] = useState(false); const [movedKey, setMovedKey] = useState(null); const [jiggleAnimation] = useState(new Animated.Value(0)); const [disableScroll, setDisableScroll] = useState(false); const startJiggleAnimation = () => { Animated.loop( Animated.sequence([ Animated.timing(jiggleAnimation, { toValue: 1, duration: 150, useNativeDriver: true, }), Animated.timing(jiggleAnimation, { toValue: -1, duration: 150, useNativeDriver: true, }), Animated.timing(jiggleAnimation, { toValue: 0, duration: 150, useNativeDriver: true, }), ]), ).start(); }; const stopJiggleAnimation = () => { jiggleAnimation.stopAnimation(); jiggleAnimation.setValue(0); }; useEffect(() => { return () => { stopJiggleAnimation(); }; }, []); const handleLongPress = (item, index) => { setMovedKey(item.id); sortableViewRef.current.startTouch(item, index); Vibration.vibrate(50); startJiggleAnimation(); setDisableScroll(true); }; const handleDragEnd = () => { setMovedKey(null); setDisableScroll(false); updateOrder(); }; useEffect(() => { setWidgets(addedWidgets); }, [addedWidgets]); const updateOrder = () => { let count = 0; widgets.forEach(i => { addedWidgets.forEach(j => { if (j.id === i.id) { j.id = count; count++; } }); }); uploadWidgetsToFirebase(userId, widgets); }; const handleDeleteWidget = async itemId => { const updatedWidgets = widgets.filter(item => item.id !== itemId); setWidgets(updatedWidgets); sortableViewRef.current.onPressOut(); deleteWidget(userId, itemId); }; // useFocusEffect( React.useCallback(() => { const fetchWidgets = async () => { setLoading(true); const updatedWidgetsAfterDeletion = await getWidgetsFromFirebase(userId); setWidgets(updatedWidgetsAfterDeletion); setLoading(false); }; fetchWidgets(); }, [userId]), // ); useEffect(() => { if (!isLoading) { sortableViewRef.current?.scrollTo({x: 0, y: 0, animated: true}); } }, [isLoading]); const renderItem = (item, index, isMoved) => { return ( <TouchableOpacity activeOpacity={1} // onPressOut={handlePressOut} onLongPress={() => handleLongPress(item, index)} // onPressOut={() => sortableViewRef.current.onPressOut()} > <Animated.View style={[ { position: 'relative', paddingTop: 0, //Change gap between widgets }, isMoved && { transform: [ { rotate: jiggleAnimation.interpolate({ inputRange: [0, 1], outputRange: ['1deg', '-1deg'], }), }, ], }, ]}> <View style={[ styles.widget, { width: item.width, height: item.height, //Change gap between widgets if paddingTop = 0 is not enough }, ]}> {isMoved ? ( <View> <TouchableOpacity style={{ zIndex: 10, }} onPress={() => { handleDeleteWidget(item.id); }}> <Image source={Images['remove_widget']} style={{ opacity: 0.7, height: 23, width: 23, position: 'absolute', top: -5, left: -5, }} /> </TouchableOpacity> {item.component} </View> ) : ( <View>{item.component}</View> )} </View> </Animated.View> </TouchableOpacity> ); }; return isLoading ? ( <LoadingIndicator /> ) : ( <SafeAreaView style={{flex: 1}}> <ScrollView // onScroll={sortableViewRef.current?.onPressOut()} scrollEnabled={!disableScroll} contentContainerStyle={{flexGrow: 1}}> <AnySizeDragSortableView //This line is the point where the error starts ref={sortableViewRef} dataSource={widgets} keyExtractor={item => item.id} renderItem={renderItem} onDataChange={(data, callback) => { setWidgets(data); callback(); }} movedWrapStyle={styles.widgetMoved} onDragEnd={handleDragEnd} /> </ScrollView> </SafeAreaView> ); };
问题分析与解决方案
原因分析
- 类型不匹配(Release模式无自动转换):调试模式下RN会自动做类型兼容处理,但Release模式会直接执行代码。
react-native-drag-sort库期望width/height/id为数字类型,但从Firebase获取的数据可能被序列化为字典(NSDictionaryM),导致库内部调用doubleValue方法时出错。 - 直接修改Props数据源:
updateOrder函数直接修改传入的addedWidgets数组的id属性,破坏了数据源的不可变性,引发类型混乱,在Release模式下这种操作的副作用会被放大。 - useCallback用法错误:代码中
useCallback未正确包裹在useFocusEffect内,导致组件聚焦时的数据源更新逻辑异常,进一步引发数据类型问题。
解决方案
1. 强制转换数据类型
在从Firebase获取数据后,显式将id、width、height转换为数字类型:
// 修改getWidgetsFromFirebase函数 const getWidgetsFromFirebase = async (userId) => { const rawData = await // 你的Firebase数据获取逻辑 return rawData.map(item => ({ ...item, id: Number(item.id), width: Number(item.width), height: Number(item.height) })); };
2. 修复数据源更新逻辑
不要直接修改Props数组,生成新数组更新状态:
const updateOrder = () => { const updatedWidgets = widgets.map((item, index) => ({ ...item, id: index })); setWidgets(updatedWidgets); uploadWidgetsToFirebase(userId, updatedWidgets); };
3. 修正useFocusEffect用法
将useCallback正确包裹在useFocusEffect内,确保聚焦时的数据源更新逻辑正常:
useFocusEffect( React.useCallback(() => { const fetchWidgets = async () => { setLoading(true); const updatedWidgetsAfterDeletion = await getWidgetsFromFirebase(userId); setWidgets(updatedWidgetsAfterDeletion); setLoading(false); }; fetchWidgets(); }, [userId]) );
4. 添加类型校验
用PropTypes校验Props的类型,提前发现类型错误:
import PropTypes from 'prop-types'; Draggable.propTypes = { userId: PropTypes.string.isRequired, addedWidgets: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, component: PropTypes.element.isRequired })).isRequired };
5. 升级依赖库
确认react-native-drag-sort版本与当前RN版本兼容,若版本较旧,升级到最新稳定版,避免库内部的Release模式类型处理bug。
内容的提问来源于stack exchange,提问作者Pranav Rokade
相关产品推荐
相关产品推荐

