From 5793305fa33850eaa54e46ffde179ccd0129b822 Mon Sep 17 00:00:00 2001
From: troZee <12766071+troZee@users.noreply.github.com>
Date: Fri, 14 Aug 2026 20:13:40 +0200
Subject: [PATCH 1/8] improve tests
---
.maestro/README.md | 12 +-
.maestro/flows/basic-pager/ensure-ltr.yaml | 22 ++++
.maestro/flows/basic-pager/ensure-rtl.yaml | 22 ++++
.maestro/flows/basic-pager/open.yaml | 12 ++
.../flows/basic-pager/verify-controls.yaml | 52 ++++++++
.../verify-horizontal-ltr-swipe.yaml | 29 +++++
.../verify-horizontal-rtl-swipe.yaml | 29 +++++
.../basic-pager/verify-vertical-swipe.yaml | 27 ++++
.maestro/setup/pager_basic_example_setup.yaml | 7 --
.../pager_vertical_basic_example_setup.yaml | 7 --
.maestro/smoke-test.yaml | 6 +-
.maestro/tests/pager_basic_example.yaml | 55 ++-------
.maestro/tests/pager_rtl_example.yaml | 12 ++
.../tests/pager_vertical_basic_example.yaml | 53 ++------
.../tests/rtl/pager_basic_example_rtl.yaml | 115 ------------------
.../rtl/pager_vertical_basic_example_rtl.yaml | 113 -----------------
example/src/App.tsx | 19 ++-
example/src/BasicPagerViewExample.tsx | 5 +-
scripts/run-maestro-tests.sh | 1 -
19 files changed, 255 insertions(+), 343 deletions(-)
create mode 100644 .maestro/flows/basic-pager/ensure-ltr.yaml
create mode 100644 .maestro/flows/basic-pager/ensure-rtl.yaml
create mode 100644 .maestro/flows/basic-pager/open.yaml
create mode 100644 .maestro/flows/basic-pager/verify-controls.yaml
create mode 100644 .maestro/flows/basic-pager/verify-horizontal-ltr-swipe.yaml
create mode 100644 .maestro/flows/basic-pager/verify-horizontal-rtl-swipe.yaml
create mode 100644 .maestro/flows/basic-pager/verify-vertical-swipe.yaml
delete mode 100644 .maestro/setup/pager_basic_example_setup.yaml
delete mode 100644 .maestro/setup/pager_vertical_basic_example_setup.yaml
create mode 100644 .maestro/tests/pager_rtl_example.yaml
delete mode 100644 .maestro/tests/rtl/pager_basic_example_rtl.yaml
delete mode 100644 .maestro/tests/rtl/pager_vertical_basic_example_rtl.yaml
diff --git a/.maestro/README.md b/.maestro/README.md
index 33abb1e4..8660c71d 100644
--- a/.maestro/README.md
+++ b/.maestro/README.md
@@ -19,6 +19,12 @@ bun run maestro:test:android
Use `bun run maestro:smoke` to run only the smoke flow.
`maestro:debug` writes failure artifacts to `.maestro/debug-output`.
-The smoke flow targets the example app ID `com.pagerviewexample` and verifies the Basic Example pager using stable `testID` selectors.
-Additional Basic Example regression flows live in `.maestro/basic_example`.
-Nested PagerView regression flows live in `.maestro/nested_pager_view_example`.
+The smoke flow targets the example app ID `com.pagerviewexample` and verifies the horizontal pager using stable `testID` selectors.
+
+Basic PagerView regression coverage is split into three deterministic flows:
+
+- `tests/pager_basic_example.yaml` verifies horizontal paging in LTR.
+- `tests/pager_vertical_basic_example.yaml` verifies vertical paging in LTR.
+- `tests/pager_rtl_example.yaml` switches to RTL before verifying the reversed horizontal gesture.
+
+Shared setup and assertions live in `flows/basic-pager`. Each flow resets the app to the required layout direction, so a failed RTL run cannot affect the next test.
diff --git a/.maestro/flows/basic-pager/ensure-ltr.yaml b/.maestro/flows/basic-pager/ensure-ltr.yaml
new file mode 100644
index 00000000..a60a4670
--- /dev/null
+++ b/.maestro/flows/basic-pager/ensure-ltr.yaml
@@ -0,0 +1,22 @@
+appId: com.pagerviewexample
+---
+- launchApp:
+ stopApp: true
+
+- extendedWaitUntil:
+ visible:
+ text: 'PagerView Example'
+ timeout: 15000
+
+- runFlow:
+ when:
+ visible:
+ id: 'layout-direction-rtl'
+ commands:
+ - tapOn:
+ id: 'layout-direction-rtl'
+
+- extendedWaitUntil:
+ visible:
+ id: 'layout-direction-ltr'
+ timeout: 15000
diff --git a/.maestro/flows/basic-pager/ensure-rtl.yaml b/.maestro/flows/basic-pager/ensure-rtl.yaml
new file mode 100644
index 00000000..7b02ea68
--- /dev/null
+++ b/.maestro/flows/basic-pager/ensure-rtl.yaml
@@ -0,0 +1,22 @@
+appId: com.pagerviewexample
+---
+- launchApp:
+ stopApp: true
+
+- extendedWaitUntil:
+ visible:
+ text: 'PagerView Example'
+ timeout: 15000
+
+- runFlow:
+ when:
+ visible:
+ id: 'layout-direction-ltr'
+ commands:
+ - tapOn:
+ id: 'layout-direction-ltr'
+
+- extendedWaitUntil:
+ visible:
+ id: 'layout-direction-rtl'
+ timeout: 15000
diff --git a/.maestro/flows/basic-pager/open.yaml b/.maestro/flows/basic-pager/open.yaml
new file mode 100644
index 00000000..da9d4350
--- /dev/null
+++ b/.maestro/flows/basic-pager/open.yaml
@@ -0,0 +1,12 @@
+appId: com.pagerviewexample
+---
+- tapOn:
+ id: ${EXAMPLE_ID}
+
+- extendedWaitUntil:
+ visible:
+ id: ${PAGER_ID}
+ timeout: 10000
+
+- assertVisible:
+ id: 'pageNumber0'
diff --git a/.maestro/flows/basic-pager/verify-controls.yaml b/.maestro/flows/basic-pager/verify-controls.yaml
new file mode 100644
index 00000000..1fcad56e
--- /dev/null
+++ b/.maestro/flows/basic-pager/verify-controls.yaml
@@ -0,0 +1,52 @@
+appId: com.pagerviewexample
+---
+- tapOn:
+ id: 'next-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber2'
+ timeout: 5000
+
+- tapOn:
+ id: 'prev-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber1'
+ timeout: 5000
+
+- tapOn:
+ id: 'start-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber0'
+ timeout: 5000
+
+- tapOn:
+ id: 'last-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber9'
+ timeout: 5000
+
+- tapOn:
+ id: 'remove-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber8'
+ timeout: 5000
+
+- tapOn:
+ id: 'add-page-button'
+
+- tapOn:
+ id: 'next-page-button'
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber9'
+ timeout: 5000
diff --git a/.maestro/flows/basic-pager/verify-horizontal-ltr-swipe.yaml b/.maestro/flows/basic-pager/verify-horizontal-ltr-swipe.yaml
new file mode 100644
index 00000000..22fc19da
--- /dev/null
+++ b/.maestro/flows/basic-pager/verify-horizontal-ltr-swipe.yaml
@@ -0,0 +1,29 @@
+appId: com.pagerviewexample
+---
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-horizontal'
+ start: 90%, 50%
+ end: 10%, 50%
+ duration: 100
+
+- assertVisible:
+ id: 'pageNumber0'
+
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-horizontal'
+ start: 90%, 50%
+ end: 10%, 50%
+ duration: 100
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber1'
+ timeout: 5000
diff --git a/.maestro/flows/basic-pager/verify-horizontal-rtl-swipe.yaml b/.maestro/flows/basic-pager/verify-horizontal-rtl-swipe.yaml
new file mode 100644
index 00000000..fd8aa15d
--- /dev/null
+++ b/.maestro/flows/basic-pager/verify-horizontal-rtl-swipe.yaml
@@ -0,0 +1,29 @@
+appId: com.pagerviewexample
+---
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-horizontal'
+ start: 10%, 50%
+ end: 90%, 50%
+ duration: 100
+
+- assertVisible:
+ id: 'pageNumber0'
+
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-horizontal'
+ start: 10%, 50%
+ end: 90%, 50%
+ duration: 100
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber1'
+ timeout: 5000
diff --git a/.maestro/flows/basic-pager/verify-vertical-swipe.yaml b/.maestro/flows/basic-pager/verify-vertical-swipe.yaml
new file mode 100644
index 00000000..a160d576
--- /dev/null
+++ b/.maestro/flows/basic-pager/verify-vertical-swipe.yaml
@@ -0,0 +1,27 @@
+appId: com.pagerviewexample
+---
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-vertical'
+ direction: UP
+ duration: 100
+
+- assertVisible:
+ id: 'pageNumber0'
+
+- tapOn:
+ id: 'scroll-enabled-button'
+
+- swipe:
+ from:
+ id: 'pager-view-vertical'
+ direction: UP
+ duration: 100
+
+- extendedWaitUntil:
+ visible:
+ id: 'pageNumber1'
+ timeout: 5000
diff --git a/.maestro/setup/pager_basic_example_setup.yaml b/.maestro/setup/pager_basic_example_setup.yaml
deleted file mode 100644
index 1389cdce..00000000
--- a/.maestro/setup/pager_basic_example_setup.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-appId: ${APP_ID}
-# Nest PagerView Example tab is active and accessible
----
-- launchApp
-- assertVisible: 'PagerView Example'
-- tapOn: 'Basic Example'
-- assertVisible: '7 likes'
diff --git a/.maestro/setup/pager_vertical_basic_example_setup.yaml b/.maestro/setup/pager_vertical_basic_example_setup.yaml
deleted file mode 100644
index 2d6ad62e..00000000
--- a/.maestro/setup/pager_vertical_basic_example_setup.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-appId: ${APP_ID}
-# Vertical Basic Example tab is active and accessible
----
-- launchApp
-- assertVisible: 'PagerView Example'
-- tapOn: 'Vertical Basic Example'
-- assertVisible: '7 likes'
diff --git a/.maestro/smoke-test.yaml b/.maestro/smoke-test.yaml
index 3a162642..62cb0f67 100644
--- a/.maestro/smoke-test.yaml
+++ b/.maestro/smoke-test.yaml
@@ -10,15 +10,15 @@ tags:
- extendedWaitUntil:
visible:
- id: 'Basic Example'
+ id: 'example-basic-horizontal'
timeout: 15000
- tapOn:
- id: 'Basic Example'
+ id: 'example-basic-horizontal'
- extendedWaitUntil:
visible:
- id: 'pager-view'
+ id: 'pager-view-horizontal'
timeout: 10000
- assertVisible:
diff --git a/.maestro/tests/pager_basic_example.yaml b/.maestro/tests/pager_basic_example.yaml
index 16b4faa0..d6bdc1c9 100644
--- a/.maestro/tests/pager_basic_example.yaml
+++ b/.maestro/tests/pager_basic_example.yaml
@@ -1,47 +1,12 @@
appId: com.pagerviewexample
---
-- runFlow: ../setup/pager_basic_example_setup.yaml
-- tapOn: 'Scroll Enabled'
-- swipe:
- from:
- id: 'pager-view'
- start: 90%, 50%
- end: 10%, 50%
- duration: 100
-- assertVisible:
- text: 'page number 0'
-- tapOn: 'Scroll Disabled'
-- swipe:
- from:
- id: 'pager-view'
- start: 90%, 50%
- end: 10%, 50%
- duration: 100
-- assertVisible:
- text: 'page number 1'
-- tapOn:
- id: 'next-page-button'
-- assertVisible:
- text: 'page number 2'
-- tapOn:
- id: 'prev-page-button'
-- assertVisible:
- text: 'page number 1'
-- tapOn:
- id: 'start-page-button'
-- assertVisible:
- text: 'page number 0'
-- tapOn:
- id: 'last-page-button'
-- assertVisible:
- text: 'page number 9'
-- tapOn:
- id: 'remove-page-button'
-- assertVisible:
- text: 'page number 8'
-- tapOn:
- id: 'add-page-button'
-- tapOn:
- id: 'next-page-button'
-- assertVisible:
- text: 'page number 9'
+- runFlow: ../flows/basic-pager/ensure-ltr.yaml
+
+- runFlow:
+ file: ../flows/basic-pager/open.yaml
+ env:
+ EXAMPLE_ID: 'example-basic-horizontal'
+ PAGER_ID: 'pager-view-horizontal'
+
+- runFlow: ../flows/basic-pager/verify-horizontal-ltr-swipe.yaml
+- runFlow: ../flows/basic-pager/verify-controls.yaml
diff --git a/.maestro/tests/pager_rtl_example.yaml b/.maestro/tests/pager_rtl_example.yaml
new file mode 100644
index 00000000..7448acd1
--- /dev/null
+++ b/.maestro/tests/pager_rtl_example.yaml
@@ -0,0 +1,12 @@
+appId: com.pagerviewexample
+---
+- runFlow: ../flows/basic-pager/ensure-rtl.yaml
+
+- runFlow:
+ file: ../flows/basic-pager/open.yaml
+ env:
+ EXAMPLE_ID: 'example-basic-horizontal'
+ PAGER_ID: 'pager-view-horizontal'
+
+- runFlow: ../flows/basic-pager/verify-horizontal-rtl-swipe.yaml
+- runFlow: ../flows/basic-pager/verify-controls.yaml
diff --git a/.maestro/tests/pager_vertical_basic_example.yaml b/.maestro/tests/pager_vertical_basic_example.yaml
index b8907646..2e02b221 100644
--- a/.maestro/tests/pager_vertical_basic_example.yaml
+++ b/.maestro/tests/pager_vertical_basic_example.yaml
@@ -1,45 +1,12 @@
appId: com.pagerviewexample
---
-- runFlow: ../setup/pager_vertical_basic_example_setup.yaml
-- tapOn: 'Scroll Enabled'
-- swipe:
- from:
- id: 'pager-view'
- direction: UP
- duration: 100
-- assertVisible:
- text: 'page number 0'
-- tapOn: 'Scroll Disabled'
-- swipe:
- from:
- id: 'pager-view'
- direction: UP
- duration: 100
-- assertVisible:
- text: 'page number 1'
-- tapOn:
- id: 'next-page-button'
-- assertVisible:
- text: 'page number 2'
-- tapOn:
- id: 'prev-page-button'
-- assertVisible:
- text: 'page number 1'
-- tapOn:
- id: 'start-page-button'
-- assertVisible:
- text: 'page number 0'
-- tapOn:
- id: 'last-page-button'
-- assertVisible:
- text: 'page number 9'
-- tapOn:
- id: 'remove-page-button'
-- assertVisible:
- text: 'page number 8'
-- tapOn:
- id: 'add-page-button'
-- tapOn:
- id: 'next-page-button'
-- assertVisible:
- text: 'page number 9'
+- runFlow: ../flows/basic-pager/ensure-ltr.yaml
+
+- runFlow:
+ file: ../flows/basic-pager/open.yaml
+ env:
+ EXAMPLE_ID: 'example-basic-vertical'
+ PAGER_ID: 'pager-view-vertical'
+
+- runFlow: ../flows/basic-pager/verify-vertical-swipe.yaml
+- runFlow: ../flows/basic-pager/verify-controls.yaml
diff --git a/.maestro/tests/rtl/pager_basic_example_rtl.yaml b/.maestro/tests/rtl/pager_basic_example_rtl.yaml
deleted file mode 100644
index 933adb73..00000000
--- a/.maestro/tests/rtl/pager_basic_example_rtl.yaml
+++ /dev/null
@@ -1,115 +0,0 @@
-appId: com.pagerviewexample
----
-- launchApp
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- runFlow:
- when:
- visible: 'LTR'
- commands:
- - tapOn: 'LTR'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- extendedWaitUntil:
- visible:
- text: 'RTL'
- timeout: 15000
-
-- tapOn: 'Basic Example'
-
-- extendedWaitUntil:
- visible:
- id: 'pager-view'
- timeout: 10000
-
-- assertVisible: '7 likes'
-
-- tapOn: 'Scroll Enabled'
-
-- swipe:
- from:
- id: 'pager-view'
- start: 10%, 50%
- end: 90%, 50%
- duration: 100
-
-- assertVisible:
- text: 'page number 0'
-
-- tapOn: 'Scroll Disabled'
-
-- swipe:
- from:
- id: 'pager-view'
- start: 10%, 50%
- end: 90%, 50%
- duration: 100
-
-- assertVisible:
- text: 'page number 1'
-
-- tapOn:
- id: 'next-page-button'
-
-- assertVisible:
- text: 'page number 2'
-
-- tapOn:
- id: 'prev-page-button'
-
-- assertVisible:
- text: 'page number 1'
-
-- tapOn:
- id: 'start-page-button'
-
-- assertVisible:
- text: 'page number 0'
-
-- tapOn:
- id: 'last-page-button'
-
-- assertVisible:
- text: 'page number 9'
-
-- tapOn:
- id: 'remove-page-button'
-
-- assertVisible:
- text: 'page number 8'
-
-- tapOn:
- id: 'add-page-button'
-
-- tapOn:
- id: 'next-page-button'
-
-- assertVisible:
- text: 'page number 9'
-
-- tapOn: 'Navigate up'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 10000
-
-- tapOn: 'RTL'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- extendedWaitUntil:
- visible:
- text: 'LTR'
- timeout: 15000
diff --git a/.maestro/tests/rtl/pager_vertical_basic_example_rtl.yaml b/.maestro/tests/rtl/pager_vertical_basic_example_rtl.yaml
deleted file mode 100644
index d75a47ee..00000000
--- a/.maestro/tests/rtl/pager_vertical_basic_example_rtl.yaml
+++ /dev/null
@@ -1,113 +0,0 @@
-appId: com.pagerviewexample
----
-- launchApp
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- runFlow:
- when:
- visible: 'LTR'
- commands:
- - tapOn: 'LTR'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- extendedWaitUntil:
- visible:
- text: 'RTL'
- timeout: 15000
-
-- tapOn: 'Vertical Basic Example'
-
-- extendedWaitUntil:
- visible:
- id: 'pager-view'
- timeout: 10000
-
-- assertVisible: '7 likes'
-
-- tapOn: 'Scroll Enabled'
-
-- swipe:
- from:
- id: 'pager-view'
- direction: UP
- duration: 100
-
-- assertVisible:
- text: 'page number 0'
-
-- tapOn: 'Scroll Disabled'
-
-- swipe:
- from:
- id: 'pager-view'
- direction: UP
- duration: 100
-
-- assertVisible:
- text: 'page number 1'
-
-- tapOn:
- id: 'next-page-button'
-
-- assertVisible:
- text: 'page number 2'
-
-- tapOn:
- id: 'prev-page-button'
-
-- assertVisible:
- text: 'page number 1'
-
-- tapOn:
- id: 'start-page-button'
-
-- assertVisible:
- text: 'page number 0'
-
-- tapOn:
- id: 'last-page-button'
-
-- assertVisible:
- text: 'page number 9'
-
-- tapOn:
- id: 'remove-page-button'
-
-- assertVisible:
- text: 'page number 8'
-
-- tapOn:
- id: 'add-page-button'
-
-- tapOn:
- id: 'next-page-button'
-
-- assertVisible:
- text: 'page number 9'
-
-- tapOn: 'Navigate up'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 10000
-
-- tapOn: 'RTL'
-
-- extendedWaitUntil:
- visible:
- text: 'PagerView Example'
- timeout: 15000
-
-- extendedWaitUntil:
- visible:
- text: 'LTR'
- timeout: 15000
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 5883964c..b3e8491d 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -43,14 +43,22 @@ function VerticalBasicPagerViewExampleScreen() {
return ;
}
-const examples = [
+type Example = {
+ component: React.ComponentType;
+ name: string;
+ testID?: string;
+};
+
+const examples: Example[] = [
{
component: BasicPagerViewExampleScreen,
name: 'Basic Example',
+ testID: 'example-basic-horizontal',
},
{
component: VerticalBasicPagerViewExampleScreen,
name: 'Vertical Basic Example',
+ testID: 'example-basic-vertical',
},
{ component: OnPageScrollExample, name: 'OnPageScroll Example' },
@@ -80,14 +88,14 @@ const examples = [
},
];
-const tabViewExamples = [
+const tabViewExamples: Example[] = [
{ component: MaterialTopBarExample, name: 'MaterialTopBarExample' },
{ component: TabBarIconExample, name: 'TabBarIconExample' },
{ component: CustomTabBarExample, name: 'CustomTabBarExample' },
{ component: CoverflowExample, name: 'CoverflowExample' },
];
-const additionalExamples = [
+const additionalExamples: Example[] = [
{ component: PagerHookExample, name: 'Pager Hook Example' },
{ component: KeyboardExample, name: 'Keyboard Example' },
{ component: HeadphonesCarouselExample, name: 'Headphones Carousel Example' },
@@ -108,7 +116,7 @@ function App() {
{examples.map((example) => (
{
//@ts-ignore
@@ -192,6 +200,9 @@ export function Navigation() {
))}
+ Github Issues Examples
+ {ghIssues.map((example) => (
+ {
+ //@ts-ignore
+ navigation.navigate(example.name);
+ }}
+ >
+ {example.name}
+
+ ))}
);
}
diff --git a/example/src/Issue1098NestedPagerRepro.tsx b/example/src/Issue1098NestedPagerRepro.tsx
new file mode 100644
index 00000000..10b859a4
--- /dev/null
+++ b/example/src/Issue1098NestedPagerRepro.tsx
@@ -0,0 +1,145 @@
+import React, { useState } from 'react';
+import { Button, StyleSheet, Text, View } from 'react-native';
+import PagerView from 'react-native-pager-view';
+
+/**
+ * Reproduces https://github.com/callstack/react-native-pager-view/issues/1098
+ *
+ * iOS only. The action changes the outer pager's child count, which rebuilds
+ * its SwiftUI TabView while the nested pagers' native views are retained. In
+ * affected builds this leaves an inner hosting controller parented to a stale
+ * PageChildViewController and UIKit later terminates with
+ * UIViewControllerHierarchyInconsistency.
+ */
+export function Issue1098NestedPagerRepro() {
+ const [hasExtraOuterPage, setHasExtraOuterPage] = useState(false);
+ const [refreshCount, setRefreshCount] = useState(0);
+
+ const refreshOuterPager = () => {
+ setHasExtraOuterPage((value) => !value);
+ setRefreshCount((count) => count + 1);
+ };
+
+ return (
+
+
+ Issue #1098: nested pagers
+
+ Tap “Refresh outer pages”, then swipe between the outer pages. Repeat
+ a few times. On affected iOS builds, this eventually crashes with
+ UIViewControllerHierarchyInconsistency.
+
+
+ Refreshes: {refreshCount}
+
+
+
+ {[
+
+ Outer page: nested pagers
+
+
+
+
+ ,
+
+ Outer page 2
+ Swipe back to the nested page after each refresh.
+ ,
+ ...(hasExtraOuterPage
+ ? [
+
+ Conditional outer page
+
+ Its insertion/removal forces the outer SwiftUI TabView to
+ rebuild.
+
+ ,
+ ]
+ : []),
+ ]}
+
+
+ );
+}
+
+function InnerPage({ color, label }: { color: string; label: string }) {
+ return (
+
+ {label}
+ Swipe horizontally inside this nested PagerView.
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ controls: {
+ padding: 16,
+ gap: 8,
+ },
+ heading: {
+ fontSize: 20,
+ fontWeight: '600',
+ },
+ description: {
+ color: '#444',
+ lineHeight: 20,
+ },
+ counter: {
+ color: '#555',
+ },
+ outerPager: {
+ flex: 1,
+ },
+ outerPage: {
+ flex: 1,
+ padding: 16,
+ backgroundColor: '#f5f5f5',
+ },
+ secondPage: {
+ flex: 1,
+ padding: 16,
+ backgroundColor: '#d7f9e9',
+ },
+ extraPage: {
+ flex: 1,
+ padding: 16,
+ backgroundColor: '#fff2cc',
+ },
+ innerPager: {
+ flex: 1,
+ marginTop: 16,
+ },
+ innerPage: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 16,
+ },
+ pageTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+});
diff --git a/example/src/Issue1099SafeAreaRepro.tsx b/example/src/Issue1099SafeAreaRepro.tsx
new file mode 100644
index 00000000..7af682fa
--- /dev/null
+++ b/example/src/Issue1099SafeAreaRepro.tsx
@@ -0,0 +1,145 @@
+import React from 'react';
+import { ScrollView, StyleSheet, Text, View } from 'react-native';
+import PagerView from 'react-native-pager-view';
+
+import { IMAGE_URIS } from './utils';
+
+const CARD_HEIGHT = 500;
+
+/**
+ * Reproduces https://github.com/callstack/react-native-pager-view/issues/1099
+ *
+ * On iOS with Fabric and pager-view >= 8.0.4, scroll the first tab vertically.
+ * The image in the nested pager can appear pinned while the card border and page
+ * indicator continue moving. This happens because the page controller copies the
+ * outer ScrollView content view's changing safe-area inset.
+ */
+export function Issue1099SafeAreaRepro() {
+ return (
+
+
+ Issue #1099: nested pager safe area
+
+ Scroll this tab. On affected iOS Fabric builds, the image may stop
+ moving with its card temporarily while the card border and indicator
+ continue scrolling.
+
+
+
+
+
+
+ {IMAGE_URIS.slice(0, 3).map((uri, index) => (
+
+
+ TOP
+ BOTTOM
+
+
+ ))}
+
+
+ Swipe images horizontally
+
+
+
+ {Array.from({ length: 8 }, (_, index) => (
+
+ Scrollable content {index + 1}
+
+ ))}
+
+
+
+ Second tab
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tabs: {
+ flex: 1,
+ },
+ tab: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ scrollContent: {
+ padding: 16,
+ paddingBottom: 48,
+ },
+ heading: {
+ fontSize: 20,
+ fontWeight: '600',
+ },
+ description: {
+ marginTop: 8,
+ color: '#444',
+ lineHeight: 20,
+ },
+ topSpacer: {
+ height: 180,
+ },
+ card: {
+ height: CARD_HEIGHT,
+
+ borderWidth: 3,
+ borderColor: '#5b5bd6',
+ backgroundColor: 'white',
+ },
+ imagePager: {
+ height: CARD_HEIGHT - 48,
+ },
+ imagePage: {
+ flex: 1,
+ },
+ image: {
+ width: '100%',
+ height: '100%',
+ },
+ indicator: {
+ height: 42,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#e5e5ff',
+ },
+ indicatorText: {
+ fontWeight: '600',
+ },
+ row: {
+ height: 120,
+ marginTop: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 12,
+ backgroundColor: 'white',
+ },
+ secondTab: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
diff --git a/ios/PagerViewProvider.swift b/ios/PagerViewProvider.swift
index 6457b0a5..644a0575 100644
--- a/ios/PagerViewProvider.swift
+++ b/ios/PagerViewProvider.swift
@@ -91,6 +91,25 @@ import UIKit
}
}
+ override public func willMove(toWindow newWindow: UIWindow?) {
+ super.willMove(toWindow: newWindow)
+ if newWindow != nil {
+ syncParentViewController()
+ }
+ }
+
+ override public func willMove(toSuperview newSuperview: UIView?) {
+ if let parentViewController = newSuperview?.reactViewController() {
+ syncParentViewController(to: parentViewController)
+ }
+ super.willMove(toSuperview: newSuperview)
+ }
+
+ override public func didMoveToSuperview() {
+ super.didMoveToSuperview()
+ syncParentViewController()
+ }
+
override public func layoutSubviews() {
super.layoutSubviews()
if window != nil {
@@ -110,6 +129,7 @@ import UIKit
private func setupView() {
if self.hostingController != nil {
+ syncParentViewController()
return
}
@@ -131,4 +151,20 @@ import UIKit
hostingController.didMove(toParent: parentViewController)
}
+
+ /// SwiftUI can recreate the page controller around this React view while
+ /// preserving the view itself. Keep the hosting controller attached to the
+ /// current page controller so UIKit's view-controller hierarchy stays valid.
+ private func syncParentViewController(to parentViewController: UIViewController? = nil) {
+ guard let hostingController,
+ let parentViewController = parentViewController ?? reactViewController(),
+ hostingController.parent !== parentViewController else {
+ return
+ }
+
+ hostingController.willMove(toParent: nil)
+ hostingController.removeFromParent()
+ parentViewController.addChild(hostingController)
+ hostingController.didMove(toParent: parentViewController)
+ }
}