From d674e8536d0c41ad0cf60f1994701ec986cb13b1 Mon Sep 17 00:00:00 2001
From: Alies Lapatsin <150333538+lptn@users.noreply.github.com>
Date: Wed, 9 Sep 2026 13:13:42 +0200
Subject: [PATCH] fix(ios): discard stale deferred picker selections
---
example/src/PickerIOSExample.tsx | 32 ++++++++++++++++++++-
ios/RNCPicker.mm | 49 ++++++++++++++++++++++++++++++--
2 files changed, 77 insertions(+), 4 deletions(-)
diff --git a/example/src/PickerIOSExample.tsx b/example/src/PickerIOSExample.tsx
index 842d4a048..25b2ef2c3 100644
--- a/example/src/PickerIOSExample.tsx
+++ b/example/src/PickerIOSExample.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import {Text, StyleSheet, View} from 'react-native';
+import {Button, Text, StyleSheet, View} from 'react-native';
import {PickerIOS} from '@react-native-picker/picker';
const CAR_MAKES_AND_MODELS = {
@@ -127,6 +127,32 @@ function PickerExample() {
);
}
+function ControlledPickerRapidUpdatesExample() {
+ const [value, setValue] = React.useState('first');
+
+ return (
+
+ Rapidly alternate the two buttons while the wheel is moving.
+
+ );
+}
+
function PickerStyleExample() {
const [carMake, setCarMake] = React.useState('cadillac');
@@ -156,6 +182,10 @@ export const examples = [
title: '',
render: PickerExample,
},
+ {
+ title: ' with rapid controlled updates',
+ render: ControlledPickerRapidUpdatesExample,
+ },
{
title: ' with custom styling',
render: PickerStyleExample,
diff --git a/ios/RNCPicker.mm b/ios/RNCPicker.mm
index 9a9405646..23a7c5f0b 100644
--- a/ios/RNCPicker.mm
+++ b/ios/RNCPicker.mm
@@ -14,6 +14,7 @@
#import
@interface RNCPicker()
+@property (nonatomic, assign) NSUInteger selectionGeneration;
@end
@implementation RNCPicker
@@ -39,8 +40,26 @@ - (instancetype)initWithFrame:(CGRect)frame
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
+// Selection is deferred so UIPickerView can finish processing the props update.
+// A native callback or reload may establish a newer selection before this runs.
+- (void)scheduleSelectionOfRow:(NSInteger)row
+ animated:(BOOL)animated
+ generation:(NSUInteger)generation
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (self.selectionGeneration == generation) {
+ [self selectRow:row inComponent:0 animated:animated];
+ }
+ });
+}
+
- (void)setItems:(NSArray *)items
{
+ if ([_items isEqualToArray:items]) {
+ return;
+ }
+
+ _selectionGeneration += 1;
_items = [items copy];
[self setNeedsLayout];
}
@@ -50,14 +69,32 @@ - (void)setSelectedIndex:(NSInteger)selectedIndex
if (_selectedIndex != selectedIndex) {
BOOL animated = _selectedIndex != NSNotFound; // Don't animate the initial value
_selectedIndex = selectedIndex;
- dispatch_async(dispatch_get_main_queue(), ^{
- [self selectRow:selectedIndex inComponent:0 animated:animated];
- });
+ NSUInteger selectionGeneration = ++_selectionGeneration;
+ [self scheduleSelectionOfRow:selectedIndex
+ animated:animated
+ generation:selectionGeneration];
+ }
+}
+
+- (void)reloadAllComponents
+{
+ // Re-queue the latest known selection after the reload without animation.
+ NSUInteger selectionGeneration = ++_selectionGeneration;
+ [super reloadAllComponents];
+
+ if (_selectedIndex != NSNotFound && _selectedIndex < (NSInteger)_items.count) {
+ [self scheduleSelectionOfRow:_selectedIndex
+ animated:NO
+ generation:selectionGeneration];
}
}
- (void)setNumberOfLines:(NSInteger)numberOfLines
{
+ if (_numberOfLines == numberOfLines) {
+ return;
+ }
+
_numberOfLines = numberOfLines;
[self reloadAllComponents];
[self setNeedsLayout];
@@ -65,6 +102,10 @@ - (void)setNumberOfLines:(NSInteger)numberOfLines
- (void) setFont:(UIFont *)font
{
+ if ([_font isEqual:font]) {
+ return;
+ }
+
_font = font;
[self reloadAllComponents];
[self setNeedsLayout];
@@ -135,6 +176,7 @@ - (UIView *)pickerView:(UIPickerView *)pickerView
- (void)pickerView:(__unused UIPickerView *)pickerView
didSelectRow:(NSInteger)row inComponent:(__unused NSInteger)component
{
+ _selectionGeneration += 1;
_selectedIndex = row;
if (_onChange && _items.count > (NSUInteger)row) {
_onChange(@{
@@ -162,6 +204,7 @@ - (void)pickerView:(__unused UIPickerView *)pickerView
didSelectRow:(NSInteger)row inComponent:(__unused NSInteger)component
withEventEmitter:(facebook::react::SharedViewEventEmitter)eventEmitter
{
+ _selectionGeneration += 1;
_selectedIndex = row;
if (eventEmitter != nullptr && _items.count > (NSUInteger)row) {
std::dynamic_pointer_cast(eventEmitter)