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

Add ScrollView.automaticallyAdjustsScrollIndicatorInsets prop (on iOS) #29809

Conversation

justinwh
Copy link
Contributor

Summary

iOS 13 added a new property to UIScrollView: automaticallyAdjustsScrollIndicatorInsets, which is YES by default. The property changes the meaning of the scrollIndicatorInsets property. When YES, any such insets are in addition to whatever insets would be applied by the device's safe area. When NO, the iOS <13 behavior is restored, which is for such insets to not account for safe area.

In other words, this effects ScrollViews that underlay the device's safe area (i.e. under the notch). When YES, the OS "automatically" insets the scroll indicators, when NO it does not.

There are two problems with the default YES setting:

  1. It means applying scrollIndicatorInsets to a ScrollView has a different effect on iOS 13 versus iOS 12.
  2. It limits developers' control over scrollIndicatorInsets. Since negative insets are not supported, if the insets the OS chooses are too large for your app, you cannot fix it.

Further explanation & sample code is available in issue #28140 .

This change sets the default for this property to NO, making the behavior consistent across iOS versions, and allowing developers full control.

Changelog

[iOS] [Changed] - ScrollView scrollIndicatorInsets to not automatically add safe area on iOS13+

Test Plan

Here are screenshots of the demo app (from the original bug) before (with safe area applied to insets) & after (without safe area applied to insets):

before

after

@facebook-github-bot
Copy link
Contributor

Hi @justinwh!

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file.

In order for us to review and merge your code, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks!

@react-native-bot react-native-bot added the Platform: iOS iOS applications. label Aug 29, 2020
@justinwh
Copy link
Contributor Author

There is an extra concern with this PR: changing the default behavior will require developers who are using ScrollView in this way to re-adjust their code.

  • for devs who have already added their own workarounds, they'll need to revert them

  • for devs that depended on the previous iOS 13 default behavior (and aren't concerned about iOS <=12), they'll need to explicitly add appropriate insets

Based on the low interest in the initial bug, I don't think this is a lot of people, but it's not zero either.

@analysis-bot
Copy link

analysis-bot commented Aug 29, 2020

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 9,153,686 +2,525
android hermes armeabi-v7a 8,687,398 +2,727
android hermes x86 9,594,591 +2,675
android hermes x86_64 9,562,890 +2,792
android jsc arm64-v8a 10,798,008 +1,254
android jsc armeabi-v7a 9,722,980 +1,458
android jsc x86 10,834,313 +1,405
android jsc x86_64 11,444,115 +1,515

Base commit: ad0ccac

@analysis-bot
Copy link

analysis-bot commented Aug 29, 2020

Platform Engine Arch Size (bytes) Diff
ios - universal n/a --

Base commit: 006f5af

@osartun
Copy link
Contributor

osartun commented Feb 8, 2021

What are the next steps for this PR? We have a local patch for RN in our repo with pretty much the same code. I would like to see that issue fixed so that we don't need to reapply our patch with every upgrade of RN.

There is an extra concern with this PR: changing the default behavior will require developers who are using ScrollView in this way to re-adjust their code.

  • for devs who have already added their own workarounds, they'll need to revert them
  • for devs that depended on the previous iOS 13 default behavior (and aren't concerned about iOS <=12), they'll need to explicitly add appropriate insets

Can't you just put this into a new "breaking" version? Whenever we upgrade we check the changes and are expecting something to break.
Seeing this getting fixed would be a satisfying breaking change though.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 8, 2021
@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@jfrolich
Copy link

I'd love that someone in the core team would look at this. Without this, it is impossible to get the scroll indicators right on iOS in some scenarios. It's also a bit frustrating because I think a lot of people would fiddle with it and conclude it's not possible to get it right, or after a long time find this patch (like me 😃)

@justinwh
Copy link
Contributor Author

Yeah, crazy huh? I'm the original submitter—I don't know what else to do to get this fixed. I documented the issue pretty thoroughly, found the cause, and submitted a fix PR. I'm not authorized to merge it myself.

As far as I can tell, nobody from the project who is authorized to merge it is aware it exists.

I don't know what to do to get a maintainer's attention. Today I tried asking in the Reactiflux discord linked from the project website, but didn't get any useful answers there either.

The original issue (which is now just shy of its one-year anniversary) still displays the "Needs: Triage" label...

On the plus side, I can confirm that I've been shipping this fix in my own app for over a year and it has not caused any problems.

@lunaleaps
Copy link
Contributor

These changes look good to me! cc @PeteTheHeat or @p-sun for any thoughts?

@facebook-github-bot
Copy link
Contributor

@lunaleaps has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@lunaleaps
Copy link
Contributor

@justinwh Would it be possible to add your test case (demonstrating the scrollbar positions) as an RNTester case? How were you able to display the scrollbars persistently? We're trying to see how we can prevent this from regressing in the future.

@justinwh
Copy link
Contributor Author

justinwh commented May 5, 2021

@justinwh Would it be possible to add your test case (demonstrating the scrollbar positions) as an RNTester case? How were you able to display the scrollbars persistently? We're trying to see how we can prevent this from regressing in the future.

@lunaleaps Unfortunately, I captured my screenshots manually without persistent scrollbars (i.e. just kept trying until I got the timing right).

As far as I know the scrollbars cannot be made to display persistently on iOS. And I think their animation is too quick to capture consistently in a screenshot-based automated test.

I'm not aware of any good option for setting up a test case for this. (But I'm open to suggestions.)

@lunaleaps
Copy link
Contributor

As far as I know the scrollbars cannot be made to display persistently on iOS. And I think their animation is too quick to capture consistently in a screenshot-based automated test.

Yeah, I agree. I think then a manual test example case of this might be the best we can do? cc @p-sun who had some thoughts on the changes.

@justinwh justinwh force-pushed the ScrollView-automaticallyAdjustsScrollIndicatorInsets branch from ce9e405 to d48b23b Compare May 6, 2021 16:19
Copy link

@analysis-bot analysis-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code analysis results:

  • eslint found some issues. Run yarn lint --fix to automatically fix problems.


const {
Button,
DeviceInfo,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-unused-vars: 'DeviceInfo' is assigned a value but never used.

Modal,
ScrollView,
StyleSheet,
Switch,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-unused-vars: 'Switch' is assigned a value but never used.

ScrollView,
StyleSheet,
Switch,
Text,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-unused-vars: 'Text' is assigned a value but never used.

|},
> {
state = {
modalVisible: false

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comma-dangle: Missing trailing comma.

@justinwh
Copy link
Contributor Author

justinwh commented May 6, 2021

I added a second commit with an rn-tester screen that demonstrates the behavior (called ScrollViewIndicatorInsets).

When the fix is applied, this shows the scrollbars passing outside the device safe area to the screen edge (as intended). If you revert the fix, the scrollbars stay inside the device safe area.

@justinwh justinwh force-pushed the ScrollView-automaticallyAdjustsScrollIndicatorInsets branch from d48b23b to 819c91e Compare May 6, 2021 16:45
@lunaleaps
Copy link
Contributor

@facebook-github-bot import

@justinwh justinwh force-pushed the ScrollView-automaticallyAdjustsScrollIndicatorInsets branch from 7478a04 to 1f51418 Compare May 31, 2021 21:57
Copy link

@analysis-bot analysis-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code analysis results:

  • eslint found some issues. Run yarn lint --fix to automatically fix problems.

return (
<View>
<Modal
animationType='slide'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx-quotes: Unexpected usage of singlequote.

style={styles.switch}/>
<Button
onPress={() => this._setModalVisible(false)}
title='Close'/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx-quotes: Unexpected usage of singlequote.

</Modal>
<Button
onPress={() => this._setModalVisible(true, 'pageSheet')}
title='Present Sheet Modal with ScrollView'/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx-quotes: Unexpected usage of singlequote.

title='Present Sheet Modal with ScrollView'/>
<Button
onPress={() => this._setModalVisible(true, 'fullscreen')}
title='Present Fullscreen Modal with ScrollView'/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsx-quotes: Unexpected usage of singlequote.

@justinwh
Copy link
Contributor Author

I updated the PR with the changes discussed in the issue:

  • Made automaticallyAdjustsScrollIndicatorInsets a prop on ScrollView
  • Made the default true

I followed the implementation of the contentInsetAdjustmentBehavior prop as my template. What I pushed is sufficient to get the rn-tester example working. But I also found places in the tree where these props seem to be defined/redefined that I'm less familiar with. Should I add this new prop into the following files too?

if (@available(iOS 11.0, *)) {
if (oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) {
auto const contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Automatic) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::ScrollableAxes) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
}
}

contentInsetAdjustmentBehavior(convertRawProp(
rawProps,
"contentInsetAdjustmentBehavior",
sourceProps.contentInsetAdjustmentBehavior,
{ContentInsetAdjustmentBehavior::Never})),

@justinwh justinwh changed the title Change ScrollView.scrollIndicatorInsets to not automatically add safe area on iOS 13 Add ScrollView.automaticallyAdjustsScrollIndicatorInsets prop (on iOS) May 31, 2021
@justinwh justinwh force-pushed the ScrollView-automaticallyAdjustsScrollIndicatorInsets branch from 1f51418 to a8875b6 Compare May 31, 2021 22:18
@lunaleaps
Copy link
Contributor

lunaleaps commented Jun 3, 2021

I updated the PR with the changes discussed in the issue:

  • Made automaticallyAdjustsScrollIndicatorInsets a prop on ScrollView
  • Made the default true

I followed the implementation of the contentInsetAdjustmentBehavior prop as my template. What I pushed is sufficient to get the rn-tester example working. But I also found places in the tree where these props seem to be defined/redefined that I'm less familiar with. Should I add this new prop into the following files too?

if (@available(iOS 11.0, *)) {
if (oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) {
auto const contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Automatic) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::ScrollableAxes) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
}
}

contentInsetAdjustmentBehavior(convertRawProp(
rawProps,
"contentInsetAdjustmentBehavior",
sourceProps.contentInsetAdjustmentBehavior,
{ContentInsetAdjustmentBehavior::Never})),

Re: ScrollViewProps.cpp, I think this is the defaults we're using for the Fabric native view so we should probably set the default of true there as well.

Re: RCTScrollViewComponentView.mm I believe so yes, as in updating the prop value.

Let me see if someone from with more Fabric experience can confirm. cc @p-sun?

@p-sun
Copy link
Contributor

p-sun commented Jun 8, 2021

Yes, @lunaleaps is right. Any new prop should be added to the Fabric component as well. You can add the prop underneath the contentInsetAdjustmentBehavior prop in the two places that Luna linked, add a breakpoint inside RCTScrollViewComponentView.mm, and test it inside the RN Tester app to make sure it works.

Thanks for adding a Switch in your example for this prop. 👍

@fkgozali
Copy link
Contributor

fkgozali commented Jun 8, 2021

Btw, RNTester iOS/Android should already support Fabric, but perhaps with some minor quirks. You can enable Fabric in RNTester iOS manually by setting this env var before pod install: https://github.com/facebook/react-native/blob/master/packages/rn-tester/Podfile#L22-L30

USE_FABRIC=1 pod install

Then at least you can verify whether the updated .mm/C++ files compiled and ran fine.

…or iOS)

Summary:
In iOS 13 Apple added a new property to UIScrollView that "automatically" alters the scroll indicator insets in a fashion similar to the way 'contentInsetAdjustmentBehavior' alters content insets.  See here for iOS documentation:
https://developer.apple.com/documentation/uikit/uiscrollview/3198043-automaticallyadjustsscrollindica?language=objc

The OS default value for this property is `true`, which we preserve. When set to `false`, the behavior matches iOS <= 12.

Closes facebook#28140
…iew.automaticallyAdjustsScrollIndicatorInsets
@justinwh justinwh force-pushed the ScrollView-automaticallyAdjustsScrollIndicatorInsets branch from a8875b6 to 6b396b8 Compare June 21, 2021 17:40
@justinwh
Copy link
Contributor Author

I added the Fabric changes, and confirmed they compile and run when USE_FABRIC=1.

The rn-tester page I added requires Modal, which apparently isn't implemented in Fabric yet. But I confirmed that setting the prop on other ScrollViews in rn-tester has the correct effect.

@facebook-github-bot
Copy link
Contributor

@lunaleaps has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@lunaleaps
Copy link
Contributor

lunaleaps commented Jun 28, 2021

For the RNTester example, I noticed that there's no difference between true/false for the property in the case of displaying a Modal with pageSheet presentation style (there is for fullScreen).

@justinwh was there a reason to include pageSheet modal as an example? There's a typo on the fullScreen example which I've fixed on the imported PR. I'm going to add some explanation to what we should expect to see when you toggle this on/off.

Additionally did we discuss adding documentation for this? If not, we should get a PR in react-native-website for this new property! Let me know if you want to work on that.

For now, I'll delete the pageSheet Modal example and we can do a follow up PR to add it back in if I'm misunderstanding.

See pageSheet screenshots with the property toggled on/off -- I see no difference in the scrollbar position.
Simulator Screen Shot - iPhone 8 - 2021-06-28 at 14 06 58
Simulator Screen Shot - iPhone 8 - 2021-06-28 at 14 06 47

@justinwh
Copy link
Contributor Author

@lunaleaps thank you for fixing the typo.

The pageSheet modal does show different behavior for the checked/unchecked states for me. I'm not sure why yours doesn't, but since this is an "automatic" property I guess nobody but Apple knows how it actually works. Maybe it's device or OS version dependent? (I tested on the Simulator with the iOS 14.5/iPhone 12 Pro device type and on a real iPhone 12 Mini running iOS 14.6.)

On both devices I tested, enabling it adds a top inset to the pageSheet modal, which is incorrect (I think) since the modal does not underlay the statusbar/notch area. So the behavior you're getting is actually an improvement.

In any case, what it does when enabled is outside our control. The important thing is that it leaves the scroll indicator insets alone when disabled, and it looks like that's the case in your screenshots. So I believe this is acceptable. If you want to remove the pageSheet example from rn-tester, I have no objection.

We didn't discuss documentation. I assumed the docs were generated from the comments, but it sounds like that's not the case. Is there a process for synchronizing the updates to the react-native-website repo with React Native releases that I should be aware of?

Here's my proposed documentation for the new prop (let me know if you have feedback on it):

automaticallyAdjustsScrollIndicatorInsets
Controls whether iOS should automatically adjust the scroll indicator insets. The default value is true. Available on iOS 13 and later.
@platform ios

@lunaleaps
Copy link
Contributor

@justinwh Sounds good! I'll go ahead and land..

Regarding docs that sounds good! I wonder if we should also link to the UIScrollView docs for the property since. Would you like to make a PR for react-native-website? It should go under the versioned_docs next I believe. Let me know if you need more help!

@justinwh
Copy link
Contributor Author

I added a link to the UIScrollView docs and sent a PR to the docs repo:

facebook/react-native-website#2661

justinwh added a commit to justinwh/react-native-website that referenced this pull request Jul 7, 2021
@facebook-github-bot
Copy link
Contributor

@lunaleaps merged this pull request in bc1e602.

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Jul 8, 2021
@glenna
Copy link
Contributor

glenna commented Dec 13, 2021

hey @justinwh we are integrating this change into our app now and finally removing our RN patch for this, but are confused a bit on what the default actually is. From the code, it looks like it defaults to true / YES (in iOS world), as well as in the RN documentation, but in this PR description and commit message, it says

This change sets the default for this property to NO, making the behavior consistent across iOS versions, and allowing developers full control.

What is meant?

@justinwh
Copy link
Contributor Author

@glenna The docs are correct—the default is true. I didn't realize the PR's merge commit msg comes from the PR's description (which reflects my original approach, but not the final fix) instead of the commit itself (which I did update).

Sorry for the confusion.

@glenna
Copy link
Contributor

glenna commented Dec 13, 2021

@justinwh not a problem! thanks for the clarification!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Needs: React Native Team Attention No CLA Authors need to sign the CLA before a PR can be reviewed. Platform: iOS iOS applications.
Projects
None yet
Development

Successfully merging this pull request may close these issues.