SwiftUI 进阶动画
事务、阶段动画(iOS 17+)、关键帧动画(iOS 17+)、完成处理程序(iOS 17+)以及@Animatable宏(iOS 26+)。
目录
- 事务
- 阶段动画(iOS 17+)
- 关键帧动画(iOS 17+)
- 动画完成处理程序(iOS 17+)
- @Animatable 宏(iOS 26+)
事务
SwiftUI 中所有动画的底层机制。
基本用法
// withAnimation 是 withTransaction 的简写withAnimation(.default){flag.toggle()}// 等价的显式事务vartransaction=Transaction(animation:.default)withTransaction(transaction){flag.toggle()}.transaction 修饰符
Rectangle().frame(width:flag?100:50,height:50).transaction{tint.animation=.default}注意:它的行为类似于已弃用的.animation(_:)(不带 value 参数)——它会在每次状态变化时动画。
动画优先级
隐式动画覆盖显式动画(视图树中更靠后的胜出)。
Button("Tap"){withAnimation(.linear){flag.toggle()}}.animation(.bouncy,value:flag)// .bouncy 胜出!禁用动画
// 阻止隐式动画覆盖.transaction{tint.disablesAnimations=true}// 完全移除动画.transaction{$0.animation=nil}自定义事务键(iOS 17+)
通过事务传递元数据。
structChangeSourceKey:TransactionKey{staticletdefaultValue:String="unknown"}extensionTransaction{varchangeSource:String{get{self[ChangeSourceKey.self]}set{self[ChangeSourceKey.self]=newValue}}}// 设置来源vartransaction=Transaction(animation:.default)transaction.changeSource="server"withTransaction(transaction){flag.toggle()}// 在视图树中读取.transaction{tinift.changeSource=="server"{t.animation=.smooth}else{t.animation=.bouncy}}阶段动画(iOS 17+)
自动循环切换离散阶段。每个阶段变化都是一次独立的动画。
基本用法
// 好——触发的阶段动画Button("Shake"){trigger+=1}.phaseAnimator([0.0,-10.0,10.0,-5.0,5.0,0.0],trigger:trigger){content,offsetincontent.offset(x:offset)}// 无限循环(无触发)Circle().phaseAnimator([1.0,1.2,1.0]){content,scaleincontent.scaleEffect(scale)}枚举阶段(为清晰起见推荐)
// 好——枚举阶段自文档化enumBouncePhase:CaseIterable{caseinitial,up,down,settlevarscale:CGFloat{switchself{case.initial:1.0case.up:1.2case.down:0.9case.settle:1.0}}}Circle().phaseAnimator(BouncePhase.allCases,trigger:trigger){content,phaseincontent.scaleEffect(phase.scale)}每个阶段的自定义时序
.phaseAnimator([0,-20,20],trigger:trigger){content,offsetincontent.offset(x:offset)}animation:{phaseinswitchphase{case-20:.bouncycase20:.lineardefault:.smooth}}好与坏
// 好——多步序列使用 phaseAnimator.phaseAnimator([0,-10,10,0],trigger:trigger){content,offsetincontent.offset(x:offset)}// 坏——手动 DispatchQueue 排序Button("Animate"){withAnimation(.easeOut(duration:0.1)){offset=-10}DispatchQueue.main.asyncAfter(deadline:.now()+0.1){withAnimation{offset=10}}DispatchQueue.main.asyncAfter(deadline:.now()+0.2){withAnimation{offset=0}}}关键帧动画(iOS 17+)
在特定时间点提供精确值的精细时序控制。
基本用法
Button("Bounce"){trigger+=1}.keyframeAnimator(initialValue:AnimationValues(),trigger:trigger){content,valueincontent.scaleEffect(value.scale).offset(y:value.verticalOffset)}keyframes:{_inKeyframeTrack(\.scale){SpringKeyframe(1.2,duration:0.15)SpringKeyframe(0.9,duration:0.1)SpringKeyframe(1.0,duration:0.15)}KeyframeTrack(\.verticalOffset){LinearKeyframe(-20,duration:0.15)LinearKeyframe(0,duration:0.25)}}structAnimationValues{varscale:CGFloat=1.0varverticalOffset:CGFloat=0}关键帧类型
| 类型 | 行为 |
|---|---|
CubicKeyframe | 平滑插值 |
LinearKeyframe | 直线插值 |
SpringKeyframe | 弹簧物理 |
MoveKeyframe | 瞬间跳跃(无插值) |
多个同步轨道
轨道并行运行,每个轨道动画化一个属性。
// 好——带同步旋转和缩放的铃铛晃动structBellAnimation{varrotation:Double=0varscale:CGFloat=1.0}Image(systemName:"bell.fill").keyframeAnimator(initialValue:BellAnimation(),trigger:trigger){content,valueincontent.rotationEffect(.degrees(value.rotation)).scaleEffect(value.scale)}keyframes:{_inKeyframeTrack(\.rotation){CubicKeyframe(15,duration:0.1)CubicKeyframe(-15,duration:0.1)CubicKeyframe(10,duration:0.1)CubicKeyframe(-10,duration:0.1)CubicKeyframe(0,duration:0.1)}KeyframeTrack(\.scale){CubicKeyframe(1.1,duration:0.25)CubicKeyframe(1.0,duration:0.25)}}// 坏——手动基于定时器的动画Image(systemName:"bell.fill").onTapGesture{withAnimation(.easeOut(duration:0.1)){rotation=15}DispatchQueue.main.asyncAfter(deadline:.now()+0.1){withAnimation{rotation=-15}}// ... 更多手动时序——容易出错}KeyframeTimeline(iOS 17+)
直接查询动画值,用于测试或非 SwiftUI 场景。
lettimeline=KeyframeTimeline(initialValue:AnimationValues()){KeyframeTrack(\.scale){CubicKeyframe(1.2,duration:0.25)CubicKeyframe(1.0,duration:0.25)}}letmidpoint=timeline.value(time:0.25)print(midpoint.scale)// 0.25 秒时的值动画完成处理程序(iOS 17+)
动画完成时执行代码。
配合 withAnimation
// 好——withAnimation 的完成处理Button("Animate"){withAnimation(.spring){isExpanded.toggle()}completion:{showNextStep=true}}配合事务(用于重新执行)
// 好——每次触发变化时完成处理都会触发Circle().scaleEffect(bounceCount%2==0?1.0:1.2).transaction(value:bounceCount){transactionintransaction.animation=.spring transaction.addAnimationCompletion{message="Bounce\(bounceCount)complete"}}// 坏——完成处理只触发一次(没有 value 参数)Circle().scaleEffect(bounceCount%2==0?1.0:1.2).animation(.spring,value:bounceCount).transaction{transactionin// 没有 value!transaction.addAnimationCompletion{completionCount+=1// 永远只触发一次}}@Animatable 宏(iOS 26+)
@Animatable宏从所有可动画的存储属性自动合成animatableData,消除了冗长的手动遵循代码。使用@AnimatableIgnored排除不应动画化的属性。
之前(手动)
structWedge:Shape{varstartAngle:AnglevarendAngle:AnglevardrawClockwise:BoolvaranimatableData:AnimatablePair<Double,Double>{get{AnimatablePair(startAngle.radians,endAngle.radians)}set{startAngle=.radians(newValue.first)endAngle=.radians(newValue.second)}}funcpath(inrect:CGRect)->Path{/* ... */}}之后(@Animatable)
@AnimatablestructWedge:Shape{varstartAngle:AnglevarendAngle:Angle@AnimatableIgnoredvardrawClockwise:Boolfuncpath(inrect:CGRect)->Path{/* ... */}}何时使用
- 优先使用
@Animatable:适用于任何自定义Shape、AnimatableModifier或具有多个属性且遵循Animatable的类型 - 使用
@AnimatableIgnored:用于控制行为但不应插值的属性(例如方向、标志、标识符) - 宏适用于任何遵循
Animatable的类型,不只是Shape
来源:“What’s new in SwiftUI”(WWDC25,会话 256)
何时手动实现animatableData
当插值值需要无法与存储属性一一对应的自定义逻辑时——归一化、钳制或驱动派生值——请使用显式的animatableData(而不是宏)。部署目标为 iOS 26+ 时使用AnimatableValues;更早目标使用AnimatablePair。
// iOS 26+:在插值期间将 phase 保持在 0..<2π 并钳制 amplitudestructWaveShape:Shape{varamplitude:CGFloatvarphase:CGFloatvarmaxAmplitude:CGFloatvaranimatableData:AnimatableValues<CGFloat,CGFloat>{get{AnimatableValues(amplitude,phase)}set{amplitude=min(max(newValue.value.0,0),maxAmplitude)phase=newValue.value.1.truncatingRemainder(dividingBy:2*.pi)}}funcpath(inrect:CGRect)->Path{/* ... */}}在更早的部署目标上,同样的逻辑使用AnimatablePair配合newValue.first/newValue.second。
快速参考
事务(所有 iOS 版本)
withTransaction是withAnimation的显式形式- 隐式动画覆盖显式动画(视图树中更靠后的胜出)
- 使用
disablesAnimations防止覆盖 - 使用
.transaction { $0.animation = nil }移除动画
自定义事务键(iOS 17+)
- 通过
TransactionKey经动画系统传递元数据
阶段动画(iOS 17+)
- 用于返回起点的多步序列
- 为清晰起见优先使用枚举阶段
- 每个阶段变化都是一次独立的动画
- 一次性动画使用
trigger参数
关键帧动画(iOS 17+)
- 用于精确的时序控制
- 轨道并行运行
- 测试/高级用途使用
KeyframeTimeline - 优先于手动 DispatchQueue 时序
完成处理程序(iOS 17+)
- 一次性完成处理程序使用
withAnimation(.animation) { } completion: { } - 需要在每次值变化时重新触发的处理程序使用
.transaction(value:) - 没有
value:参数时,完成处理只触发一次
@Animatable 宏(iOS 26+)
- 使用
@Animatable从存储属性自动合成animatableData - 使用
@AnimatableIgnored排除不可动画的属性 - 取代冗长的手动
animatableDatagetter/setter