Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LargeHeader stays small after pop/goBack/swipe gesture on iOS 14+ #649

Closed
hirbod opened this issue Sep 23, 2020 · 78 comments
Closed

LargeHeader stays small after pop/goBack/swipe gesture on iOS 14+ #649

hirbod opened this issue Sep 23, 2020 · 78 comments
Labels
Area: Native Stack Bug Something isn't working Platform: iOS This issue is specific to iOS

Comments

@hirbod
Copy link
Contributor

hirbod commented Sep 23, 2020

Hi,

I just upgraded to Expo SDK 39 and before of that, I had never an issue navigating to a child screen (from LargeHeader to small header). When I go back now, it stays smalls instead of growing back to large.

What has changed and is this something known?

@hirbod hirbod changed the title LargeHeader stays small after pop since EXPO SDK 39 LargeHeader stays small after pop/goBack/swipe gesture since EXPO SDK 39 Sep 23, 2020
@hirbod
Copy link
Contributor Author

hirbod commented Sep 23, 2020

This looks like it has nothing to do with SDK 39, its an issue with iOS 14. It works on iOS 13.
Here is a video @WoLewicki https://streamable.com/x4vo01

The same code on iOS 13 will grow the large header back to its original size.

@WoLewicki
Copy link
Member

Yeah, I can spot problems with this. It looks like some kind of bug because it doesn't happen in the Settings of your phone. Am I right?

@WoLewicki WoLewicki added Bug Something isn't working Platform: iOS This issue is specific to iOS Area: Native Stack labels Sep 23, 2020
@hirbod
Copy link
Contributor Author

hirbod commented Sep 23, 2020

@WoLewicki you are right, works great everywhere else in the system.

@ArekChr
Copy link

ArekChr commented Sep 28, 2020

The same here 👍

@WoLewicki
Copy link
Member

Does this behavior appear only while using ScrollView?

@hirbod
Copy link
Contributor Author

hirbod commented Oct 14, 2020

For me, it happens when using ScrollView and FlatList

@hirbod hirbod changed the title LargeHeader stays small after pop/goBack/swipe gesture since EXPO SDK 39 LargeHeader stays small after pop/goBack/swipe gesture on iOS 14+ Oct 14, 2020
@julian-baumann
Copy link

Maybe this is related: iOS 14 Large title collapse when app run firstly.

I experience this issue too, when I start the app or scroll to the top, navigate and go back on iOS 14.

@WoLewicki
Copy link
Member

Can you check if #670 fixes the issue and does not introduce any new issues?

@grahammendick
Copy link
Contributor

I’ve got the same problem on my Navigation router (@WoLewicki your fix didn’t work for me).

I’ve worked out a fix but we need to tweak the RCTScrollContentView class in React Native. We must ensure that updateContentOffsetIfNeeded first runs after the ScrollView is moved to the window. So we prevent it running inside reactSetFrame if there’s no window yet and get it to run from didMoveToWindow instead.

Here’s the fixed RCTScrollContentView. Can you check it works for you, please? You can open your project in XCode and paste this code into RCTScrollContentView

@implementation RCTScrollContentView

- (void)didMoveToWindow
{
    [super didMoveToWindow];
    RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;
    [scrollView updateContentOffsetIfNeeded];
}

- (void)reactSetFrame:(CGRect)frame
{
  [super reactSetFrame:frame];

  RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;

  if (!scrollView || !self.window) {
    return;
  }

  RCTAssert([scrollView isKindOfClass:[RCTScrollView class]], @"Unexpected view hierarchy of RCTScrollView component.");

  [scrollView updateContentOffsetIfNeeded];
}

@end

@WoLewicki
Copy link
Member

Looks like it works @grahammendick 🎉. Are you going to submit a PR with this in the react-native repository?

@grahammendick
Copy link
Contributor

@WoLewicki I'm glad it works for you too!
I don't really like submitting PRs in the react-native repo. It takes me a while to set everything up to run locally and then they never look at the PR anyway. I'm happy for someone on this issue to submit the PR instead if they want

@hirbod
Copy link
Contributor Author

hirbod commented Oct 19, 2020

I agree with @grahammendick on this. If you're an unknown name in the original react-native repo, PRs won't get a merge in years. Super frustrating to submit something there. I don't know how many times I tried to get their (facebook) eyes on broken scroll-to-top behaviour on inverted scrollviews.

I think, you guys from SWM have a better reputation and are more likely to have success submitting something there.

Is there a chance to fix this trough react-native-screens instead of fixing it upstream? @WoLewicki

@grahammendick
Copy link
Contributor

I tried fixing it upstream in my Navigation router but had no luck. The problem is we need to stop the updateContentOffsetIfNeeded running in reactSetFrame because that's too early. You can see what I mean by commenting out the !self.windowcheck in the fix and see it doesn't work anymore

@grahammendick
Copy link
Contributor

@hirbod You're right, but the react-native repo is just one example of the problems you have if you're an unknown name in React. I've found it impossible to get my work looked at. If you're not a name then you're not getting in

@ArekChr
Copy link

ArekChr commented Oct 19, 2020

@WoLewicki can someone from your team create PR to react-native repository?

@WoLewicki
Copy link
Member

It would need more debugging, submitting a PR with changes to a component used by almost every developer must have a very strong explanation and must ensure that no right behavior will be broken after it.

@DasArthur
Copy link

DasArthur commented Oct 31, 2020

For me, this quick hack worked.

return(

      <FlatList
           contentOffset={{ x: 0, y: -1}}
                contentInsetAdjustmentBehavior={"always"}
                maintainVisibleContentPosition={
                    {

                        autoscrollToTopThreshold: -1,
                        minIndexForVisible: -1
                    }
                }
       > 
          {/*your content goes here */}
       </FlatList>
)

@WoLewicki
Copy link
Member

@DasArthur in my test cases, it looks like there is some race condition and it sometimes loads initially a large header and sometimes a small one. Can you check this solution @ArekChr @grahammendick ?

@grahammendick
Copy link
Contributor

I remember trying a variety of contentOffset and contentInset and repeatedly reloading - sometimes it worked and sometimes it didn't. My guess is that the times it worked is when the reactSetFrame was called after it was added to the window - @DasArthur Could you check if the timing of the reactSetFrame changes when it works and when it doesn't? Or does it always work for you every time you reload?

@ArekChr
Copy link

ArekChr commented Nov 2, 2020

I don't have maintainVisibleContentPosition prop, @DasArthur what version of RN are you using?

@grahammendick
Copy link
Contributor

The workaround of @alexanderstrom seems to be the only reliable one. This is where you conditionally render the ScrollView after a delay (useEffect with a timeout of 0). I think it works for the same reason that my fix works - the delayed render means that didMoveToWindow runs before reactSetFrame. It runs as soon as it’s added as a subview because the parent is already in the window.

@hirbod
Copy link
Contributor Author

hirbod commented Nov 13, 2020

@grahammendick still feels so hacky :/

@grahammendick
Copy link
Contributor

@hirbod I think another option is you could use patch-package to apply the fix to RCTScrollContentView

@hirbod
Copy link
Contributor Author

hirbod commented Nov 13, 2020

@grahammendick unfortunately not an option for me, because I'm still on Expo and I don't want to eject. Since I only have 3 LargeTitle Screens, I'll try the delay render useEffect hack.

@aryanm5
Copy link

aryanm5 commented Oct 23, 2021

@grahammendick Thank you for your input. I can confirm that the large titles work for me on iOS 15 without the fix. This is what I came up with to add the iOS 14 version check. Does it look good to you? I created an osVersion variable and made all the changes conditional to a check for iOS 14. It works as expected when I test it. I don't have much experience with the language so I'm not sure if this is the best way.

diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollContentView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollContentView.m
index 8006540..2c73bf4 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollContentView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollContentView.m
@@ -14,13 +14,28 @@
 
 @implementation RCTScrollContentView
 
+int osVersion = nil;
++ (void)initialize {
+    if(osVersion == nil)
+        osVersion = [[[UIDevice currentDevice] systemVersion] intValue];
+}
+
+- (void)didMoveToWindow
+{
+    [super didMoveToWindow];
+    if (osVersion == 14) {
+      RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;
+      [scrollView updateContentSizeIfNeeded];
+    }
+}
+
 - (void)reactSetFrame:(CGRect)frame
 {
   [super reactSetFrame:frame];
 
   RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;
 
-  if (!scrollView) {
+  if (!scrollView || osVersion == 14 && !self.window) {
     return;
   }

@grahammendick
Copy link
Contributor

@aryanm5 Hey, your best bet is to make your changes on the PR so Phillip can advise you of how to add the version check. But I can understand you might feel under pressure putting your code before the React Native review process. My objective-c isn't great but here's how I would've done it.

- (void)didMoveToWindow
{
  [super didMoveToWindow];
    
  if (@available(iOS 14.0, *)) {
    if (@available(iOS 15.0, *)) {
    } else {
      RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;
      [scrollView updateContentOffsetIfNeeded];
    }
  }
}

- (void)reactSetFrame:(CGRect)frame
{
  [super reactSetFrame:frame];

  if (@available(iOS 14.0, *)) {
    if (@available(iOS 15.0, *)) {
    } else {
      if (!self.window) {
        return;
      }
    }
  }

  RCTScrollView *scrollView = (RCTScrollView *)self.superview.superview;

  if (!scrollView) {
    return;
  }

  RCTAssert([scrollView isKindOfClass:[RCTScrollView class]], @"Unexpected view hierarchy of RCTScrollView component.");

  [scrollView updateContentOffsetIfNeeded];
}

Also, make sure the formatting is consistent (use 2 spaces throughout). I wasn't bothered about this when I was patching the package, but now you're making a PR the code has to be consistent.

@hirbod
Copy link
Contributor Author

hirbod commented Oct 24, 2021

iOS 15 fixed the bug I've reported initally but introduced a new one. The large header is now snapping to a small header for me as soon as I scroll, looking super weird. (Expo SDK 43, iOS 15, iPhone 13 Pro Max)

https://streamable.com/x57lig

@hirbod
Copy link
Contributor Author

hirbod commented Nov 10, 2021

@WoLewicki is the new introduced bug something this package can solve? Or is this still a RN Upstream issue? I really don't know how to proceed with large header if its basically broken :/

@WoLewicki
Copy link
Member

@hirbod do you have knowledge if such behavior happens in other libraries doing kind of the same thing, like react-native-navigation or @grahammendick Navigation router ? And if so, did they come up with a solution?

@hirbod
Copy link
Contributor Author

hirbod commented Nov 17, 2021

@WoLewicki unfortunately not. I never tried another library. I can only tell you that it doesn’t happen in other system apps or any app I am using.

@WoLewicki
Copy link
Member

It is probably another issue of react-native's ScrollView and large header. Could you post here a quick reproduction of the newest problem?

@grahammendick
Copy link
Contributor

@hirbod @WoLewicki The Navigation router doesn't have any problem with large headers on iOS 15. You could run my zoom sample and compare view hierarchies?

@nandorojo
Copy link

iOS 15 fixed the bug I've reported initally but introduced a new one. The large header is now snapping to a small header for me as soon as I scroll, looking super weird. (Expo SDK 43, iOS 15, iPhone 13 Pro Max)

https://streamable.com/x57lig

I am having the exact same issue as @hirbod on iOS 15; glad I found this thread. I'm using react-native-screens@3.9.0 with @react-navigation/native-stack@6.2.2 and react-native@0.64.3. I'll try to make a repro.

@nandorojo
Copy link

It is probably another issue of react-native's ScrollView and large header. Could you post here a quick reproduction of the newest problem?

@WoLewicki Here is a reproduction:

Video

iOS 14 (works) iOS 15
Screen.Shot.-.28.November.2021.mp4
RPReplay_Final1638128423.MP4

Code

Repo here: https://github.com/nandorojo/large-title-bug

Click to view code
import React from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { NavigationContainer } from "@react-navigation/native";

const { Navigator, Screen } = createNativeStackNavigator();

function Home() {
  return (
    <ScrollView>
      {new Array(40).fill(0).map((_, i) => (
        <View style={[styles.item, i % 2 && styles.odd]}>{i}</View>
      ))}
    </ScrollView>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Navigator>
        <Screen
          name="Home"
          component={Home}
          options={{
            headerLargeTitle: true,
          }}
        />
      </Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  item: {
    height: 100,
    backgroundColor: "#41b87a",
  },
  odd: {
    backgroundColor: "#333333",
  },
});

@hirbod
Copy link
Contributor Author

hirbod commented Nov 28, 2021

@nandorojo thanks for providing a repro for the issue. My desk was overloaded and I haven’t managed to create one!

@WoLewicki
Copy link
Member

@nandorojo the reproduction seems to be the same issue as the old ones with ScrollView and was resolved by this: #320 (comment). I checked the repro and it works correctly when applying those props. Can you check if it fixes your issue? And if so, is there something more to be done about it?

@hirbod
Copy link
Contributor Author

hirbod commented Dec 7, 2021

I don’t like the fact we need to set special props in order to make it work. It wasn’t like this till iOS 13, broken in iOS 14 and now semi working on iOS 15 (but working under special circumstances when changing header and Scrollview).

Is there a way to make the DX better? I mean there is also no documentation about this.

@WoLewicki
Copy link
Member

I am afraid these options are the defaults used by native apps on iOS and are required for correct behavior of ScrollView integrated with large header.

@sallar
Copy link

sallar commented Dec 8, 2021

@WoLewicki Then I guess this issue can be closed right? Since it's not actually a bug?

@WoLewicki
Copy link
Member

@sallar yeah, I also think there is not much to be added from the react-native-screens side. I will close it then, feel free to comment here if something is wrong and we can always reopen this.

@toriqo
Copy link

toriqo commented Dec 31, 2021

For me, this quick hack worked.

return(

      <FlatList
           contentOffset={{ x: 0, y: -1}}
                contentInsetAdjustmentBehavior={"always"}
                maintainVisibleContentPosition={
                    {

                        autoscrollToTopThreshold: -1,
                        minIndexForVisible: -1
                    }
                }
       > 
          {/*your content goes here */}
       </FlatList>
)

actually, for me, just adding contentInsetAdjustmentBehavior="automatic" to my flat list did the trick

@branaust
Copy link

branaust commented Dec 30, 2022

Unfortunately, this is still an issue for me. None of the solutions in this thread have fixed this behavior. The problem seems to stem from hiding the parent navigators header and then navigating to the child screen with largeHeaderTitle set to true.

@trajano
Copy link

trajano commented Dec 30, 2022

@branaust you may have to bite the bullet a bit on this one and create your own header/navigator yourself. I had to do that on my project and spent quite a bit of time trying to get the nuances of the animation correct. However, the header behaviour is predictable after that. I am just revisiting the idea again and posted to SO https://stackoverflow.com/questions/74957469/can-a-custom-header-in-react-navigation-receive-the-scrollview-ref-in-a-createst to see if there's a saner approach than what I did.

However, if you do create your own you'd have to remember the Pan Gesture to support swipe left or right on the stack or down to minimize a modal.

@branaust
Copy link

branaust commented Dec 30, 2022

@trajano I did find a bit of a hacky workaround that someone else suggested in another thread. The solution consists of rendering the scrollView after the screen has been mounted.

Here's my screen:

const [renderScrollView, setRenderScrollView] = useState<boolean>(false);

  useEffect(() => {
    setTimeout(() => {
      setRenderScrollView(true);
    }, 100);
  }, []);

and here is the return:

const Content = () => {
    return (
      <Container
        style={{
          flex: 1,
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor: theme.colors.background,
        }}></Container>
    );
  };

  return !renderScrollView ? (
    <Content />
  ) : (
    <ScrollView
      style={{ backgroundColor: theme.colors.background }}
      contentInsetAdjustmentBehavior="automatic"
      contentContainerStyle={{
        flex: 1,
      }}>
      <Content />
    </ScrollView>
  );
};

This solution does fix the largeHeaderTitle issue and now every time I navigate to the screen the large header is shown by default.

In my special use case, I'm also rendering the native search bar in the header that react-navigation offers out of the box.
The solution I provided above does not fix the auto-collapsed search bar. Instead, user needs to scroll up once screen is rendered in order for it to appear in view.

My workaround for this was to add:

headerSearchBarOptions: {
              hideWhenScrolling: false,
            }

Now, when the screen is mounted, the large header works perfectly & search bar is visible (although it is always in view even when the user scrolls which is fine for now).

@trajano
Copy link

trajano commented Dec 30, 2022

@branaust what if you did

const setRenderScrollViewToTrue = useCallback(()=>setRenderScrollView(true));
...
<Component onLayout={setRenderScrollViewToTrue} />

Would you eliminate the "timeout"?

@dioncodes
Copy link

Unfortunately, this is still an issue for me. None of the solutions in this thread have fixed this behavior. The problem seems to stem from hiding the parent navigators header and then navigating to the child screen with largeHeaderTitle set to true.

Probably not the answer you are looking for and an unpopular opinion, but if you're early in the development I can only recommend not to use React Native 😄. Use Ionic or native development instead... RN comes with tons of dependencies and people tend to use a lot of libraries. Whenever an update comes out either for the OS or React Native pretty much everything breaks. I previously tried realizing a large app project with React Native and more time was spent on maintaining, forking, manually patching libraries and investigating than the usage of RN saved in time... And nowadays in most cases web apps like Ionic feel quite native as well and save A LOT of time in development. RN is just something in between (web and native) but not doing any of both really well.

@trajano
Copy link

trajano commented Dec 30, 2022

@dioncodes I somewhat agree with you, but I also came from the hell of Ionic v2->v3->v4->aborted v5 upgrades which require me to restructure my entire code base to get things working. Working on React Native is actually less painful once you have control of the dependencies (which takes a bit of time but I think more fruitful), but Ionic upgrades require project structure changes which means you'd have to know what everyone has done and migrate over.

NativeScript would've been a good option if HMR with Vue worked.

Vue Native has been deprecated. Their team has focus has moved to NativeBase.

The primary selling point for React Native for me is still Expo Go and the snacks which allows me to quickly prototype UIs and show without huge drama. Without Expo, React-Native is too much of a pain to work with alone.

As far as managing dependencies learning from experience, whenever we use any 3rd party dependency, we have to think of how to get rid of it asap or merge their code base into our own using yarn workspaces (haven't gotten that far myself, what I've done is clone and push to our internal npmjs with updated package.json once we verify it works correctly in the current versions of React(native). The issue also appears with Ionic though not as much because Angular tends to control everything for you much like Vue.

@branaust
Copy link

@branaust what if you did

const setRenderScrollViewToTrue = useCallback(()=>setRenderScrollView(true));
...
<Component onLayout={setRenderScrollViewToTrue} />

Would you eliminate the "timeout"?

Works like a charm. Thank you!

@dioncodes
Copy link

@trajano Yeah, Expo and the previews are definitely a good point with RN. However, you also get these with Ionic or web based frameworks, because you can already preview everything in your browser including HMR... The upgrade paths for Ionic weren't that well either in history, ngl. But it's less of an issue to stick to a previous Ionic version because most is still compatible to newer OS as there are less native components. I have maintained some apps that are running in latest OS versions on a code base that is 3 years old without any migrations so far.

One more reason why I stopped working with RN was that there were a few bugs with native libraries (including this react-native-screens) that no solutions existed for yet. Issues and comments were all over Github (including the official RN repo, just take a look at how many unsolved issues there are). And fixing them on your own would be a lot of work and require knowledge of the libraries or RN itself including its ancient Objective-C code. Maybe it was bad luck but during my larger RN project I stumbled in so many issues that were so time intense to solve, that it would have been much quicker to build a completely native app.

Anyways I'm also not a big fan of React anymore, especially since v3 Vue became so much better and that's already another reason for me not work with RN. And Vue Native isn't a reasonable option either (as you already mentioned). I'll just stick to Vue Ionic if Android is necessary and Swift + SwiftUI otherwise. In the end everything other from completely native development is a compromise. However native Android development is such a PITA that at least for Android some web/hybrid solution is necessary (if the platform needs to be supported at all) and dedicated/separate apps for iOS and Android require a higher budget...

@dvcrn
Copy link

dvcrn commented Jun 4, 2023

2 years old issue, but since I'm dealing with this right now as well, some observations on my specific case

Code:

  return (
    <Stack.Navigator
      screenOptions={{
        headerLargeTitle: true,
        headerShown: true,
        headerShadowVisible: false,
        headerLargeTitleShadowVisible: false,
        headerStyle: {
          backgroundColor: '#F7F6FB',
        },
      }}
    >
      <Stack.Screen
        options={{
          headerLeft: () => (isFetching && <ActivityIndicator />) || <></>,
          headerRight: () => (
            <Button onPress={() => navigation.navigate('Settings')} title="Settings" color="#000" />
          ),
        }}
        name="My Header"
        component={HomeScreen}
      />
    </Stack.Navigator>
  );

where HomeScreen is returning a FlatList inside a View

        <FlatList
          ListHeaderComponent={renderHeader()}
          contentInsetAdjustmentBehavior="automatic"
          style={styles.flatlist}
          renderItem={renderItem}
          keyExtractor={(item) => item.id}
          refreshControl={
            <RefreshControl title="Loading..." refreshing={refreshing} onRefresh={onRefresh} />
          }
        />
image
  • When rendering the header directly after app start, things are broken. The header stays in 'big mode' and scrolling doesn't hide it
  • When I save any file and react does a hot refresh, the header is fixed and scrolls correctly / hides when scrolling
  • When I set headerLargeTitle to false, then update it in a timeout to true, scrolling / hiding / showing works fine, but the header is now small first and I need to scroll to the top to show it in it's big form again
  • Delaying rendering of HomeScreen (which contains my FlatList) didn't do anything for this issue, neither did delaying only the FlatList
  • Updating the title text in a timeout doesn't do anything either

So far what worked the most reliable with fixing scroll related issues was setting headerLargeTitle to false and true again, but as mentioned it puts causes the header to be small first. Gonna do a bit more poking around.

@dvcrn
Copy link

dvcrn commented Jun 4, 2023

Man I've figured my issue out, and it was the exact opposite of the hacky workarounds here

It boiled down to 2 issues:

  1. The FlatList was not mounted immediately when the StackView/Screen mounted and only a few ms later. That was because I loaded some data from async storage, so for a fraction of a second I displayed an ActivityIndicator instead of the FlatList (switched with the usual {isLoading && <loader> || <flatlist}

  2. The FlatList was not the first element within the embedded view. I had something like {!rows.length && <NoDataView />} before rendering the FlatList. I moved that after the FlatList and it resolved the issue.

Making sure the FlatList is always mounted when the Screen is mounted, and making sure there are no other elements before the FlatList inside the view resolved this issue for me. I no longer have the app start with a small title, and I have no more issues with scrolling and not hiding the title

@fabOnReact
Copy link

This issue can be solved by creating your own implementation of ScrollView based on react-native APIs. https://github.com/fabOnReact/react-native-improved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Native Stack Bug Something isn't working Platform: iOS This issue is specific to iOS
Projects
None yet
Development

Successfully merging a pull request may close this issue.