swiftui-expert-skill - animation-basics
SwiftUI 动画基础核心动画概念、隐式动画与显式动画、时间曲线以及性能模式。目录核心概念隐式动画显式动画动画放置位置选择性动画时间曲线动画性能禁用动画调试核心概念状态变化会触发视图更新。SwiftUI 提供了让这些变化产生动画的机制。动画过程状态变化触发视图树重新求值SwiftUI 将新视图树与当前渲染树进行比较识别可动画属性并进行插值约 60 fps关键特性动画是可叠加且可取消的总是从当前渲染树状态开始被中断时能够平滑混合隐式动画使用.animation(_:value:)在特定值变化时播放动画。// 好 - 使用 value 参数Rectangle().frame(width:isExpanded?200:100,height:50).animation(.spring,value:isExpanded).onTapGesture{isExpanded.toggle()}// 坏 - 已废弃会意外地动画所有变化Rectangle().frame(width:isExpanded?200:100,height:50).animation(.spring)// 已废弃显式动画对事件驱动的状态变化使用withAnimation。// 好 - 显式动画Button(Toggle){withAnimation(.spring){isExpanded.toggle()}}// 坏 - 没有动画上下文Button(Toggle){isExpanded.toggle()// 突变}何时使用哪种隐式动画动画与特定值变化绑定视图树作用域精确显式动画事件驱动的动画按钮点击、手势动画放置位置将动画修饰符放在它们要动画的属性之后。// 好 - 动画放在属性之后Rectangle().frame(width:isExpanded?200:100,height:50).foregroundStyle(isExpanded?.blue:.red).animation(.default,value:isExpanded)// 两者都动画// 坏 - 动画放在属性之前Rectangle().animation(.default,value:isExpanded)// 太早了.frame(width:isExpanded?200:100,height:50)选择性动画使用多个动画修饰符或作用域动画只动画特定属性。// 好 - 选择性动画Rectangle().frame(width:isExpanded?200:100,height:50).animation(.spring,value:isExpanded)// 动画尺寸.foregroundStyle(isExpanded?.blue:.red).animation(nil,value:isExpanded)// 不动画颜色// iOS 17 作用域动画Rectangle().foregroundStyle(isExpanded?.blue:.red)// 不动画.animation(.spring){$0.frame(width:isExpanded?200:100,height:50)// 动画}时间曲线内置曲线曲线使用场景.spring交互式元素、大多数 UI.easeInOut外观变化.bouncy俏皮的反馈iOS 17.linear仅用于进度指示器修饰符.animation(.default.speed(2.0),value:flag)// 快 2 倍.animation(.default.delay(0.5),value:flag)// 延迟启动.animation(.default.repeatCount(3,autoreverses:true),value:flag)好的时机与坏的时机// 好 - 与交互类型匹配的时机Button(Tap){withAnimation(.spring(response:0.3,dampingFraction:0.7)){isActive.toggle()}}.scaleEffect(isActive?0.95:1.0)// 坏 - 对按钮反馈来说太慢Button(Tap){withAnimation(.easeInOut(duration:1.0)){// 太慢了isActive.toggle()}}// 坏 - linear 感觉很机械Rectangle().animation(.linear(duration:0.5),value:isActive)// 机械动画性能优先使用变换而非布局// 好 - GPU 加速的变换Rectangle().frame(width:100,height:100).scaleEffect(isActive?1.5:1.0)// 快.offset(x:isActive?50:0)// 快.rotationEffect(.degrees(isActive?45:0))// 快.animation(.spring,value:isActive)// 坏 - 布局变化开销大Rectangle().frame(width:isActive?150:100,height:isActive?150:100)// 开销大.padding(isActive?50:0)// 开销大收窄动画作用域// 好 - 动画限定到特定子视图VStack{HeaderView()// 不受影响ExpandableContent(isExpanded:isExpanded).animation(.spring,value:isExpanded)// 仅此视图FooterView()// 不受影响}// 坏 - 动画放在根部VStack{HeaderView()ExpandableContent(isExpanded:isExpanded)FooterView()}.animation(.spring,value:isExpanded)// 动画所有内容避免在热点路径中动画// 好 - 用阈值门控.onPreferenceChange(ScrollOffsetKey.self){offsetinletshouldShowoffset.y-50ifshouldShow!showTitle{// 仅在跨越阈值时withAnimation(.easeOut(duration:0.2)){showTitleshouldShow}}}// 坏 - 每次滚动变化都动画.onPreferenceChange(ScrollOffsetKey.self){offsetinwithAnimation{// 持续触发self.offsetoffset.y}}禁用动画// 好 - 用事务禁用Text(Count:\(count)).transaction{$0.animationnil}// 好 - 从父上下文禁用DataView().transaction{$0.disablesAnimationstrue}// 坏 - 使用零时长的 hackText(Count:\(count)).animation(.linear(duration:0),value:count)// Hack调试// 放慢速度以便检查#ifDEBUG.animation(.linear(duration:3.0).speed(0.2),value:isExpanded)#else.animation(.spring,value:isExpanded)#endif// 记录数值的调试修饰符structAnimationDebugModifier:ViewModifier,Animatable{varvalue:DoublevaranimatableData:Double{get{value}set{valuenewValueprint(Animation:\(newValue))}}funcbody(content:Content)-someView{content.opacity(value)}}快速参考应该做使用带 value 参数的.animation(_:value:)对事件驱动的动画使用withAnimation优先使用变换而非布局变化收窄动画作用域选择合适的时间曲线不应该做使用不带 value 的已废弃.animation(_:)在热点路径中动画布局属性在根层级应用宽泛的动画对 UI 使用 linear 时间曲线感觉机械在滚动处理器中逐帧动画