diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
index 03d2463a0b..f5178f3e4d 100644
--- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
@@ -234,6 +234,9 @@ class Screen(
if (activityState == this.activityState) {
return
}
+ if (container is ScreenStack && this.activityState != null && activityState < this.activityState!!) {
+ throw IllegalStateException("[RNScreens] activityState can only progress in NativeStack")
+ }
this.activityState = activityState
container?.notifyChildUpdate()
}
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
index a4e6fea83e..75453fa7ae 100644
--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
@@ -104,7 +104,7 @@ class ScreenStack(
for (i in screenWrappers.indices.reversed()) {
val screenWrapper = getScreenFragmentWrapperAt(i)
- if (!dismissedWrappers.contains(screenWrapper)) {
+ if (!dismissedWrappers.contains(screenWrapper) && screenWrapper.screen.activityState !== Screen.ActivityState.INACTIVE) {
if (newTop == null) {
newTop = screenWrapper
} else {
@@ -182,8 +182,16 @@ class ScreenStack(
R.anim.rns_no_animation_medium,
)
StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_fade_from_bottom, R.anim.rns_no_animation_350)
- StackAnimation.IOS_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_ios_from_right_foreground_open, R.anim.rns_ios_from_right_background_open)
- StackAnimation.IOS_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_ios_from_left_foreground_open, R.anim.rns_ios_from_left_background_open)
+ StackAnimation.IOS_FROM_RIGHT ->
+ it.setCustomAnimations(
+ R.anim.rns_ios_from_right_foreground_open,
+ R.anim.rns_ios_from_right_background_open,
+ )
+ StackAnimation.IOS_FROM_LEFT ->
+ it.setCustomAnimations(
+ R.anim.rns_ios_from_left_foreground_open,
+ R.anim.rns_ios_from_left_background_open,
+ )
}
} else {
when (stackAnimation) {
@@ -221,8 +229,16 @@ class ScreenStack(
R.anim.rns_slide_out_to_bottom,
)
StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_no_animation_250, R.anim.rns_fade_to_bottom)
- StackAnimation.IOS_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_ios_from_right_background_close, R.anim.rns_ios_from_right_foreground_close)
- StackAnimation.IOS_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_ios_from_left_background_close, R.anim.rns_ios_from_left_foreground_close)
+ StackAnimation.IOS_FROM_RIGHT ->
+ it.setCustomAnimations(
+ R.anim.rns_ios_from_right_background_close,
+ R.anim.rns_ios_from_right_foreground_close,
+ )
+ StackAnimation.IOS_FROM_LEFT ->
+ it.setCustomAnimations(
+ R.anim.rns_ios_from_left_background_close,
+ R.anim.rns_ios_from_left_foreground_close,
+ )
}
}
}
@@ -258,7 +274,9 @@ class ScreenStack(
break
}
// detach all screens that should not be visible
- if (fragmentWrapper !== newTop && !dismissedWrappers.contains(fragmentWrapper)) {
+ if ((fragmentWrapper !== newTop && !dismissedWrappers.contains(fragmentWrapper)) ||
+ fragmentWrapper.screen.activityState === Screen.ActivityState.INACTIVE
+ ) {
it.remove(fragmentWrapper.fragment)
}
}
diff --git a/apps/src/tests/TestActivityStateProgression.tsx b/apps/src/tests/TestActivityStateProgression.tsx
new file mode 100644
index 0000000000..045e1e16c5
--- /dev/null
+++ b/apps/src/tests/TestActivityStateProgression.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import { View, Text, Button } from 'react-native';
+import { Screen, ScreenStack } from '../../../src';
+
+function HomeScreen({
+ setActivityState,
+}: {
+ setActivityState: (state: 0 | 1 | 2) => void;
+}) {
+ console.log('Render home');
+ return (
+
+ Home!
+
+ );
+}
+
+function ProfileScreen({
+ setActivityState,
+}: {
+ setActivityState: (state: 0 | 1 | 2) => void;
+}) {
+ console.log('Render profile');
+ return (
+
+ Profile!
+
+ );
+}
+
+const App = () => {
+ const [activityState, setActivityState] = React.useState<0 | 1 | 2>(0);
+ console.log('activityState', activityState);
+
+ /**
+ * Remove isNativeStack if you want to test native checks
+ */
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+export default App;
diff --git a/apps/src/tests/TestPreload.tsx b/apps/src/tests/TestPreload.tsx
new file mode 100644
index 0000000000..ec67ff6f80
--- /dev/null
+++ b/apps/src/tests/TestPreload.tsx
@@ -0,0 +1,352 @@
+import * as React from 'react';
+
+import { View, Text, Button } from 'react-native';
+import {
+ useNavigation,
+ CommonActions,
+ NavigationContainer,
+} from '@react-navigation/native';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+
+function HomeScreen() {
+ const navigation = useNavigation();
+
+ return (
+
+ Home!
+
+ );
+}
+
+function ProfileScreen({ route }: any) {
+ const navigation = useNavigation();
+ const [startTime] = React.useState(Date.now());
+ const [endTime, setEndTime] = React.useState(null);
+
+ React.useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ setEndTime(Date.now());
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [navigation]);
+
+ return (
+
+ Profile!
+ {route.params.user}'s profile
+ Preloaded for: {endTime ? endTime - startTime : 'N/A'}ms
+
+ );
+}
+
+const Tab = createBottomTabNavigator();
+const StackTabA = createNativeStackNavigator();
+
+function TabA() {
+ return (
+
+
+
+
+ );
+}
+
+function TabA_1() {
+ const navigation = useNavigation();
+ const [startTime] = React.useState(Date.now());
+ const [endTime, setEndTime] = React.useState(null);
+
+ React.useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ setEndTime(Date.now());
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [navigation]);
+
+ return (
+
+ TabA_1!
+ Preloaded for: {endTime ? endTime - startTime : 'N/A'}ms
+ {
+ navigation.dispatch(
+ CommonActions.preload('TabA_2', { user: 'jane' }),
+ );
+ }}
+ />
+ {
+ navigation.dispatch(
+ CommonActions.navigate('TabA_2', { user: 'jane' }),
+ );
+ }}
+ />
+
+ );
+}
+
+function TabA_2() {
+ const navigation = useNavigation();
+ const [startTime] = React.useState(Date.now());
+ const [endTime, setEndTime] = React.useState(null);
+
+ React.useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ setEndTime(Date.now());
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [navigation]);
+
+ return (
+
+ TabA_2!
+ Preloaded for: {endTime ? endTime - startTime : 'N/A'}ms
+ {
+ navigation.dispatch(
+ CommonActions.preload('ModalProfile', { user: 'jane' }),
+ );
+ }}
+ />
+ {
+ navigation.dispatch(
+ CommonActions.navigate('ModalProfile', { user: 'jane' }),
+ );
+ }}
+ />
+
+ );
+}
+
+const StackTabB = createNativeStackNavigator();
+
+function TabB() {
+ return (
+
+
+
+
+ );
+}
+
+function TabB_1() {
+ const navigation = useNavigation();
+ const [startTime] = React.useState(Date.now());
+ const [endTime, setEndTime] = React.useState(null);
+
+ React.useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ setEndTime(Date.now());
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [navigation]);
+
+ return (
+
+ TabB_1!
+ Preloaded for: {endTime ? endTime - startTime : 'N/A'}ms
+ {
+ navigation.dispatch(
+ CommonActions.preload('TabB_2', { user: 'jane' }),
+ );
+ }}
+ />
+ {
+ navigation.dispatch(
+ CommonActions.navigate('TabB_2', { user: 'jane' }),
+ );
+ }}
+ />
+
+ );
+}
+
+function TabB_2() {
+ const navigation = useNavigation();
+ const [startTime] = React.useState(Date.now());
+ const [endTime, setEndTime] = React.useState(null);
+
+ React.useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ setEndTime(Date.now());
+ });
+
+ return () => {
+ unsubscribe();
+ };
+ }, [navigation]);
+
+ return (
+
+ TabB_2!
+ Preloaded for: {endTime ? endTime - startTime : 'N/A'}ms
+ {
+ navigation.dispatch(
+ CommonActions.preload('ModalProfile', { user: 'jane' }),
+ );
+ }}
+ />
+ {
+ navigation.dispatch(
+ CommonActions.navigate('ModalProfile', { user: 'jane' }),
+ );
+ }}
+ />
+
+ );
+}
+
+function TabScreen({ route }: any) {
+ console.log('route?.params?.user', route?.params?.user);
+
+ return (
+
+
+
+
+ );
+}
+
+const Stack = createNativeStackNavigator();
+
+const App = () => (
+
+
+
+
+
+
+
+
+
+);
+
+export default App;
diff --git a/apps/src/tests/index.ts b/apps/src/tests/index.ts
index cb516cbdd6..20c3a880ee 100644
--- a/apps/src/tests/index.ts
+++ b/apps/src/tests/index.ts
@@ -115,5 +115,7 @@ export { default as Test2332 } from './Test2332';
export { default as Test2395 } from './Test2395';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestHeader } from './TestHeader';
+export { default as TestPreload } from './TestPreload';
+export { default as TestActivityStateProgression } from './TestActivityStateProgression';
export { default as TestHeaderTitle } from './TestHeaderTitle';
export { default as TestModalNavigation } from './TestModalNavigation';
diff --git a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
index d86bd10c9a..c7e4fee14e 100644
--- a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
+++ b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
@@ -16,7 +16,7 @@ While transitioning between views we may want to activate a second screen for th
## ``
This component is a container for views we want to display on a navigation screen.
-It is designed to only be rendered as a direct child of `ScreenContainer`.
+It is designed to only be rendered as a direct child of `ScreenContainer` or `ScreenStack`.
In addition to plain React Native [View props](http://facebook.github.io/react-native/docs/view#props) this component only accepts a single additional property called `activityState`.
When `activityState` is set to `0`, the parent container will detach its views from the native view hierarchy. When `activityState` is set to `1`, the `Screen` will stay attached, but on iOS it will not respond to touches. We only want to set `activityState` to `1` for during the transition or e.g. for modal presentation, where the previous screen should be visible, but not interactive. When `activityState` is set to `2`, the views will be attached and will respond to touches as long as the parent container is attached too. When one of the `Screen` components get the `activityState` value set to `2`, we interpret it as the end of the transition.
@@ -30,6 +30,9 @@ When `activityState` is set to `0`, the parent container will detach its views f
```
+When used in `` `activityState` can only be increased. The checks are added (in both native sides and JS part) to prevent situation when it's being removed, but still exists in in React Tree or if someones tries to preload already displayed screen.
+
+
## ``
Screen stack component expects one or more `Screen` components as direct children and renders them in a platform-native stack container (for iOS it is `UINavigationController` and for Android inside `Fragment` container). For `Screen` components placed as children of `ScreenStack` the `activityState` property is ignored and instead the screen that corresponds to the last child is rendered as active. All types of updates done to the list of children are acceptable when the top element is exchanged the container will use platform default (unless customized) animation to transition between screens.
diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm
index 9b1cc543a0..7f2fd3b8d9 100644
--- a/ios/RNSScreen.mm
+++ b/ios/RNSScreen.mm
@@ -304,6 +304,10 @@ - (void)setActivityStateOrNil:(NSNumber *)activityStateOrNil
{
int activityState = [activityStateOrNil intValue];
if (activityStateOrNil != nil && activityState != -1 && activityState != _activityState) {
+ if ([_controller.navigationController isKindOfClass:RNSNavigationController.class] &&
+ _activityState < activityState) {
+ RCTLogError(@"[RNScreens] activityState can only progress in NativeStack");
+ }
_activityState = activityState;
[_reactSuperview markChildUpdated];
}
diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm
index 82dc6fd555..c2a983c0a0 100644
--- a/ios/RNSScreenStack.mm
+++ b/ios/RNSScreenStack.mm
@@ -673,7 +673,7 @@ - (void)updateContainer
NSMutableArray *pushControllers = [NSMutableArray new];
NSMutableArray *modalControllers = [NSMutableArray new];
for (RNSScreenView *screen in _reactSubviews) {
- if (!screen.dismissed && screen.controller != nil) {
+ if (!screen.dismissed && screen.controller != nil && screen.activityState != RNSActivityStateInactive) {
if (pushControllers.count == 0) {
// first screen on the list needs to be places as "push controller"
[pushControllers addObject:screen.controller];
@@ -947,6 +947,7 @@ - (void)navigationController:(UINavigationController *)navigationController
- (void)markChildUpdated
{
// do nothing
+ [self updateContainer];
}
- (void)didUpdateChildren
diff --git a/react-navigation b/react-navigation
index 6b6b846f49..4caf3cb849 160000
--- a/react-navigation
+++ b/react-navigation
@@ -1 +1 @@
-Subproject commit 6b6b846f4984ac4834bab6bd8474b390313c741e
+Subproject commit 4caf3cb849a8708a36d0dbf22dac432d2ca780be
diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx
index 2dd3dfd8f7..ed369dd898 100644
--- a/src/components/Screen.tsx
+++ b/src/components/Screen.tsx
@@ -16,6 +16,7 @@ import {
// Native components
import ScreenNativeComponent from '../fabric/ScreenNativeComponent';
import ModalScreenNativeComponent from '../fabric/ModalScreenNativeComponent';
+import { usePrevious } from './helpers/usePrevious';
type NativeScreenProps = Omit<
ScreenProps,
@@ -165,6 +166,7 @@ export const InnerScreen = React.forwardRef(
function InnerScreen(props, ref) {
const innerRef = React.useRef(null);
React.useImperativeHandle(ref, () => innerRef.current!, []);
+ const prevActivityState = usePrevious(props.activityState);
const setRef = (ref: ViewConfig) => {
innerRef.current = ref;
@@ -239,6 +241,18 @@ export const InnerScreen = React.forwardRef(
activityState = active !== 0 ? 2 : 0; // in the new version, we need one of the screens to have value of 2 after the transition
}
+ if (
+ isNativeStack &&
+ prevActivityState !== undefined &&
+ activityState !== undefined
+ ) {
+ if (prevActivityState > activityState) {
+ throw new Error(
+ '[RNScreens] activityState cannot be decreased in NativeStack',
+ );
+ }
+ }
+
const handleRef = (ref: ViewConfig) => {
if (ref?.viewConfig?.validAttributes?.style) {
ref.viewConfig.validAttributes.style = {
diff --git a/src/components/helpers/usePrevious.tsx b/src/components/helpers/usePrevious.tsx
new file mode 100644
index 0000000000..22b422777a
--- /dev/null
+++ b/src/components/helpers/usePrevious.tsx
@@ -0,0 +1,11 @@
+import { useEffect, useRef } from 'react';
+
+export function usePrevious(state: T): T | undefined {
+ const ref = useRef();
+
+ useEffect(() => {
+ ref.current = state;
+ });
+
+ return ref.current;
+}