Skip to content

Commit be64b6d

Browse files
authored
feat(iOS, Tabs): partially implement RFC-1028 for Tabs on iOS (#3781)
## Description Partially implements [RFC-1028](#3702) for the Tabs component on iOS. This mirrors what was already done for Android in #3776, adapting the iOS native implementation to the new communication model. Two things are explicitly out of scope for this PR and will be addressed in follow-up PRs: - **State conflict resolution** logic (as described in RFC-1028) - **`moreNavigationController` support** — it is currently handicapped and will be brought up to speed separately Related to #3702. ## Changes ### iOS (native) - **Introduced `RNSTabsNavigationState`**: A new struct/class that follows the RFC-1028 model — `selectedScreenKey` + `provenance`. It carries the information about which tab is selected and whether the change originated from the native side or from JS. - **Refactored container update logic**: JS-initiated and native-initiated tab changes now have separate entry paths that converge into a common set of helper methods to update the state. Native navigation is slightly special — `UITabBarController` manages `selectedViewController` internally, so the state update path for native-initiated changes differs slightly from the JS-initiated path. - **Moved `UITabBarControllerDelegate` methods into `RNSTabBarController`**: Previously handled by an external `RNSTabBarControllerDelegate`. Since the controller needs to update navigation state on user actions (which only the delegate is aware of), inlining the delegate into the controller is a cleaner fit. The external `RNSTabBarControllerDelegate` class has been removed entirely. - **Removed `RNSTabBarControllerSelectionObserving` protocol**: No longer needed after the delegate consolidation. - **Replaced `OnNativeFocusChange` with `OnTabChange` event**: The new event follows RFC-1028 and carries the extended `RNSTabsNavigationState` payload. Aligns with the Android implementation from #3776. - **`TabsScreen` no longer exposes `isFocused`**: Screen-level selection awareness has been removed on iOS, consistent with the Android changes. - **Renamed `needsUpdateOfReactChildrenControllers` → `needsUpdateOfChildViewControllers`**: Cleanup rename for accuracy. ### JS / TypeScript - **Updated `TabsHost.ios.tsx`**: Wired up the new `navState` prop and `onTabChange` event, aligning iOS JS bindings with the updated native interface and with the Android implementation. - **Updated `TabsHostIOSNativeComponent.ts`**: Reflects the new native props and events (`navState`, `onTabChange`, removal of `onNativeFocusChange`). - **Removed `isFocused` from `TabsScreen.ios.tsx`**: Consistent with the screen no longer managing its own selection state. ## Test plan - Tested manually using the existing tabs example app on iOS. - Verified basic tab navigation works for both native-initiated (user tap) and JS-initiated tab changes. ## Checklist - [x] Included code example that can be used to test this change. - [ ] Updated / created local changelog entries in relevant test files. - [ ] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] For API changes, updated relevant public types. - [ ] Ensured that CI passes
1 parent 6e825ea commit be64b6d

17 files changed

Lines changed: 457 additions & 242 deletions

ios/tabs/host/RNSTabBarController.h

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#import <UIKit/UIKit.h>
44
#import "RNSTabBarAppearanceCoordinator.h"
5+
#import "RNSTabsNavigationState.h"
56
#import "RNSTabsScreenViewController.h"
67

78
#if !TARGET_OS_TV
@@ -41,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
4142
* Get reference to the host component view that owns this tab bar controller.
4243
*
4344
* Might return null in cases where the controller view hierararchy is not attached to parent.
45+
* The reference is retained strongly and it is expected to be managed by `TabsHost`.
4446
*/
4547
@property (nonatomic, readonly, nullable) RNSTabsHostComponentView *tabsHostComponentView;
4648

@@ -50,14 +52,24 @@ NS_ASSUME_NONNULL_BEGIN
5052
*/
5153
@property (nonatomic, readonly, strong, nonnull) RNSTabBarAppearanceCoordinator *tabBarAppearanceCoordinator;
5254

55+
/**
56+
* Represents current navigation state.
57+
*
58+
* After each model update, the container (controller) updates the navigation state. The `provenance` part is
59+
* incremented monotonically with each state update.
60+
*
61+
* The controller manages this state. It MUST NOT be overwritten by any external actor.
62+
*/
63+
@property (nonatomic, readonly, strong, nullable) RNSTabsNavigationState *navigationState;
64+
5365
/**
5466
* Update tab controller state with previously provided children.
5567
*
5668
* This method does nothing if the children have not been changed / update has not been requested before.
5769
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
5870
* invalidation signal & let the controller decide when to flush the updates.
5971
*/
60-
- (void)updateReactChildrenControllersIfNeeded;
72+
- (void)updateChildViewControllersIfNeeded;
6173

6274
/**
6375
* Force update of the tab controller state with previously provided children.
@@ -68,7 +80,7 @@ NS_ASSUME_NONNULL_BEGIN
6880
- (void)updateReactChildrenControllers;
6981

7082
/**
71-
* Find out which tab bar controller is currently focused & select it.
83+
* If any state update operation is pending - perform it.
7284
*
7385
* This method does nothing if the update has not been previously requested.
7486
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
@@ -77,7 +89,9 @@ NS_ASSUME_NONNULL_BEGIN
7789
- (void)updateSelectedViewControllerIfNeeded;
7890

7991
/**
80-
* Find out which tab bar controller is currently focused & select it.
92+
* Update the selected view controller to what's currently requested.
93+
*
94+
* To request update use `setPendingNavigationStateUpdate`.
8195
*
8296
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
8397
* invalidation signal & let the controller decide when to flush the updates.
@@ -161,18 +175,20 @@ NS_ASSUME_NONNULL_BEGIN
161175
- (void)childViewControllersHaveChangedTo:(nonnull NSArray<RNSTabsScreenViewController *> *)childViewControllers;
162176

163177
/**
164-
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
165-
* updated.
178+
* Request navigation state update from the controller to the given one.
166179
*
167-
* Do not raise this signal only when focused state of the tab has changed - use `needsSelectedTabUpdate` instead.
180+
* If you want to execute multiple updates in sequence you must flush the container after each one separately.
168181
*/
169-
@property (nonatomic, readwrite) bool needsUpdateOfReactChildrenControllers;
182+
- (void)setPendingNavigationStateUpdate:(nullable RNSTabsNavigationState *)navState;
170183

171184
/**
172185
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
173186
* updated.
187+
*
188+
* Do not raise this signal only when you want to modify selected view controller. Use `setPendingNavigationStateUpdate`
189+
* instead.
174190
*/
175-
@property (nonatomic, readwrite) bool needsUpdateOfSelectedTab;
191+
@property (nonatomic, readwrite) bool needsUpdateOfChildViewControllers;
176192

177193
/**
178194
* Tell the controller that some configuration regarding the tab bar apperance has changed & the appearance requires

0 commit comments

Comments
 (0)