Skip to content

Commit

Permalink
fix(iOS): flickering custom header items (software-mansion#2247)
Browse files Browse the repository at this point in the history
## Description

This PR intents to fix flickering custom header items when going to a
previous screen on `fabric` architecture.
The items are unmounted before the transition happens when `POP` action
is dispatched on navigation from JS causing the items to vanish for a
moment.
The adopted solution uses snapshots of the custom items to be used until
the transition's done.

Fixes software-mansion#2243.

## Changes

- added snapshots of the custom header items
- modified `Test556` for repro

## Screenshots / GIFs

### Before


https://github.com/user-attachments/assets/9da060ed-b65e-4b32-9ab1-debfc2bfd02d


### After


https://github.com/user-attachments/assets/0413fab0-05f6-4e55-adab-f283e01bc551


## Test code and steps to reproduce

- Use `Test556` repro

## Checklist

- [x] Ensured that CI passes

---------

Co-authored-by: Kacper Kafara <kacperkafara@gmail.com>
  • Loading branch information
2 people authored and ja1ns committed Oct 9, 2024
1 parent 776561a commit d61bc24
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 35 deletions.
35 changes: 0 additions & 35 deletions apps/src/tests/Test556.js

This file was deleted.

70 changes: 70 additions & 0 deletions apps/src/tests/Test556.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

type ScreenBaseProps = {
navigation: NativeStackNavigationProp<ParamListBase>;
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
animation: 'fade',
}}>
<Stack.Screen name="First" component={First} options={{
headerTitle: () => (
<View style={[styles.container, { backgroundColor: 'goldenrod' }]}>
<Text>Hello there!</Text>
</View>
),
headerRight: () => (
<View style={[styles.container, { backgroundColor: 'lightblue' }]}>
<Text>Right-1</Text>
</View>
),
}} />
<Stack.Screen name="Second" component={Second} options={{
headerTitle: () => (
<View style={[styles.container, { backgroundColor: 'mediumseagreen' }]}>
<Text>General Kenobi</Text>
</View>
),
headerRight: () => (
<View style={[styles.container, { backgroundColor: 'mediumvioletred' }]}>
<Text>Right-2</Text>
</View>
),
}} />
</Stack.Navigator>
</NavigationContainer>
);
}

function First({ navigation }: ScreenBaseProps) {
return (
<Button
title="Tap me for second screen"
onPress={() => navigation.navigate('Second')}
/>
);
}

function Second({ navigation }: ScreenBaseProps) {
return (
<Button
title="Tap me for first screen"
onPress={() => navigation.popTo('First')}
/>
);
}

const styles = StyleSheet.create({
container: {
padding: 3,
},
});
31 changes: 31 additions & 0 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,8 @@ + (void)updateViewController:(UIViewController *)vc
navitem.titleView = nil;

for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
// This code should be kept in sync on Fabric with analogous switch statement in
// `- [RNSScreenStackHeaderConfig replaceNavigationBarViewsWithSnapshotOfSubview:]` method.
switch (subview.type) {
case RNSScreenStackHeaderSubviewTypeLeft: {
#if !TARGET_OS_TV
Expand Down Expand Up @@ -755,10 +757,39 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
// For explanation of why we can make a snapshot here despite the fact that our children are already
// unmounted see https://github.com/software-mansion/react-native-screens/pull/2261
[self replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView];
[_reactSubviews removeObject:(RNSScreenStackHeaderSubview *)childComponentView];
[childComponentView removeFromSuperview];
}

- (void)replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubview *)childComponentView
{
UINavigationItem *navitem = _screenView.controller.navigationItem;
UIView *snapshot = [childComponentView snapshotViewAfterScreenUpdates:NO];

// This code should be kept in sync with analogous switch statement in
// `+ [RNSScreenStackHeaderConfig updateViewController: withConfig: animated:]` method.
switch (childComponentView.type) {
case RNSScreenStackHeaderSubviewTypeLeft:
navitem.leftBarButtonItem.customView = snapshot;
break;
case RNSScreenStackHeaderSubviewTypeCenter:
case RNSScreenStackHeaderSubviewTypeTitle:
navitem.titleView = snapshot;
break;
case RNSScreenStackHeaderSubviewTypeRight:
navitem.rightBarButtonItem.customView = snapshot;
break;
case RNSScreenStackHeaderSubviewTypeSearchBar:
case RNSScreenStackHeaderSubviewTypeBackButton:
break;
default:
RCTLogError(@"[RNScreens] Unhandled subview type: %ld", childComponentView.type);
}
}

static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode)
{
switch (resizeMode) {
Expand Down

0 comments on commit d61bc24

Please sign in to comment.