Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion ios/tabs/bottom-accessory/RNSTabsBottomAccessoryComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,37 @@ - (void)initState
- (void)didMoveToWindow
{
if (self.window != nil) {
// The helper & shadow proxy are torn down below whenever the accessory
// leaves the window (e.g. a full-screen push covering the tab bar), so
// they must be recreated on re-entry — previously
// `registerForAccessoryFrameChanges` was messaged at nil here and all
// trait/frame callbacks and content-view switching stayed permanently
// dead after the first full-screen push, freezing the accessory on
// whichever content copy was last visible. The content views' own
// `didMoveToWindow` runs after this one (window changes propagate
// top-down), so they re-register into the fresh helper.
if (@available(iOS 26, *)) {
if (_helper == nil) {
_helper = [[RNSTabsBottomAccessoryHelper alloc] initWithBottomAccessoryView:self];
}
if (_shadowStateProxy == nil) {
_shadowStateProxy = [[RNSTabsBottomAccessoryShadowStateProxy alloc] initWithBottomAccessoryView:self];
}
}
[_helper registerForAccessoryFrameChanges];
} else {
[self invalidate];
// Lighter teardown than `invalidate` — keep the Fabric `_state` alive
// across a detach/reattach cycle. Only React can re-deliver the state
// (via `updateState` on a commit), so resetting it on a mere window
// detach would leave the recreated shadow proxy unable to publish
// accessory frame updates until an unrelated commit happens to arrive.
// True unmount still fully invalidates via RNSTabsHostComponentView.
if (@available(iOS 26, *)) {
[_helper invalidate];
_helper = nil;
[_shadowStateProxy invalidate];
_shadowStateProxy = nil;
}
}
}

Expand Down
51 changes: 32 additions & 19 deletions ios/tabs/bottom-accessory/RNSTabsBottomAccessoryHelper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ - (void)initState

#pragma mark - Content view switching workaround

- (BOOL)isContentViewSwitchingWorkaroundActive
{
return _regularContentView != nil && _inlineContentView != nil;
}

- (void)setContentView:(RNSTabsBottomAccessoryContentComponentView *)contentView
forEnvironment:(RNSTabsBottomAccessoryEnvironment)environment
{
Expand All @@ -65,20 +60,29 @@ - (void)setContentView:(RNSTabsBottomAccessoryContentComponentView *)contentView

- (void)handleContentViewVisibilityForEnvironmentIfNeeded
{
if (!self.isContentViewSwitchingWorkaroundActive) {
return;
}

switch (self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment) {
case UITabAccessoryEnvironmentInline:
_regularContentView.layer.opacity = 0.0;
_inlineContentView.layer.opacity = 1.0;
break;
default:
_regularContentView.layer.opacity = 1.0;
_inlineContentView.layer.opacity = 0.0;
break;
}
// `hidden` is used as the sole carrier of invisibility, with alpha
// normalized to 1.0 on both content views:
// - `RCTViewComponentView.invalidateLayer` unconditionally resets
// `layer.opacity` to the React props value on arbitrary commits, which
// used to re-show the off-environment copy. It never touches `hidden`,
// and with alpha kept at 1.0 the reset becomes a no-op by construction.
// - The off-environment copy is mounted absoluteFill ON TOP of the active
// one (later sibling); when it regained opacity it also captured all
// hit-testing, making the accessory unresponsive to taps. Hidden views
// are excluded from hit-testing.
// Applied to whichever content views are registered (messages to nil are
// no-ops) — the previous both-registered early-return left a lone
// registered view fully visible until its sibling registered.
BOOL isInline =
self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment == UITabAccessoryEnvironmentInline;

UIView *viewToShow = isInline ? _inlineContentView : _regularContentView;
UIView *viewToHide = isInline ? _regularContentView : _inlineContentView;

viewToShow.hidden = NO;
viewToShow.alpha = 1.0;
viewToHide.hidden = YES;
viewToHide.alpha = 1.0;
}

#pragma mark - Observing environment changes
Expand Down Expand Up @@ -149,6 +153,15 @@ - (void)notifyWrapperViewFrameHasChanged
// We use self.nativeWrapperView because it has both the size and the origin
// that we want to send to the ShadowNode.
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:self.nativeWrapperView.frame];

// The wrapper frame changes on every regular <-> inline move, so re-evaluate
// content-view visibility here as well. The one-shot
// UITraitTabAccessoryEnvironment registration callback is occasionally
// missed during the minimize transition, which left the wrong copy visible
// inside the inline accessory (or vice versa); this KVO fires reliably on
// each transition (and once on registration via
// NSKeyValueObservingOptionInitial) and self-corrects that.
[self handleContentViewVisibilityForEnvironmentIfNeeded];
}

#pragma mark - Invalidation
Expand Down