Metadata
- File Name:
2024-06-05 - Weird Ideas for Tamagui + Native - URL: Gist -
While reading the code + docs for tamagui, i came up w/ some weird ideas to impl, for tamagui. They are possible to implement, but might be impractical or don't have any merit; nevertheless, i wanted to write them down anyways.
In adaptive-modal, layout values can be composed/combined together, or have different values depending on some condition; these are called "computable values", and "evaluable conditions". Basically it's just a state machine in the form of a struct (in swift) or JSON (in react/js) that can be evaluated at run-time.
For example the modal's frame.y position could be defined like this: safeAreaInsets.bottom + 10 + keyboardFrame.height; the "variables" can be replaced at runtime, and it is by an ugly JSON configuration.
{
layoutConfig: {
marginBottom: {
mode: {
mode: "multipleValues",
values: [
{
mode: "safeAreaInsets",
insetKey: "bottom",
},
{
mode: "keyboardRelativeSize",
sizeKey: "height",
},
{
mode: "constant",
value: 15,
},
],
},
},
}
}But what if react views could also accept an json like this, e.g.
// Before Compilation - Terse Form
<TamaguiNativeView
marginBottom={"$nativeValue.safeAreaInsets.bottom + $nativeValue.keyboardRelativeSize.height + 15"}
/>
// After Compilation - Verbose Form
<TamaguiNativeView
marginBottom={{
mode: "multipleValues",
values: [
{
mode: "safeAreaInsets",
insetKey: "bottom",
},
{
mode: "keyboardRelativeSize",
sizeKey: "height",
},
{
mode: "constant",
value: 15,
},
],
}}
/>The JSON then gets evaluated and computed in the native side, resulting in a value. This API could be implemented via making custom native shadow components (RCTShadowView in paper, and ShadowNode in fabric).
The upside to this is that there is no JS code; all of this will run in the native side, and as such all the values will always be "correct" and will be updated on each layout pass.
There are a lot of downsides to this, mainly that this would be diverging from the RN API, and the burden of testing and maintenance would be shouldered by tamagui. Not to mention in order to support "computable values", a custom subclass and the corresponding prop needs to be implemented for every view primitive (ScrollView, Text, etc).
So on second thought, it's not really practical?
The high level animation API for iOS is UIView.animate, and it uses a block based API:
UIView.animate(withDuration: 1.0) {
view.frame = CGRect(x: 0, y: 0, width: 100, height: 200);
view.backgroundColor = .red;
}Since RN just uses UIView's, it's also possible to use this API however you would need update the view inside the UIView.animate block.
Since RN uses props, this is not possible. Because of this, you cannot use styles directly, and instead create an intermediate config that will hold and apply the "styles" you want; for things like backgroundColor, and cornerRadius this would work.
The problem lies with layout (i.e. animating the size of a view); since UIView.animate is block based + synchronous, it doesn't work with RN.
In summary, layout in RN is async, and happens in another thread; yoga is the one that's responsible for computing the layout of the RN view (e.g. via "shadow components"). So when you request to change the size of the view, UIView.animate cannot "know" that the size/layout of the RN view has changed because it isn't applied to the UIView directly, but rather happens on a different pass outside the block of UIView.animate. in other words, the ff. code does not work:
UIView.animate(withDuration: 1.0) {
RCTUIManager.setSize(
for: reactView,
newSize: .init(width: 100, height: 200)
);
reactView.backgroundColor = .red;
}Because of this, in order to animate the size of a react view, setSize is called manually on each frame; however, there is a potential workaround that we can do that utilizes "snapshot views" (i.e. UIView.snapshotView(...)).
- Before the invocation of
UIView.animate, a snapshot of the "react view" is taken. - The original "react view" is hidden, and "react view snapshot" is superimposed on top of it.
- The size/layout of the "original react view" is then changed (e.g. via
setSizein native, or a layout change in JS). - Once the layout for the "original react view" is completed, it is traversed and compared with "react view snapshot"; taking note of the changes for each view (e.g. size, color, corner radius, etc).
UIView.animateis now invoked, and the changes from "original react view" is applied to "react view snapshot".- Once the animation is complete, "react view snapshot" is removed, and "original react view" is shown.
I am not sure yet if this will work in practice though; testing and experimentation is needed to verify. However if this works, it would now be possible to leverage the native animation API provided by UIKit (e.g. animating the changes to styles for example).
But again, RN already has Animated, and Reanimated; is making this really worth it?