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.
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.
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(_: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.
| Props | Type | Description |
|---|---|---|
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. | unknown | Runs 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) => ReactNode | The content closure. |
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> )}| Props | Type | Description |
|---|---|---|
initialValue (required)ValueA number, or a record of up to eight numbers. | Value | A number, or a record of up to eight numbers. |
repeatingboolean = trueLoops the timeline. Not allowed with trigger. | boolean = true | Loops the timeline. Not allowed with trigger. |
triggerunknownPlays the timeline once each time it changes. | unknown | Plays 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) => ReactNode | The 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.
| Keyframe | SwiftUI | Options |
|---|---|---|
LinearKeyframe | Linear | duration, timingCurve |
SpringKeyframe | Spring | duration, spring, startVelocity |
CubicKeyframe | Cubic | duration, startVelocity, endVelocity |
MoveKeyframe | Move | none |
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 }.
| iOS | Android | Web | |
|---|---|---|---|
| Renders | SwiftUI timing | Throws | Throws |
| Frames | Main thread | None | None |
swiftStyle, element types,
and children must come out the same for every phase or value.keyframes function must not read its
argument.Edit this page on GitHub.