Community-maintained Agent Skills for complete Swift and Apple-platform app delivery.
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
Detailed API reference for SwiftUI animation types, protocols, and patterns. Covers material beyond the SKILL.md summary.
Create entirely custom animation curves by conforming to CustomAnimation.
@preconcurrency protocol CustomAnimation: Hashable, Sendablefunc animate<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> V?Return the interpolated value at the given time. Return nil when the
animation is complete.
func velocity<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: AnimationContext<V>
) -> V?
func shouldMerge<V: VectorArithmetic>(
previous: Animation,
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> Boolstruct ElasticAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> V? {
guard time <= duration else { return nil }
let p = time / duration
let s = sin((20 * p - 11.125) * ((2 * .pi) / 4.5))
let progress: Double
if p < 0.5 {
progress = -(pow(2, 20 * p - 10) * s) / 2
} else {
progress = (pow(2, -20 * p + 10) * s) / 2 + 1
}
return value.scaled(by: progress)
}
}Expose custom animations as static members on Animation.
extension Animation {
static var elastic: Animation {
elastic(duration: 0.35)
}
static func elastic(duration: TimeInterval) -> Animation {
Animation(ElasticAnimation(duration: duration))
}
}
// Usage
withAnimation(.elastic(duration: 0.5)) { isActive.toggle() }| Type | Role |
|---|---|
AnimationContext<V> | Carries environment and per-animation state |
AnimationState | Key-value storage for persisted state |
AnimationStateKey | Protocol for defining custom state keys |
Spring(duration: 0.5, bounce: 0.0)duration -- Perceptual duration controlling pace. Default 0.5.bounce -- Bounciness. 0.0 = no bounce, 1.0 = undamped. Negative values
produce overdamped springs. Default 0.0.Spring(mass: 1.0, stiffness: 100.0, damping: 10.0, allowOverDamping: false)mass -- Mass at end of spring. Default 1.0.stiffness -- Spring stiffness coefficient.damping -- Friction-like drag force.allowOverDamping -- Permit damping ratio > 1. Default false.Spring(response: 0.5, dampingRatio: 0.7)response -- Stiffness expressed as approximate duration in seconds.dampingRatio -- Fraction of critical damping. 1.0 = critically damped.Spring(settlingDuration: 1.0, dampingRatio: 0.8, epsilon: 0.001)settlingDuration -- Estimated time to come to rest.dampingRatio -- Fraction of critical damping.epsilon -- Threshold for considering the spring at rest. Default 0.001.Spring.smooth // no bounce
Spring.smooth(duration: 0.5, extraBounce: 0.0)
Spring.snappy // small bounce
Spring.snappy(duration: 0.4, extraBounce: 0.1)
Spring.bouncy // visible bounce
Spring.bouncy(duration: 0.5, extraBounce: 0.2)let spring = Spring(duration: 0.5, bounce: 0.3)
let v = spring.value(target: 1.0, initialVelocity: 0.0, time: 0.25)
let vel = spring.velocity(target: 1.0, initialVelocity: 0.0, time: 0.25)
let settle = spring.settlingDuration(target: 1.0, initialVelocity: 0.0, epsilon: 0.001)let spring = Spring(duration: 0.5, bounce: 0.3)
// Access physical equivalents:
spring.mass // 1.0
spring.stiffness // 157.9
spring.damping // 17.6
spring.response
spring.dampingRatio
spring.settlingDurationMap input progress [0,1] to output progress [0,1]. Used with
.timingCurve(_:duration:).
UnitCurve.linear
UnitCurve.easeIn
UnitCurve.easeOut
UnitCurve.easeInOut
UnitCurve.circularEaseIn
UnitCurve.circularEaseOut
UnitCurve.circularEaseInOutUnitCurve.bezier(
startControlPoint: UnitPoint(x: 0.42, y: 0.0),
endControlPoint: UnitPoint(x: 0.58, y: 1.0)
)let curve = UnitCurve.easeInOut
curve.value(at: 0.5) // output progress at midpoint
curve.velocity(at: 0.5) // rate of change at midpoint
curve.inverse // swaps x and y components.animation(.timingCurve(UnitCurve.circularEaseIn, duration: 0.4), value: x)
// Cubic bezier control points
.animation(.timingCurve(0.68, -0.55, 0.27, 1.55, duration: 0.5), value: x)enum LoadPhase: CaseIterable {
case ready, loading, spinning, complete
var scale: Double {
switch self {
case .ready: 1.0
case .loading: 0.9
case .spinning: 1.0
case .complete: 1.1
}
}
var rotation: Angle {
switch self {
case .spinning: .degrees(360)
default: .zero
}
}
var opacity: Double {
self == .loading ? 0.7 : 1.0
}
}
struct LoadingIndicator: View {
var body: some View {
PhaseAnimator(LoadPhase.allCases) { phase in
Image(systemName: "arrow.triangle.2.circlepath")
.font(.title)
.scaleEffect(phase.scale)
.rotationEffect(phase.rotation)
.opacity(phase.opacity)
} animation: { phase in
switch phase {
case .ready: .smooth(duration: 0.2)
case .loading: .easeIn(duration: 0.15)
case .spinning: .linear(duration: 0.6)
case .complete: .spring(duration: 0.3, bounce: 0.4)
}
}
}
}Each trigger change advances to the next phase. It does not run the full phase
array as a one-shot sequence; use KeyframeAnimator or explicit phase state
when each tap should play a complete timeline.
struct FeedbackDot: View {
@State private var feedbackTrigger = 0
var body: some View {
Button { feedbackTrigger += 1 } label: {
Circle()
.frame(width: 20, height: 20)
.phaseAnimator(
[false, true],
trigger: feedbackTrigger
) { content, phase in
content.scaleEffect(phase ? 1.5 : 1.0)
} animation: { _ in
.spring(duration: 0.25, bounce: 0.5)
}
}
.buttonStyle(.plain)
}
}Text("Hello")
.phaseAnimator([0.0, 1.0, 0.0]) { content, phase in
content.opacity(phase)
} animation: { _ in .easeInOut(duration: 0.8) }struct BounceValues {
var yOffset: Double = 0
var scale: Double = 1.0
var opacity: Double = 1.0
var rotation: Angle = .zero
}
struct BouncingBadge: View {
@State private var trigger = false
var body: some View {
Button { trigger.toggle() } label: {
Text("NEW")
.font(.caption.bold())
.padding(.horizontal)
.background(.red, in: Capsule())
.keyframeAnimator(
initialValue: BounceValues(),
trigger: trigger
) { content, value in
content
.offset(y: value.yOffset)
.scaleEffect(value.scale)
.opacity(value.opacity)
.rotationEffect(value.rotation)
} keyframes: { _ in
KeyframeTrack(\.yOffset) {
SpringKeyframe(-20, duration: 0.2)
CubicKeyframe(5, duration: 0.15)
SpringKeyframe(0, duration: 0.3)
}
KeyframeTrack(\.scale) {
CubicKeyframe(1.3, duration: 0.2)
CubicKeyframe(0.95, duration: 0.15)
SpringKeyframe(1.0, duration: 0.3)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(.degrees(-5), duration: 0.1)
LinearKeyframe(.degrees(5), duration: 0.1)
SpringKeyframe(.zero, duration: 0.2)
}
KeyframeTrack(\.opacity) {
MoveKeyframe(1.0)
}
}
}
.buttonStyle(.plain)
}
}KeyframeAnimator(
initialValue: PulseValues(),
repeating: true
) { value in
Circle()
.fill(.blue)
.frame(width: 40, height: 40)
.scaleEffect(value.scale)
.opacity(value.opacity)
} keyframes: { _ in
KeyframeTrack(\.scale) {
CubicKeyframe(1.3, duration: 0.5)
CubicKeyframe(1.0, duration: 0.5)
}
KeyframeTrack(\.opacity) {
CubicKeyframe(0.6, duration: 0.5)
CubicKeyframe(1.0, duration: 0.5)
}
}| Type | Interpolation | Use case |
|---|---|---|
LinearKeyframe(value, duration:) | Straight line between values | Steady movement |
CubicKeyframe(value, duration:) | Cubic bezier curve | Smooth easing |
SpringKeyframe(value, duration:, spring:) | Spring physics | Natural settle |
MoveKeyframe(value) | Instant jump | Reset to value immediately |
keyframeAnimator content and keyframe closures are @Sendable. In Swift 6,
avoid direct reads of @State or @Environment from nested helper closures
inside the modifier; capture plain values in body before the modifier or pass
them through the animated value model.
let timeline = KeyframeTimeline(initialValue: AnimValues()) {
KeyframeTrack(\.scale) {
CubicKeyframe(1.5, duration: 0.3)
CubicKeyframe(1.0, duration: 0.4)
}
}
let totalDuration = timeline.duration
let valueAtHalf = timeline.value(time: totalDuration / 2)A Transaction carries the animation context for a state change. Every
withAnimation call creates a transaction internally.
// Explicit transaction
var transaction = Transaction(animation: .spring)
withTransaction(transaction) {
isExpanded = true
}// Remove the incoming transaction animation for this scoped content
SomeView()
.transaction { transaction in
transaction.animation = nil
}
// Override the scoped transaction animation when a value changes
SomeView()
.transaction(value: selectedTab) { transaction in
transaction.animation = .smooth(duration: 0.3)
}Store custom metadata in transactions.
struct IsInteractiveKey: TransactionKey {
static let defaultValue = false
}
extension Transaction {
var isInteractive: Bool {
get { self[IsInteractiveKey.self] }
set { self[IsInteractiveKey.self] = newValue }
}
}
// Usage
var transaction = Transaction(animation: .interactiveSpring)
transaction.isInteractive = true
withTransaction(transaction) { dragOffset = newOffset }// Apply transaction only within a body closure
ParentView()
.transaction { $0.animation = .spring } body: { content in
content.scaleEffect(scale)
}Use .animation(_:body:) when only selected modifiers should animate.
Use .animation(_:value:) when a single value change should drive the view's
animatable modifiers together. Use .transaction(_:body:) when you need to
scope transaction overrides rather than attach one animation.
CardView(isExpanded: isExpanded)
.animation(.smooth) { content in
content
.scaleEffect(isExpanded ? 1.05 : 1.0)
.shadow(radius: isExpanded ? 12 : 4)
}| Transition | Description | Example |
|---|---|---|
.opacity | Fade in/out | .transition(.opacity) |
.slide | Slide from leading, exit trailing | .transition(.slide) |
.scale | Scale from zero | .transition(.scale) |
.scale(_:anchor:) | Scale with amount and anchor | .transition(.scale(0.5, anchor: .bottom)) |
.move(edge:) | Move from specified edge | .transition(.move(edge: .top)) |
.push(from:) | Push from edge with fade | .transition(.push(from: .trailing)) |
.offset(_:) | Offset by CGSize | .transition(.offset(CGSize(width: 0, height: 50))) |
.offset(x:y:) | Offset by x and y | .transition(.offset(x: 0, y: -100)) |
.identity | No visual change | .transition(.identity) |
.blurReplace | Blur and scale combined | .transition(.blurReplace) |
.blurReplace(_:) | Configurable blur replace | .transition(.blurReplace(.downUp)) |
.symbolEffect | Default symbol effect | .transition(.symbolEffect) |
.symbolEffect(_:options:) | Custom symbol effect | .transition(.symbolEffect(.appear)) |
// Slide + fade
.transition(.slide.combined(with: .opacity))
// Move from top + scale
.transition(.move(edge: .top).combined(with: .scale))Different animation for insertion vs removal.
.transition(.asymmetric(
insertion: .push(from: .bottom).combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))struct RotateTransition: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content
.rotationEffect(phase.isIdentity ? .zero : .degrees(90))
.opacity(phase.isIdentity ? 1 : 0)
}
}
extension AnyTransition {
static var rotate: AnyTransition {
.init(RotateTransition())
}
}enum TransitionPhase {
case willAppear // View is about to be inserted
case identity // View is fully presented
case didDisappear // View is being removed
}
// Check current phase
phase.isIdentity // true when fully presented.transition(
.move(edge: .bottom)
.combined(with: .opacity)
.animation(.spring(duration: 0.4, bounce: 0.2))
).tessl-plugin
skills
accessorysetupkit
references
activitykit
adattributionkit
references
alarmkit
references
app-clips
app-intents
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
audioaccessorykit
references
authentication
references
avfoundation-audio
references
avkit
background-processing
references
browserenginekit
callkit
references
carplay
cloudkit
contacts-framework
references
core-bluetooth
references
core-data
core-haptics
references
core-motion
references
core-nfc
references
coreml
references
cryptokit
cryptotokenkit
debugging-instruments
device-integrity
references
energykit
references
eventkit
family-controls
references
financekit
focus-engine
gamekit
git-branching-workflow
references
healthkit
references
homekit
references
ios-accessibility
ios-app-workflow
references
ios-ettrace-performance
ios-localization
ios-memgraph-analysis
ios-networking
ios-simulator
references
metrickit
references
musickit
references
natural-language
paperkit
references
passkit
references
pdfkit
pencilkit
references
permissionkit
references
photokit
push-notifications
quicklook
references
realitykit
references
relevancekit
references
scenekit
sensorkit
shazamkit
references
speech-recognition
references
spritekit
storekit
swift-api-design-guidelines
swift-architecture
references
swift-charts
swift-codable
references
swift-code-review
swift-concurrency
swift-formatstyle
references
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-responsive-layout
swiftui-uikit-interop
swiftui-webkit
tabletopkit
tipkit
vision-framework
weatherkit
references