SwiftUI动画期间禁用默认插值,实现视图逐帧重绘需求咨询
解决SwiftUI动画中禁用默认插值、逐帧重绘视图的问题
我希望下方示例中的视图在数值变化的动画过程中,不使用SwiftUI默认的插值逻辑,而是直接重绘整个视图。拖动滑块从10到100的效果是符合预期的,我想让动画也达到同样的效果——不是让所有视图渐入,而是在动画时长内逐帧单独绘制每个步骤。我试过带关键帧和不带关键帧的自定义动画,还有自定义转场,但都没得到想要的结果。
import Foundation import SwiftUI struct SampleView: View { private let value: Int init(value: Double) { self.value = Int(max(min(value, 1.0), 0.0) * 100) } var body: some View { VStack(spacing: 0.0) { ForEach(0 ..< 101) { value in Rectangle() .fill(color(for: value)) .frame(width: 100, height: 6.0) .opacity(value < self.value ? 1.0 : 0.0) } } } private func color(for value: Int) -> Color { switch value % 4 { case 0: return Color.red case 1: return Color.blue case 2: return Color.yellow case 3: return Color.green default: return Color.black } } } #Preview("Interactive Sample") { struct InteractiveTestView: View { @State var value: Double = 0.1 var body: some View { VStack { Text("\(Int(value * 100))%").font(.largeTitle) Button("Set to max") { withAnimation(.easeInOut(duration: 5.0)) { value = 0.9 } } Slider(value: $value, in: 0.0...1.0, step: 0.01).padding() SampleView(value: value) } } } return InteractiveTestView() }
核心解决方案
SwiftUI默认会对视图的可动画属性(比如opacity)做平滑插值过渡,要实现逐帧重绘,关键是让SampleView在动画的每一帧都根据当前的整数value完全重建,而非对浮点中间值做插值。以下两种方法可以实现需求:
方法一:定时器驱动整数逐帧更新
通过定时器按动画时长拆分步骤,逐帧更新整数value,让SampleView每次都完全重建,完全规避插值逻辑:
import SwiftUI class ValueViewModel: ObservableObject { @Published var intValue: Int = 10 func update(to target: Double, duration: TimeInterval) { let targetInt = Int(max(min(target, 1.0), 0.0) * 100) let steps = targetInt - intValue guard steps != 0 else { return } let stepDuration = duration / Double(abs(steps)) let step = steps > 0 ? 1 : -1 Timer.scheduledTimer(withTimeInterval: stepDuration, repeats: true) { timer in self.intValue += step if self.intValue == targetInt { timer.invalidate() } } } } struct SampleView: View { let value: Int var body: some View { VStack(spacing: 0.0) { ForEach(0 ..< 101) { value in Rectangle() .fill(color(for: value)) .frame(width: 100, height: 6.0) .opacity(value < self.value ? 1.0 : 0.0) } } } private func color(for value: Int) -> Color { switch value % 4 { case 0: return .red case 1: return .blue case 2: return .yellow case 3: return .green default: return .black } } } #Preview("Interactive Sample") { struct InteractiveTestView: View { @StateObject private var vm = ValueViewModel() @State private var sliderValue: Double = 0.1 var body: some View { VStack { Text("\(vm.intValue)%").font(.largeTitle) Button("Set to max") { vm.update(to: 0.9, duration: 5.0) } Slider(value: $sliderValue, in: 0.0...1.0, step: 0.01) .padding() .onChange(of: sliderValue) { newValue in vm.intValue = Int(max(min(newValue, 1.0), 0.0) * 100) } SampleView(value: vm.intValue) } } } return InteractiveTestView() }
方法二:通过事务禁用属性插值
修改视图事务,强制SwiftUI不对SampleView的属性做插值,配合withAnimation控制时长,实现逐帧重绘:
import SwiftUI struct SampleView: View { let value: Int var body: some View { VStack(spacing: 0.0) { ForEach(0 ..< 101) { value in Rectangle() .fill(color(for: value)) .frame(width: 100, height: 6.0) .opacity(value < self.value ? 1.0 : 0.0) } } .transaction { transaction in // 禁用该视图及其子视图的属性插值 transaction.disablesAnimations = true } } private func color(for value: Int) -> Color { switch value % 4 { case 0: return .red case 1: return .blue case 2: return .yellow case 3: return .green default: return .black } } } #Preview("Interactive Sample") { struct InteractiveTestView: View { @State private var value: Double = 0.1 @State private var animatedIntValue: Int = 10 var body: some View { VStack { Text("\(animatedIntValue)%").font(.largeTitle) Button("Set to max") { let targetInt = Int(max(min(0.9, 1.0), 0.0) * 100) withAnimation(.linear(duration: 5.0)) { animatedIntValue = targetInt } } Slider(value: $value, in: 0.0...1.0, step: 0.01) .padding() .onChange(of: value) { newValue in animatedIntValue = Int(max(min(newValue, 1.0), 0.0) * 100) } SampleView(value: animatedIntValue) } } } return InteractiveTestView() }
效果说明
- 方法一通过定时器精准控制每一步的整数更新,效果和拖动滑块完全一致,无任何插值过渡。
- 方法二利用事务禁用插值,让SwiftUI在动画周期内,根据当前整数
value逐帧重绘整个视图,同样能达到需求的效果。
内容的提问来源于stack exchange,提问作者user23507826
相关产品推荐
相关产品推荐

