One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

iOS Animators

SwiftUI PhaseAnimator and KeyframeAnimator under One.iOS, run natively

One.iOS.PhaseAnimator and One.iOS.KeyframeAnimator are SwiftUI’s PhaseAnimator and KeyframeAnimator. SwiftUI runs the timing, interpolation, springs, and interruption on the main thread; JavaScript does not drive any frames. The content closure is the children function, and it returns native views as any other One.iOS container does.

Reach for PhaseAnimator to cycle or step through discrete phases, and KeyframeAnimator for a timeline over a number or record of numbers. Only swiftStyle may change with the phase or value: every other prop, the element types, and the children must come out the same. Both throw on web and Android.

Usage

import { One } from 'one'
function Liked({ likes }: { likes: number }) {
return (
<One.iOS.PhaseAnimator
phases={[0, 1, 2]}
trigger={likes}
animation={(phase) => (phase === 1 ? 'bouncy' : 'easeInOut')}
>
{(phase) => (
<One.iOS.Image
systemName="heart.fill"
swiftStyle={{ scaleEffectWithSAndAnchor: { s: phase === 1 ? 1.4 : 1, anchor: 'center' } }}
/>
)}
</One.iOS.PhaseAnimator>
)
}

Each change of likes runs every phase once. The heart scales up on phase 1 and settles back, with the animation into each phase picked by the animation function.

How it works

The content closure never runs per frame in JavaScript. Instead it runs during render, once per phase for PhaseAnimator, and at the initial value plus one unit step per field for KeyframeAnimator. The evaluations must agree on everything except swiftStyle; each differing style becomes an animated style carrying all of its evaluations, and SwiftUI computes every frame’s numbers natively from them.

For keyframes, each swiftStyle number must follow the value linearly, such as value.scale or 40 * value.width + 8. A checkpoint sample proves the linearity at render: a closure whose numbers curve with the value, or whose strings or colors change with it, throws. The keyframes function must not read its argument, since it is not bridged.

PhaseAnimator

PhaseAnimator(_:trigger:content:animation:) without a trigger cycles through phases while it is on screen. With trigger, each change, compared with Object.is, runs every phase once and returns to the first.

import { One } from 'one'
function LikedBadge({ likes }: { likes: number }) {
return (
<One.iOS.PhaseAnimator
phases={[0, 1, 2]}
trigger={likes}
animation={(phase) => (phase === 1 ? 'bouncy' : 'easeInOut')}
>
{(phase) => (
<One.iOS.Image
systemName="heart.fill"
swiftStyle={{ scaleEffectWithSAndAnchor: { s: phase === 1 ? 1.4 : 1, anchor: 'center' } }}
/>
)}
</One.iOS.PhaseAnimator>
)
}

The closure runs once per phase during render, and SwiftUI animates from one phase’s swiftStyle to the next. Colors that change by phase must be plain colors; a platform color throws. animation returns one of the named animations: default, interpolatingSpring, spring, interactiveSpring, smooth, snappy, bouncy, easeInOut, easeIn, easeOut, or linear, or a configured value such as { kind: 'easeOut', duration: 0.5 } or { kind: 'spring', response: 0.35, dampingFraction: 0.8 }. Use null to change without animation. Durations are in seconds. Unset, every phase uses default.

PropsTypeDescription
phases (required)Iterable<Phase>The phases, in order. Must not be empty.
Iterable<Phase>The phases, in order. Must not be empty.
triggerunknownRuns the phases once each time it changes. Omit it to cycle.
unknownRuns the phases once each time it changes. Omit it to cycle.
animation(phase: Phase) => SDKAnimationValue | null = 'default'A named or configured SDK animation into a phase, or null to change without one.
(phase: Phase) => SDKAnimationValue | null = 'default'A named or configured SDK animation into a phase, or null to change without one.
children (required)(phase: Phase) => ReactNodeThe content closure.
(phase: Phase) => ReactNodeThe content closure.

KeyframeAnimator

KeyframeAnimator(initialValue:repeating:content:keyframes:) and KeyframeAnimator(initialValue:trigger:content:keyframes:). The value is a number or a record of up to eight numbers. With repeating, the default without a trigger, the timeline loops; with trigger it plays once per change. Passing both throws.

import { One } from 'one'
function Done({ count }: { count: number }) {
return (
<One.iOS.KeyframeAnimator
initialValue={{ scale: 1, lift: 0 }}
trigger={count}
keyframes={() => [
One.iOS.KeyframeTrack('scale', [
One.iOS.LinearKeyframe(1.2, { duration: 0.2 }),
One.iOS.SpringKeyframe(1, { spring: 'bouncy' }),
]),
One.iOS.KeyframeTrack('lift', [
One.iOS.CubicKeyframe(-30, { duration: 0.3 }),
One.iOS.MoveKeyframe(0),
]),
]}
>
{(value) => (
<One.iOS.Text
text="Done"
swiftStyle={{ scaleEffectWithSAndAnchor: { s: value.scale, anchor: 'center' }, offset: { x: 0, y: value.lift } }}
/>
)}
</One.iOS.KeyframeAnimator>
)
}
PropsTypeDescription
initialValue (required)ValueA number, or a record of up to eight numbers.
ValueA number, or a record of up to eight numbers.
repeatingboolean = trueLoops the timeline. Not allowed with trigger.
boolean = trueLoops the timeline. Not allowed with trigger.
triggerunknownPlays the timeline once each time it changes.
unknownPlays the timeline once each time it changes.
keyframes (required)(value: Value) => readonly KeyframeTrack<Value>[] | readonly Keyframe<Value>[]Tracks, or keyframes for the whole value.
(value: Value) => readonly KeyframeTrack<Value>[] | readonly Keyframe<Value>[]Tracks, or keyframes for the whole value.
children (required)(value: Value) => ReactNodeThe content closure.
(value: Value) => ReactNodeThe content closure.

KeyframeTrack(keyPath, keyframes) animates one field; KeyframeTrack(keyframes) or a bare list of keyframes animates the whole value. A field takes one track, and tracks and bare keyframes must not mix. Durations must be finite and nonnegative; velocities, curves, and springs are checked at render.

KeyframeSwiftUIOptions
LinearKeyframeLinearduration, timingCurve
SpringKeyframeSpringduration, spring, startVelocity
CubicKeyframeCubicduration, startVelocity, endVelocity
MoveKeyframeMovenone

timingCurve is a UnitCurve name, linear, easeIn, easeOut, easeInOut, circularEaseIn, circularEaseOut, or circularEaseInOut, or a bezier { startControlPoint, endControlPoint }. spring is smooth, snappy, bouncy, or a Spring initializer: { duration, bounce }, { mass, stiffness, damping }, { response, dampingRatio }, or { settlingDuration, dampingRatio }.

Platform differences

iOSAndroidWeb
RendersSwiftUI timingThrowsThrows
FramesMain threadNoneNone

Limits and gotchas

Edit this page on GitHub.