-
Notifications
You must be signed in to change notification settings - Fork 24.2k
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): modal detents #34834
feat(iOS): modal detents #34834
Conversation
Base commit: 745f3ee |
Base commit: 4f142bf |
Thoughts on calling the prop |
Hmm. I tend to agree with keeping the prop name to |
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.
Amazing, thank you so much for doing this! 👏
I left a couple of very minor suggestions to improve a bit the codebase.
React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm
Outdated
Show resolved
Hide resolved
React/Fabric/Mounting/ComponentViews/Modal/RCTModalHostViewComponentView.mm
Outdated
Show resolved
Hide resolved
Could you also rebase the PR on |
@cipolleschi Thanks for the reviews, these are now incorporated. Would be great if you could re-review 🚀 I am not sure why CI fails on test_windows. |
that's just a flaky test, unfortunately |
@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Awesome seeing this approved so quickly, thanks @hurali97 & @cipolleschi! |
@cipolleschi Can you see what caused the Facebook Internal - Linter to fail? 🙂 |
case ModalHostViewDetent::Large: | ||
break; | ||
case ModalHostViewDetent::Medium: | ||
detents = @[UISheetPresentationControllerDetent.mediumDetent]; |
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.
From @sammy-SC:
here it looks like we're holding some state in view controller that does not get reset when component is reused or detent value changes. Is that expected?
I think we should update the prepareForRecycle
method.
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 tried changing the detent values using the states and they are working as they should. Also, the prepareForRecycle
method already sets the _viewController
to nil which sets the detents to nil automatically. So we're in good shape I think.
Here's the attached video, on how it looks like with states:
detents.mp4
@@ -258,6 +293,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & | |||
self.viewController.modalPresentationStyle = presentationConfiguration(newProps); | |||
|
|||
_shouldPresent = newProps.visible; | |||
[self handleDetent: newProps.detent]; |
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.
From @sammy-SC
should we check if detent changes?
I think we should check whether the new detent is different from the old one and proceed updating the props only if the two differs.
What do you think?
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.
Hey so I tested by comparing old and new props but it turns out we can’t really add that guard. The reason is, whenever the modal closes, prepareForRecycle
gets called and it sets _viewController
to nil. With this we loses the detents we set on the _viewController
. So when the next time we open the modal,
detents aren’t applied due to the guard we set previously and _viewController
has already been reset and lost the detents on it.
Here's how the guard is added:
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.
Thank you for checking this!
…react-native into feat/ios/modal-detents
@jacobp100 @cipolleschi So the final state of this PR is such that it allows the user to set medium, large or custom sized detents.
This works as expected and produces the following output: ios-detents.mp4However there's one case where I think we could investigate. When we supply a value of 100 or less, I have noticed that the modal doesn't dismiss as
@jacobp100 I am sending you the access rights to my fork, since I am on vacation and sadly can't find enough time right now. |
I've got a branch cleaning up the conversions here - https://github.com/hurali97/react-native/tree/detents-converters I can't get Fabric working - I think that will be more difficult because it won't allow mixed array types I did notice one thing - when resizing it, the content jumps. I don't think we can ship it like this - at minimum we need to set the background colour so the gap isn't visible I did spend a fair amount of time @grahammendick trying to fix this exact issue - but I don't think we ever found a full solution. I think if we can force a synchronous re-layout, we might be able to get iOS to animate it properly - but I don't know enough about fabric to do that Simulator.Screen.Recording.-.iPhone.SE.3rd.generation.-.2022-11-15.at.21.11.23.mp4 |
Hey @jacobp100, Thanks for your work, appreciated. You're right Fabric doesn't work with mixed array types that's why I used And regarding the jumping layout, you're right and this happens for both fabric and paper, we need to handle this behaviour. I can take a look at this ✍️ |
@hurali97 I think we should just support the medium and large sizes for now - until Fabric adds support for mixed types |
Understandable. So that leaves us with handling the jumping layout. I will pull your changes to my branch and start on top of it. |
@hurali97 it might be possible to get numbers working by removing the codegen for that prop, and manually doing it in the The layout jumping is probably the bigger issue though! |
Guys @jacobp100 @cipolleschi here's an update on this, sorry for being late. A quick recap of the problem we were facing with layout. So I tried debugging the cause for the jump in layout or the glitch at bottom of the modal. It turns out to me that there might be some underlying in the - (void)boundsDidChange:(CGRect)newBounds
{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onOrientationChange(onOrientationChangeStruct(newBounds));
}
if (_state != nullptr) {
auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)};
_state->updateState(std::move(newState));
}
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!CGRectEqualToRect(_lastViewBounds, self.view.bounds)) {
[_delegate boundsDidChange:self.view.bounds];
_lastViewBounds = self.view.bounds;
}
} RCTFabricModalHostViewController So I tried injecting a UIView with a UILabel directly as a subview of Here's a video experimenting UIView as subview of VC: native-view.mp4Here's a video with the default rct-view.mp4@cipolleschi @jacobp100 If you guys can provide some more details here or have some other suggestions that would be great in moving forward. Apart from this, as a temporary fix we may go with setting the backgroundColor to the view of VC, it may fix the glitch but not the layout jump of content inside. Also, with this approach we might need to get a hold of the RCTView in order to read the backgroundColor and then set it on the VC's view, as I think taking backgroundColor from user isn't right in such case. |
Is it possible to force a synchronous layout? I’ve not done enough fabric to know how - but if we do that in the resize delegate method, I think iOS should animate the resizing |
Yes, we can try that too. We will need to look for pointers in the other RN components to get a hold of how it will be done in fabric. Maybe @cipolleschi have some info on this🧐. |
Hi there! Small PSA: I'll be on PTO for a week, from today to the next Thursday. On this specific matter, I think @sammy-SC could have more information on how to ask Fabric to execute a sync layout. |
Hi @sammy-SC, A quick recap can be seen here. What we basically want is to ask Fabric to execute layout synchronously. Before diving in myself to see how we can achieve this, I can really use some pointers here 👀 |
…t-native into feat/ios/modal-detents
Update about my recent findings. I tried to find how we can cause fabric to do synchronous layout. But before that, I wanted to make sure whether it is really related to fabric synchronous layout or otherwise. For that, I again analysed this PR on both old paper and new fabric architecture. And on the both archs, we have the same behaviour of layout jump. So it's hard to say it can be fixed by synchronous layout using fabric. I decided to dive a bit deeper in the C++ implementation of I tried to experiment setSize method by passing hardcoded height and width, and the layout jump was gone however, it resulted in content being not centred, which makes sense. There's another observation, when we resize the modal slowly, the layout doesn't jump however when we do it fast, it causes jump. Resizing Fastwith-out-bg-fast.mp4Resizing Slowwith-out-bg-slow.mp4These observations tends me to think it's related to how Yoga is trying to layout this underneath. Since, we don't know the height of the modal and Yoga tries to calculate it upon resizing, as we update the shared state in As we also have this same behaviour on old paper architecture, I think it maybe known to the core team. If such is the case, we can consider testing the second option that we had, to set the The point is how do we approach it. I tried to get the Now with this approach, we get the same color set by the user without implementation differences on both old and new architectures. I haven't yet measured the performance this process may cause. I used guard to calculate it only once and for the minimum rect from the bottom of screen, but still need to measure it. If there's any other more efficient way we can get the backgroundColor from the underlying view, I am more than happy to discuss it. With background colorwith-bg.mp4I haven't pushed the code for backgroundColor stuff, if we opt-in going this path, I will do it and we can then review it. |
Wow @hurali97, thank you so much for this detailed explanation! 😮 Unfortunately, I don't have clear answers and a full context for you. I'll try to route this PR to people working on Yoga and which have more context on how the renderers work. It could be tricky as the Christmas is coming, but I'll try. Thank you again for this. |
Hey guys, hate to be the one pinging PRs, but alas...does it seem like this is close to merging? I'm still using the original patch from the linked tweet, would be awesome to have it built in! Thanks for all the work here. |
@nandorojo, sorry but with those glitches we can't really merge it. I'll try to ping some other people to have more clarity on how to move forward! |
This PR is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days. |
This PR was closed because it has been stalled for 7 days with no activity. |
Hey, I just added Bottom Sheet support to my Navigation router on iOS. I removed the flicker you're seeing when the sheet is dropped. Details in the PR but let me know if you want to know more |
## 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
## 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
Summary
As proposed in this discussion, this PR introduces feature for iOS 15+ detents with fabric support. Kudos to @barthap for his wonderful gist which introduces the feature for paper. Additional work was done to introduce this feature for fabric.
We have three values for detents and the usage is like below:
Down the road when adding support for fabric, it occurred to me that
onRequestClose
is not implemented, which prevents the dismiss offormSheet
orPageSheet
when we drag it down. So, added the support foronRequestClose
as well. I am not sure why this was left out for fabric, if it was intentional, it might worth a discussion.Since we don't have detents for android AFAIK, an empty method
setDetent
implemented inReactModalHostManager
to confirm to the generated interface.Thanks to @nandorojo for his POC with the above gist and a nice tweet showcasing the demo 🚀
Changelog
[iOS] [Added] - iOS 15 detents to Modal
Test Plan
This feature is tested on rn-tester with both old and new arch, below is the output:
detent-demo.mp4
Code for rn-tester is not committed to not bloat the PR. I may address it following the design principles of rn-tester in a couple of days, once this PR is merged.
TODO
detent
on react-native websitedetent
in rn-tester