You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
  );
};
问题分析与解决方案

原因分析

  1. 类型不匹配(Release模式无自动转换):调试模式下RN会自动做类型兼容处理,但Release模式会直接执行代码。react-native-drag-sort库期望width/height/id为数字类型,但从Firebase获取的数据可能被序列化为字典(NSDictionaryM),导致库内部调用doubleValue方法时出错。
  2. 直接修改Props数据源:updateOrder函数直接修改传入的addedWidgets数组的id属性,破坏了数据源的不可变性,引发类型混乱,在Release模式下这种操作的副作用会被放大。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 00:39:50