Skip to content

Commit

Permalink
fix(iOS): restore old header animation to prevent content jump (#2563)
Browse files Browse the repository at this point in the history
## Description

Fixes #2550

Empirical research shows that UIKit uses
`interruptibleAnimatorForTransition:` method on
`UIViewAnimatedTransitioning` (our `RNSStackAnimator`)
for navigation item animation. This allowed us customizing navigation
item timing curve & animation style, however it has caused unexpected
content jumps (see #2550 or video below 👇).

> [!important]
> This PR reverts changes to navigation item animation introduced with
v4. I guess it could be considered breaking unless it weren't a bit
broken => I'm treating this as a necessary fix
> & will try to bring back "new behaviour" soon, once we figure why
`interruptibleAnimatorForTransition:` causes such bugs.


https://github.com/user-attachments/assets/000fa79d-b01a-4261-941b-c5922e0d17f6

After the changes this looks as follows:




https://github.com/user-attachments/assets/42870bab-304e-4a9d-b253-42d94c6159e7



## Changes

- **Add reproduction**
- **Do not override `interruptibleAnimatorForTransition:` preventing
content jump**

## Test code and steps to reproduce

`TestAnimation`

## Checklist

- [x] Included code example that can be used to test this change
- [ ] Ensured that CI passes
  • Loading branch information
kkafar authored Dec 10, 2024
1 parent afd5e72 commit 5571050
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 5 deletions.
104 changes: 104 additions & 0 deletions apps/src/tests/TestAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NavigationContainer, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp, createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { View } from 'react-native';
import { Button, Square } from '../shared';

type ParamList = {
Home: undefined;
Second: undefined;
Third: undefined;
Fourth: undefined;
Fifth: undefined;
}

type RoutePropBase<RouteName extends keyof ParamList> = {
navigation: NativeStackNavigationProp<ParamList>,
route: RouteProp<ParamList, RouteName>;
}

const Stack = createNativeStackNavigator<ParamList>();

function Contents(): React.ReactNode {
return (
<View>
<Square size={200} color="lightblue" />
</View>
);
}

function Home({ navigation }: RoutePropBase<'Home'>): React.ReactNode {
return (
<View style={{ flex: 1, backgroundColor: 'lightgreen', justifyContent: 'center', alignItems: 'center' }}>
<View style={{ width: '100%', height: 20, backgroundColor: 'red' }} />
<Button title="Go Second" onPress={() => navigation.navigate('Second')} />
<Contents />
</View>
);
}


function Second({ navigation }: RoutePropBase<'Second'>): React.ReactNode {
return (
<View style={{ flex: 1, backgroundColor: 'lightseagreen', justifyContent: 'center', alignItems: 'center' }}>
<Button title="Go Third" onPress={() => navigation.navigate('Third')} />
<Button title="Go back" onPress={() => navigation.popTo('Home')} />
<Contents />
</View>
);
}

function Third({ navigation }: RoutePropBase<'Third'>): React.ReactNode {
return (
<View style={{ flex: 1, backgroundColor: 'lightcoral', justifyContent: 'center', alignItems: 'center' }}>
<Button title="Go Fourth" onPress={() => navigation.navigate('Fourth')} />
<Button title="Go back" onPress={() => navigation.popTo('Second')} />
<Contents />
</View>
);
}

function Fourth({ navigation }: RoutePropBase<'Fourth'>): React.ReactNode {
return (
<View style={{ flex: 1, backgroundColor: 'orange', justifyContent: 'center', alignItems: 'center' }}>
<Button title="Go Fifth" onPress={() => navigation.navigate('Fifth')} />
<Button title="Go back" onPress={() => navigation.popTo('Second')} />
<Contents />
</View>
);
}

function Fifth({ navigation }: RoutePropBase<'Fifth'>): React.ReactNode {
return (
<View style={{ flex: 1, backgroundColor: 'pink', justifyContent: 'center', alignItems: 'center' }}>
<Button title="Go back" onPress={() => navigation.popTo('Fourth')} />
<Contents />
</View>
);
}

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{
fullScreenGestureEnabled: true,
animation: 'simple_push',
//animationMatchesGesture: true,
}}>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Second" component={Second} options={{
headerShown: true,
}}/>
<Stack.Screen name="Third" component={Third} options={{
headerShown: true,
}} />
<Stack.Screen name="Fourth" component={Fourth} options={{
headerShown: false,
}} />
<Stack.Screen name="Fifth" component={Fifth} options={{
headerShown: true,
}} />
</Stack.Navigator>
</NavigationContainer>
);
}
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,5 @@ export { default as TestModalNavigation } from './TestModalNavigation';
export { default as TestMemoryLeak } from './TestMemoryLeak';
export { default as TestFormSheet } from './TestFormSheet';
export { default as TestAndroidTransitions } from './TestAndroidTransitions';
export { default as TestAnimation } from './TestAnimation';

10 changes: 5 additions & 5 deletions ios/RNSScreenStackAnimator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)t
return _transitionDuration;
}

- (id<UIViewImplicitlyAnimating>)interruptibleAnimatorForTransition:
(id<UIViewControllerContextTransitioning>)transitionContext
{
return _inFlightAnimator;
}
//- (id<UIViewImplicitlyAnimating>)interruptibleAnimatorForTransition:
// (id<UIViewControllerContextTransitioning>)transitionContext
//{
// return _inFlightAnimator;
//}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
Expand Down

0 comments on commit 5571050

Please sign in to comment.