From dcb526bdc7fb05bac419f4162c0b4aca98bf6336 Mon Sep 17 00:00:00 2001 From: Adam McDonald Date: Fri, 17 Jul 2026 11:56:22 -0700 Subject: [PATCH] fix(ios): skip programmatic selectRow while user is scrolling On the New Architecture (Fabric), a controlled Picker can crash with EXC_BAD_ACCESS during a fast fling. Every committed value flows through -setSelectedIndex:, which calls -selectRow:animated:. Mid-gesture, lagging React state echoes back a stale index that re-drives the live wheel, re-entering UIPickerView's suspended update cycle during UIKit hit-testing. Add an -isUserScrolling helper that inspects the picker's backing UIScrollView(s) (isTracking/isDragging/isDecelerating) and bail out of -setSelectedIndex: while the user is actively manipulating the wheel. -didSelectRow: already keeps the internal index accurate, so idle programmatic resets continue to work. --- ios/RNCPicker.mm | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ios/RNCPicker.mm b/ios/RNCPicker.mm index 9a9405646..388344581 100644 --- a/ios/RNCPicker.mm +++ b/ios/RNCPicker.mm @@ -45,8 +45,37 @@ - (void)setItems:(NSArray *)items [self setNeedsLayout]; } +- (BOOL)isUserScrolling +{ + // UIPickerView drives each wheel with an internal UIScrollView. Walk the + // subview tree and report whether any of them is currently being touched or + // is still settling after a fling. + NSMutableArray *stack = [NSMutableArray arrayWithArray:self.subviews]; + while (stack.count > 0) { + UIView *view = stack.lastObject; + [stack removeLastObject]; + if ([view isKindOfClass:[UIScrollView class]]) { + UIScrollView *scrollView = (UIScrollView *)view; + if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) { + return YES; + } + } + [stack addObjectsFromArray:view.subviews]; + } + return NO; +} + - (void)setSelectedIndex:(NSInteger)selectedIndex { + // While the user is physically manipulating the wheel, a controlled parent can + // echo back a stale selected index and re-drive selectRow: mid-gesture. Under + // the New Architecture this re-enters UIPickerView's suspended update cycle + // during hit-testing and crashes. didSelectRow: already keeps the internal + // index accurate, so skipping the programmatic update here is safe; idle resets + // still apply. + if ([self isUserScrolling]) { + return; + } if (_selectedIndex != selectedIndex) { BOOL animated = _selectedIndex != NSNotFound; // Don't animate the initial value _selectedIndex = selectedIndex;