Skip to content

[iOS] Nested PagerView crashes with UIViewControllerHierarchyInconsistency (8.0.4) #1098

Description

@ullavans

Describe the bug

On iOS, a PagerView rendered inside another PagerView's page crashes the app with UIViewControllerHierarchyInconsistency. It is not immediate — it fires the next time that subtree moves to a window, so it typically lands seconds or minutes into a session, after the outer pager rebuilds.

*** Terminating app due to uncaught exception 'UIViewControllerHierarchyInconsistency',
reason: 'child view controller:<_TtGC7SwiftUI19UIHostingControllerV23react_native_pager_view9PagerView_: 0x454869900>
should have parent view controller:<react_native_pager_view.PageChildViewController: 0x4548d6400>
but actual parent is:<react_native_pager_view.PageChildViewController: 0x427f5f400>'

3   UIKitCore  -[UIView(Hierarchy) _associatedViewControllerForwardsAppearanceCallbacks:performHierarchyCheck:isRoot:] + 228
4   UIKitCore  -[UIView(Hierarchy) _willMoveToWindow:withAncestorView:] + 236
...
24  SwiftUI    _UIHostingView.swiftui_insertManagedSubview(_:at:) + 68
26  SwiftUICore DisplayList.ViewUpdater.updateItemView(container:from:localState:) + 228

Root cause

PagerViewProvider.setupView() parents its UIHostingController to reactViewController() exactly once, then returns early forever after:

private func setupView() {
  if self.hostingController != nil {
    return          // <- never re-parents
  }
  guard let parentViewController = reactViewController() else { return }
  ...
  parentViewController.addChild(hostingController)

But the view controller it parents to is not stable. Each page is wrapped by RepresentableView in a PageChildViewController, and SwiftUI discards and recreates those whenever the enclosing TabView rebuilds. Two things guarantee such rebuilds:

  • IdentifiablePlatformView.id is a fresh UUID() per insert, so a re-mounted child gets a new SwiftUI identity even though the underlying UIView instance is reused.
  • PagerView applies .id(props.children.count), and insertChild(_:atIndex:) is called once per child, so the count churns while children mount.

The React Native views survive those rebuilds; the hosting controller's parent does not. It keeps pointing at a discarded PageChildViewController while its view now lives under a fresh one, and UIKit's hierarchy check aborts on the next move-to-window.

Steps to reproduce

  1. Render a PagerView whose pages each contain another PagerView (in our app: a tab pager whose pages hold card carousels).
  2. Let the inner pagers mount and the outer pager's children insert/re-mount — data loading into the pages is enough.
  3. Swipe between outer pages.

Crash reproduces on the simulator and on device.

Environment

  • react-native-pager-view 8.0.4
  • React Native 0.83.6, New Architecture (Fabric)
  • Expo SDK 55
  • iOS 26.4 simulator, Xcode 26
  • SwiftUI 7.5.3

Suggested fix

Re-parent the hosting controller whenever the ancestor view controller has changed, rather than parenting once and never again. willMove(toWindow:) is the important call site — it runs top-down, so the provider fixes its parenting before UIKit reaches the hosting controller's own check.

+  override public func willMove(toWindow newWindow: UIWindow?) {
+    super.willMove(toWindow: newWindow)
+    if newWindow != nil {
+      syncParentViewController()
+    }
+  }
+
+  override public func didMoveToSuperview() {
+    super.didMoveToSuperview()
+    syncParentViewController()
+  }
+
+  private func syncParentViewController() {
+    guard let hostingController = self.hostingController,
+          let parentViewController = reactViewController(),
+          hostingController.parent !== parentViewController else {
+      return
+    }
+
+    hostingController.willMove(toParent: nil)
+    hostingController.removeFromParent()
+    parentViewController.addChild(hostingController)
+    hostingController.didMove(toParent: parentViewController)
+  }
+
   private func setupView() {
     if self.hostingController != nil {
+      syncParentViewController()
       return
     }

We have been running this as a patch-package patch and the crash is gone; the app has otherwise been on 8.0.4 unchanged. Happy to open it as a PR if the approach looks right to you.

(#167 reported the same exception years ago on the old UIKit implementation — different code path, since the SwiftUI rewrite introduced PageChildViewController.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions