dominicstop / react-native-ios-adaptive-modal

TBA
MIT License
14 stars 1 forks source link

`UIViewPropertyAnimator` not animating `RCTView` #2

Closed dominicstop closed 10 months ago

dominicstop commented 10 months ago

Bug Summary

Summary: UIViewPropertyAnimator does not animate RCTView size.


Bug Description

adaptive-modal uses UIViewPropertyAnimator and AutoLayout to animate the modal

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/036f1ad9-494c-4f90-b390-cfe84088e5a0

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/780710b5-d987-4ec2-912d-7c0835560658


However, when making the RN wrapper for adaptive-modal, i encountered an issue. When the modal is being animated, the modal content’s bounds (which is a RN view) is not being updated during the course of the animation

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/e1f428e4-6972-494d-8c09-e77923b5280f


I used autolayout to stretch the react "content view" to the view controller's view:

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/bdf304b1-b559-4f89-b4f4-f7a0e0d346c3


The react modal content view is now being resized by autolayout (since the blue BG color fills the modal), however the subviews of the react modal content (the view with red BG color) is not aware that it's parent's bounds have changed.

As a workaround, I also manually set the size of the "modal content" via RCTBridge + UIManager.setSize(size: CGSize, forView: UIView) during UIViewController.viewDidLayoutSubviews.

This triggers YGLayout recalculate the bounds of the react modal content, and it's subviews.

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/46f3c094-03d3-43e9-93bc-9a59f1ce3af8


But i noticed that it was still not enough, so i used CADisplayLink to periodically update the size of the modal content

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/f71ff1c1-a9de-400e-8a56-a2c795f3ecd3


It was still a bit jittery, so i centered the modal content via autolayout so it wouldn’t move around as much

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/bc11c170-d012-4605-b1bc-3a618b828588


It's less jittery now, but it's still a bit janky. But if we're careful with how you style the modal content, it can be less noticeable.

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/4ccb2bb8-91fc-480f-9e36-2f7d5826b10b

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/0180a11e-4b17-4017-9631-a2d33a1e9ada


Unfortunately, this solution is not perfect.

Here are a list of things i’ve tried:


Fixing The Underlying Problem

Is there anything that we can try to make UIViewPropertyAnimator work with YGLayout + RCTView? Or is this just not possible due to how RN works?

If the RCTView's somehow recalculate + update their frame/bounds inside of a UIViewPropertyAnimator's animation block, it should work right?

Is there something the wrapper library is doing wrong? Maybe not using the API's correctly?

Is there something i can change in adaptive-modal to make it better work w/ react-native?


Running The Library's Example

An unfinished version of the library is available via NPM:

npm install react-native-ios-adaptive-modal


However, i recommend just cloning the library's repo, and running the example instead:

# Clone the libary repo
git clone https://github.com/dominicstop/react-native-ios-adaptive-modal.git

# Setup library + example
cd ./react-native-ios-adaptive-modal
npm install ; cd example && npm install && cd ios && pod install

# Build and run the example
npm run start
open ./reactnativeiosadaptivemodalexample.xcworkspace/


This is what you should see when running the example:

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/ecdae9f5-4279-4f02-8836-b5f9349e23a9


The library is still in active development, so it's incomplete/unstable, and the API/props for the modal component is not yet finalized. Here are some relevant files related to the example:


How The Library Works

adaptive-modal is a small helper library for presenting a view controller with custom modal presentations.

adaptive-modal accepts a config that describes the state, behavior and layout of the modal, e.g. the various snapping points, layout (e.g. width/height, margin/padding), transition animation (e.g. duration, easing, etc), keyframes (e.g. BG blur, modal content BG blur, BG color, corner radius), etc.

It then transforms that config into something UIKit can understand via handling all the necessary conformances to UIViewControllerAnimatedTransitioning, UIAdaptivePresentationControllerDelegate, UIViewControllerTransitioningDelegate, and animating the modal to create a custom modal transition.

To define the various snapping points, ComputableLayout is used to configure the size and position of the modal. It is essentially just a function that turns a config into raw x, y, width and height values.

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/68a37565-8134-4c8e-add2-760b13a94846


DGSwiftUtilities and react-native-ios-utilities are just helper libraries.


More Workarounds (Proposal)

A possible solution could be batched layout updates.

UIViewPropertyAnimator smoothly animates the modal between each snap point; this is to say that the layout values (position and size) are animated continuously.

Because of this, when we use CADisplayLink to periodically update the react modal content, it stutters because it’s being updated discreetly; In other words, we are sampling the layout values frame by frame, and as such the size of the react modal content will be slightly behind.

UIViewPropertyAnimator uses CAAnimation under the hood, so the modal animation is offloaded to the GPU; as such the view’s frame/bounds isn’t actually updated in real-time (only after the animation is finished). Because of this, for every tick/frame, we are taking a snapshot of the modal via view.layer.presentation() and reading the CALayer’s frame.


Untitled_Artwork 4



I think the main reason for the stutter is because the react modal content’s layout updates is not exactly in sync with the modal.

A hacky workaround is to do: synced snapshot/frame-by-frame updates — e.g. do not use UIViewPropertyAnimator directly for animating the layout for the modal.


Untitled_Artwork



This workaround has some downsides though.

https://github.com/dominicstop/react-native-ios-adaptive-modal/assets/18517029/4382a1f8-c02b-4aad-825d-34f24c39cbce