-
-
Notifications
You must be signed in to change notification settings - Fork 530
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
feat(iOS): support for custom detents with formSheet
presentation
#1852
Conversation
formSheet
presentationformSheet
presentation
formSheet
presentationformSheet
presentation
Do you want some help to test this @kkafar ? Also, if you make this native for Android... you will be a hero 😅. |
Hey @ferrannp, Currently I'm working on Fabric implementation which has some intricacies and it is not ready yet. Also I'm still thinking on prop naming and best way of exposing this functionality, any suggestions would also be welcome. At the moment I'm using dedicated prop temporarily named
But I also consider accepting an array with |
I will test it then @kkafar 👍 . For me this one |
I'm not aware of any limitation on number of detents. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General remarks:
- Prop definitions differ for native stack v5 and native component
ScreenProps
.
In native stack we have:
sheetAllowedDetents: SheetDetentTypes | number[],
sheetLargestUndimmedDetent: SheetDetentTypes | number,
while native component goes with four distinct props:
sheetAllowedDetents: SheetDetentTypes,
sheetLargestUndimmedDetent: SheetDetentTypes,
sheetCustomDetents: number[],
sheetCustomLargestUndimmedDetent: number,
It made native implementation much easier. Alternatively we could take an array of strings:
sheetAllowedDetents: (SheetDetentTypes | string)[],
sheetLargestUndimmedDetent: SheetDetentTypes | string,
where string is a percentage, e.g. '50%'
, and parse it on native side?
Looking for any suggestions
- Either native detent levels ('medium', 'large', 'all') or custom ones can be used, but not both at the same time.
This is because the values I receive must be sorted & exact height of native detents is not known and it is context dependent, thus I reject mixing them.
FabricTestExample/package.json
Outdated
@@ -35,6 +35,7 @@ | |||
"babel-jest": "^29.2.1", | |||
"eslint": "^8.19.0", | |||
"jest": "^29.2.1", | |||
"jotai": "^2.2.3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added it to both TestsExample
& FabricTestExample
to make my life much easier (it is a simple state management library, 399kB unpacked).
@@ -85,9 +85,11 @@ namespace react = facebook::react; | |||
// Props controlling UISheetPresentationController | |||
@property (nonatomic) RNSScreenDetentType sheetAllowedDetents; | |||
@property (nonatomic) RNSScreenDetentType sheetLargestUndimmedDetent; | |||
@property (nonatomic) NSNumber *sheetCustomLargestUndimmedDetent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sheetUndimmedDetentIndex
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would choose even sheetLargestUndimmedDetent
without Custom
keyword. On the first look, the name sheetUndimmedDetentIndex
isn't quite clear for me - what is Index
there? 🤔
@property (nonatomic) BOOL sheetGrabberVisible; | ||
@property (nonatomic) CGFloat sheetCornerRadius; | ||
@property (nonatomic) BOOL sheetExpandsWhenScrolledToEdge; | ||
@property (nonatomic) NSArray<NSNumber *> *sheetCustomDetents; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sheetDetentValues
, sheetMaxDetentValueFractions
or some variation of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would stay with sheetCustomDetents
, as sheetAllowedDetents
is already there and it would be confusing to use justsheetDetentValues
. Also, sheetMaxDetentValueFractions
seems a bit odd for me 🤔
customDetentWithIdentifier:ident | ||
resolver:^CGFloat( | ||
id<UISheetPresentationControllerDetentResolutionContext> ctx) { | ||
return ctx.maximumDetentValue * frac.floatValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure yet whether I should use ctx.maximumDetentValue
or try using large
system detent height (by using resolveValueInContext:
). Needs to be tested.
[NSMutableArray arrayWithCapacity:fractions.count]; | ||
int detentIndex = 0; | ||
for (NSNumber *frac in fractions) { | ||
NSString *ident = [[NSNumber numberWithInt:detentIndex] stringValue]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System API is organised is such way, that if I want to refer to particular detent I need to do it by its identifier (unique string). Thus I'm using indexes here. I rejected doing it by detent value (height) because the value is context dependent.
Alternative would be to make user define detents as objects with id
/ name
field, e.g.:
[{
id: "3/5",
value: 0.6
}]
But I did not like this idea even more so.
This is why sheetCustomLargestUndimmedDetent
(the name must be changed) takes an index as a value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about if someone provided 2 same values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iOS will treat them as a single detent. The resolved value in such case is rather iOS implementation specific and not documented. It's better to treat such case as undefined behaviour.
I see your point though.
I guess we will stick to using indices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a quick review with some questions. Looks good overall 🎉
[NSMutableArray arrayWithCapacity:fractions.count]; | ||
int detentIndex = 0; | ||
for (NSNumber *frac in fractions) { | ||
NSString *ident = [[NSNumber numberWithInt:detentIndex] stringValue]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about if someone provided 2 same values?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job with this one 🥳 However, I've got some suggestions that should be detented (haha) 😂😂🤣
android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java
Show resolved
Hide resolved
I am testing this and TBH, being able to do things like: sheetCornerRadius: 20,
sheetAllowedDetents: [0.4, 0.9],
sheetLargestUndimmedDetent: 0, is just amazing 🔥 . However: #1722 This bug is very bad. You do not notice it in a jumping.movThere is an opened PR... but IMO we need to fix this. It kinda breaks the whole experience. |
I'm aware of this flickering (on Fabric it gets even worse). #1767 can't be merged unfortunately, because while it helps with this particular problem, it brings back other already solved issues. I will see what can be done here. |
Will I be able to use a |
So that the method's name better describes its purpose
Test example was much more easier to write with some kind of state management library.
## Description Should be merged to #1852 or first rebased and then merged to main after #1852. Closes #1722 So the exact roots of the issue are unclear & obfuscated. It seems that it might be related to following (not 100% sure): 1. It looks like during animation `viewDidLayoutSubviews` is being called which in turn calls `setSize:forView:` on UIManager. This triggers Yoga layout underneath which sets view dimensions to the target values (end animation values) resulting in view clipping during the animation. There are two more important facts: a. its hard to determine whether its Yoga who sets invalid value or it gets invalid value from us (passed in `updateBounds` method after `viewDidLayoutSubviews` is triggered by system. b. when uimanager is not notified of bounds size change everything works fine I've considered various approaches: 1. Do not pass the value to UIManager when animation is ongoing. Presence of animation was detected by checking `animationKeys` property of view's layer. This still resulted in visual glitches. Moreover if I sent the final value after animation finish (via completion block) it resulted in content jumping (see [here](facebook/react-native#34834 (comment))). 2. Use `CADisplayLink` & report to UIManager bounds size from `presentationLayer` (that should be (almost) accurate value), but it still resulted in visual glitches (even when sliding down), both flickering and content jumping or just content had wrong top offset / padding. 3. Do not notify UIManager at all on bounds change. ## Changes I went with this approach for now. That is: I do not notify uimanager on bounds change when `stackPresentation == formSheet`. This is wild I know. I experimented a bit trying to find out what did it broke, but I did not find anything on my toy example (Test1649), however it is highly unlikely that such approach does not have negative impact, but I believe it is better that way, than having formsheets unusable due to this flickering. ## Test code and steps to reproduce Test1649 ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
Superseded by #2045 |
Description
This PR adds support for custom detents with
formSheet
modal presentation oniOS >= 16.0
.This new feature can be used with
native-stack v5
(react-native-screens/native-stack
) viasheetAllowedDetents
prop by passing an array of values to it (see prop description in docs).I plan on exposing it in v6 (
@react-navigation/native-stack
) in react-navigation/react-navigation#11516 or in a follow up PR.Note: I'm looking for any suggestions on better prop naming :D
See #1870 for view flickering issue description & solution
Resolves #1772
Changes
Implementation notes
Please look below for my review.
Known issues
View flickering on Paper and Fabric (the exact cause is not determined yet, but the code responsible for this is in
updateBounds
method which is called on every layout recalculation and it reports current view size to react (/ updates shadow node).See:
Test code and steps to reproduce
Test1649
in bothTestsExample
andFabricTestExample
apps.Checklist