Change Animations
To animate a change in a component, two conditions must be met:
it needs to have a view or eventually render to a view
it needs to declare a scope so the infra is able to find the same logical component in both previous and current tree
The actual animation should be constructed and returned in the animationsFromPreviousComponent:
method in the form of a CKComponentAnimation
instance. In the vast majority of cases, CKComponentAnimation
is created from a component / animation pair.
Let's see how this all applies in practice. Consider a component that changes it's background colour depending on one of its props:
+ (instancetype)newWithActive:(BOOL)isActive{CKComponentScope scope(self);return[supernewWithView:{[UIView class],{{@selector(setBackgroundColor:), isActive ? UIColor.green : UIColor.gray}}}size:{}];}
Note, that the background colour is set as a part of the view configuration. Now, to change the background colour in an animated fashion, we need to provide an implementation for animationFromPreviousComponent:
:
- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent{return {{self, CK::Animation::backgroundColor()}};}
and that's pretty much it. Note that we didn't have to specify the from
and to
animation values at all, since both are inferred from the view configuration.
Another (somewhat obvious) observation here is that, if you want to animate a change in a child component, not self
, you need to store a reference to the animated component in an ivar on the parent component. Here's an example:
@implementation Example {CKComponent *_greenBox;}+ (instancetype)newWithTrailing:(BOOL)isTrailing{auto const greenBox =[CKComponentnewWithView:{[UIView class],{{@selector(setBackgroundColor:), UIColor.greenColor}}}size:...];auto const c =[supernewWithComponent:[CKFlexboxComponentnewWithView:...size:{}style:{.direction = CKFlexboxDirectionRow,.justifyContent = isTrailing ? CKFlexboxJustifyContentEnd : CKFlexboxJustifyContentStart}children:{{greenBox}}]];c->_greenBox = greenBox;return c;}
This component lays out it's only child component (a green box) either in a leading or in a trailing horizontal position depending on the isTrailing
prop. In order to animate this change in the position of the green box we also need animationsFromPreviousComponent:
implementation:
- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent{return {{_greenBox, CK::Animation::position().easeOut()}};}
Here, we used the stored reference to the child component to initialise the returned instance of CKComponentAnimation
. Most importantly, we didn't need to explicitly specify from
and to
values again because both will be inferred again, this time not from the view configuration specifically but from the bounds
and center
properties that are set at mount time.
Conditional Animations
What if we put both these examples together and allow for changes in both background colour and position by passing both flags in props:
@implementation Example {CKComponent *_box;}+ (instancetype)newWithActive:(BOOL)isActive trailing:(BOOL)isTrailing{auto const box =[CKComponentnewWithView:{[UIView class],{{@selector(setBackgroundColor:), isActive ? UIColor.greenColor : UIColor.grayColor}}}size:...];auto const c =[supernewWithComponent:[CKFlexboxComponentnewWithView:...size:{}style:{.direction = CKFlexboxDirectionRow,.justifyContent = isTrailing ? CKFlexboxJustifyContentEnd : CKFlexboxJustifyContentStart}children:{{box}}]];c->_box = box;return c;}
You can combine animations for both properties using parallel()
combinator like this:
- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent{return {{_greenBox, CK::Animation::parallel(CK::Animation::position().easeOut(), CK::Animation::backgroundColor())}};}
However, if only one of the flags (isActive
or isTrailing
) changed as a result of a state update, there is no need to animate the other property. It won't have any visual effect in this case but still puts additional load on the infra. The component tree may also be regenerated for other reasons, such as state updates in other components, so the animations should be conditional. This leads us to a conclusion that, in most cases, there has to be something in the state or props that will tell you if animation is needed. The relevant information needs to be saved as an ivar and used in animationsFromPreviousComponent:
like this:
@implementation Example {CKComponent *_box;BOOL _isActive;BOOL _isTrailing;}+ (instancetype)newWithActive:(BOOL)isActive trailing:(BOOL)isTrailing{...c->_box = box;c->_isActive = isActive;c->_isTrailing = isTrailing;return c;}- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent{auto const prev = CK::objCForceCast<Example>(previousComponent);auto animations = std::vector<CKComponentAnimation>{};if (prev->_isActive != _isActive) {animations.push_back({_box, CK::Animation::backgroundColor()});}if (prev->_isTrailing != isTrailing) {animations.push_back({_box, CK::Animation::position().easeOut()});}return animations;}
In order to animate a change you just need to set the proper values in the view configuration or alter the layout, and return one or more animations for the corresponding properties from animationsFromPreviousComponent:
without specifying initial and final values. Make sure to return only animations for properties that have actually changed.