callstack / react-native-pager-view

React Native wrapper for the Android ViewPager and iOS UIPageViewController.
MIT License
2.74k stars 420 forks source link

NSInternalInconsistencyException crashes app #507

Open darajava opened 2 years ago

darajava commented 2 years ago

Environment

iOS

Description

I have some fairly simple code that populates pages and then navigates to one of them. For some reason, if I am generating more than 20/30 pages on iOS, the app crashes with:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unexpected view controller: <UIViewController: 0x7fe24088d100>'

Reproducible Demo

I populate maybe 20 pages, and then elsewhere in the app, asynchronously navigate to a page somewhere towards the end (but after the pages in theory have been populated). I can see for a split second that it's trying to land on a page that doesn't yet exist, then it crashes. I guess that it's trying to access this screen before it has time to generate it. Oddly, using a setTimeout to access the page does not help. I call the page with setPage on the ref.

The code of the component in question:

type Props = {
  // Should always be in order
  dates: SliderDate[]
  onDateSelect: (date: Date) => void
}

function sliceIntoChunks(arr: any[], chunkSize: number) {
  const res = []
  for (let i = 0; i < arr.length; i += chunkSize) {
    const chunk = arr.slice(i, i + chunkSize)
    res.push(chunk)
  }

  return res
}

export const DateSlider = (props: Props) => {
  const [weeks, setWeeks] = useState<SliderDate[][]>([])

  const getWeeks = (dates: SliderDate[]): SliderDate[][] => {
    const weekResult: SliderDate[] = []

    /// Fill with blank dates
    const dateTicker = new Date()
    dateTicker.setHours(0, 0, 0, 0)
    dateTicker.setDate(dateTicker.getDate() - 100) // <--- breaks it
    // dateTicker.setDate(dateTicker.getDate() - 10) // <- this one is ok

    // find closest monday
    while (dateTicker.getDay() !== 1) {
      dateTicker.setDate(dateTicker.getDate() + 1)
    }

    console.log("got here")
    for (let i = -10; i < 30; i++) {
      // console.log(dateTicker.getDay())
      if (dateTicker.getDay() !== 0 && dateTicker.getDay() !== 6) {
        weekResult.push({
          date: new Date(dateTicker),
          selected: false,
          recurring: false,
          ordered: false,
          disabled: true,
        })
      }

      dateTicker.setDate(dateTicker.getDate() + 1)
    }

    while (weekResult.length % 5 !== 0) {
      console.log("got here2", weekResult.length)

      if (dateTicker.getDay() !== 0 && dateTicker.getDay() !== 6) {
        weekResult.push({
          date: new Date(dateTicker),
          selected: false,
          recurring: false,
          ordered: false,
          disabled: true,
        })
      }

      dateTicker.setDate(dateTicker.getDate() + 1)
    }

    /// Overwrite with dates from props
    for (const date of props.dates) {
      const realIndex = weekResult.findIndex(day => {
        return day.date.toISOString() === date.date.toISOString()
      })

      weekResult[realIndex] = date
    }

    return sliceIntoChunks(weekResult, 5)
  }

  useEffect(() => {
    const newWeeks = getWeeks(props.dates)
    setWeeks(newWeeks)

    const idx = newWeeks.findIndex(week => {
      return !!week.find(day => day.selected)
    })
    if (idx < newWeeks.length) {
      setTimeout(() => {
        // @ts-ignore
        pagerRef.current!.setPage(idx)
      }, 500)
    }
  }, [props.dates])
  const pagerRef = useRef(null)

  return (
    <View style={styles.main}>
      <PagerView
        style={styles.pagerView}
        ref={pagerRef}
        // transitionStyle="curl"
      >
        {weeks.map(week => {
          return (
            <View style={styles.datesHolder} key={week[0].date.toISOString()}>
              <DateItem onSelect={props.onDateSelect} date={week[0]} />
              <DateItem onSelect={props.onDateSelect} date={week[1]} />
              <DateItem onSelect={props.onDateSelect} date={week[2]} />
              <DateItem onSelect={props.onDateSelect} date={week[3]} />
              <DateItem onSelect={props.onDateSelect} date={week[4]} />
            </View>
          )
        })}
      </PagerView>
    </View>
  )
}
troZee commented 2 years ago

@darajava could you try the next version yarn add react-native-pager-view@next ?

ivan-mumtak commented 2 years ago

@darajava could you try the next version yarn add react-native-pager-view@next ?

This bug is still present. I don't have reproducible demo because I'm using react-native-tab-view based on this repo and I have updated pager-view to 5.4.15 but this problem was fixed only partially and everything works fine just 1/3 of time

aike19115 commented 2 years ago

Having the same issue with version 5.4.9 (that's the current recommended version for Expo managed projects).

My implementation of the PagerView:

import * as React from 'react';
import { StyleSheet, TouchableOpacity, View, ViewStyle } from 'react-native';
import PagerView, { PagerViewOnPageSelectedEvent } from 'react-native-pager-view';

import { sizes } from '@/constants/sizes';
import { ArrowLeft } from '@/components/svgs/ArrowLeft';
import { ArrowRight } from '@/components/svgs/ArrowRight';
import { colors } from '@/constants/colors';
import { WelcomeCard } from '@/components/molecules/WelcomeCard';

const AMOUNT_OF_CARDS = 12;

export function ToursScreen() {
  const [selectedPage, setSelectedPage] = React.useState(0);
  const refPagerView = React.useRef<PagerView>(null);

  const onPressLeft = () => {
    if (selectedPage > 0) {
      refPagerView.current?.setPage(selectedPage - 1);
      setSelectedPage(selectedPage - 1);
    }
  };

  const onPressRight = () => {
    if (selectedPage < AMOUNT_OF_CARDS - 1) {
      refPagerView.current?.setPage(selectedPage + 1);
      setSelectedPage(selectedPage + 1);
    }
  };

  const onPageSelected = (event: PagerViewOnPageSelectedEvent) => {
    setSelectedPage(event.nativeEvent.position);
  };

  const cardElements = React.useMemo(
    () =>
      Array(AMOUNT_OF_CARDS)
        .fill(null)
        .map((_, index) => (
          // eslint-disable-next-line react/no-array-index-key
          <View key={index} style={styles.cardWrapper}>
            <WelcomeCard />
          </View>
        )),
    [],
  );

  return (
    <>
      <PagerView onPageSelected={onPageSelected} ref={refPagerView} style={styles.flex} initialPage={0}>
        {cardElements}
      </PagerView>
      <View style={styles.row}>
        <TouchableOpacity
          disabled={selectedPage === 0}
          onPress={onPressLeft}
          style={selectedPage === 0 ? styles.buttonDisabled : styles.button}
        >
          <ArrowLeft />
        </TouchableOpacity>
        <TouchableOpacity
          disabled={selectedPage === AMOUNT_OF_CARDS - 1}
          onPress={onPressRight}
          style={selectedPage === AMOUNT_OF_CARDS - 1 ? styles.buttonDisabled : styles.button}
        >
          <ArrowRight />
        </TouchableOpacity>
      </View>
    </>
  );
}

const buttonStyle: ViewStyle = {
  borderRadius: sizes.m,
  backgroundColor: colors.grey2,
  alignItems: 'center',
  justifyContent: 'center',
  paddingVertical: sizes.s,
  flex: 1,
};

const styles = StyleSheet.create({
  button: buttonStyle,
  buttonDisabled: {
    ...buttonStyle,
    opacity: 0.5,
  },
  cardWrapper: {
    padding: sizes.m,
  },
  row: {
    flexDirection: 'row',
  },
  flex: {
    flex: 1,
  },
});
robrechtme commented 2 years ago

Issue still occurs on v6.0.1.

grindos commented 1 year ago

Seems to still occur on v6.2.1

Nader-CS commented 1 month ago

still in ^6.3.0