Skip to content

[iOS] New Arch PagerView (SwiftUI TabView): firstIndex(of:) yields duplicate .tag → crash / wrong page #1079

@ios-youngsun-ban

Description

@ios-youngsun-ban

Description

In the new SwiftUI TabView-based iOS implementation (ios/PagerView.swift, 8.0.x), the tag index is derived with props.children.firstIndex(of:) while iterating that same array:

TabView(selection: $props.currentPage) {
  ForEach(props.children) { child in
    if let index = props.children.firstIndex(of: child) {
      RepresentableView(view: child.view)
        .tag(index)
    }
  }
}

firstIndex(of:) returns the index of the first element equal under Equatable, so when two entries in props.children compare equal, both resolve to the same index and multiple pages render with an identical .tag. With TabView(selection:), duplicate tags make the selection bind to the wrong page (page snaps back / won't switch) and, on iOS 17 and below, throw NSInternalInconsistencyException (hard crash). The lookup is also O(n²).

Note: The crash reproduces only on iOS 17 and below (verified on both a device and a simulator). On newer OS versions the duplicate tag does not crash.

Library version

react-native-pager-view 8.0.0 – 8.0.2 (current master)

React Native version

0.84.1 — New Architecture / Fabric, Hermes. (The faulty code path does not depend on the RN version.)

Platform

iOS only (new SwiftUI implementation). Crash occurs on iOS 17 and below.

Steps to reproduce

  1. Render a <PagerView> with multiple children whose backing items compare equal under the element's Equatable conformance.
  2. Swipe or programmatically change pages.
  3. On iOS 17 or below, this crashes with NSInternalInconsistencyException.

Expected

Each page gets a unique tag matching its position; switching works and never crashes.

Actual

Duplicate .tag values → wrong/sticky page selection, and on iOS 17 and below an NSInternalInconsistencyException crash.

Proposed fix

Use the positional index from enumerated() (always unique) and keep stable identity via the element id:

-      ForEach(props.children) { child in
-        if let index = props.children.firstIndex(of: child) {
-          RepresentableView(view: child.view)
-            .tag(index)
-        }
-      }
+      ForEach(Array(props.children.enumerated()), id: \.element.id) { index, child in
+        RepresentableView(view: child.view)
+          .tag(index)
+      }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions