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
[super
newWithView:{
[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 =
[CKComponent
newWithView:{
[UIView class],
{{@selector(setBackgroundColor:), UIColor.greenColor}}
}
size:...];
auto const c =
[super
newWithComponent:
[CKFlexboxComponent
newWithView:...
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 =
[CKComponent
newWithView:{
[UIView class],
{{@selector(setBackgroundColor:), isActive ? UIColor.greenColor : UIColor.grayColor}}
}
size:...];
auto const c =
[super
newWithComponent:
[CKFlexboxComponent
newWithView:...
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.