应用从后台返回后,圆形进度环会短暂地向后旋转一次
我有一个SwiftUI iOS应用,屏幕名为“工作时间”。
在该屏幕上,一个带有60个刻度的圆环在跟踪运行时持续旋转。应用本身不再崩溃,但仍然存在一个视觉问题:
- 应用进入后台一段时间后再回到前台,圆环会短暂地朝相反方向移动一个小步一次,然后继续顺时针正常旋转。
- 未显示运行时错误,这只是一个视觉问题。
预期结果:在前台恢复后,圆环应平滑地以相同方向继续旋转,不应有反向的步骤。
实际结果:在前台恢复后,会先出现一个短暂的“反向刻度”,随后再恢复正常旋转。
我尝试了几种方法,仍然会出现反向步进:
withAnimation(.linear(...).repeatForever)+在前台重新启动。- 使用
TimelineView的时间线驱动旋转,并搭配.animation与.periodic两种调度。 - 旋转角度带模与不带模包装。
- 基于墙钟时间和基于单调运行时的旋转。
- 渲染调整(几何中的Canvas旋转vs
rotationEffect,有/无预热延迟)。 - 不同的圆环视觉效果(统一刻度vs长短的类似小时刻度)。
问题仍然可复现。
import SwiftUI
struct ReproClockView: View {
@Environment(\.scenePhase) private var scenePhase
private let tickCount = 60
private let rotationDuration: Double = 120
@State private var rotationDegrees: Double = 0
@State private var hasAppeared = false
var body: some View {
ZStack {
Canvas { context, size in
let center = CGPoint(x: size.width / 2, y: size.height / 2)
let radius = min(size.width, size.height) / 2 - 8
for index in 0..<tickCount {
let angle = Double(index) * (360.0 / Double(tickCount)) - 90
let radians = angle * .pi / 180
let isLongTick = index % 5 == 0
let length: CGFloat = isLongTick ? 22 : 14
let outerRadius = radius
let innerRadius = radius - length
let startPoint = CGPoint(
x: center.x + cos(radians) * outerRadius,
y: center.y + sin(radians) * outerRadius
)
let endPoint = CGPoint(
x: center.x + cos(radians) * innerRadius,
y: center.y + sin(radians) * innerRadius
)
var path = Path()
path.move(to: startPoint)
path.addLine(to: endPoint)
context.stroke(
path,
with: .color(.red),
style: StrokeStyle(lineWidth: 2.5, lineCap: .round)
)
}
}
.rotationEffect(.degrees(rotationDegrees))
.drawingGroup()
}
.frame(width: 340, height: 340)
.onAppear {
guard !hasAppeared else { return }
hasAppeared = true
startRotation()
}
.onChange(of: scenePhase) { oldPhase, newPhase in
if oldPhase == .background && newPhase == .active {
withAnimation(.linear(duration: 0)) {
rotationDegrees = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
startRotation()
}
}
}
}
private func startRotation() {
rotationDegrees = 0
withAnimation(.linear(duration: rotationDuration).repeatForever(autoreverses: false)) {
rotationDegrees = 360
}
}
}
要实现一个在后台/前台切换时仍保持方向稳定的连续旋转圆环(没有一个帧的反向步骤),应该采用什么样的正确SwiftUI做法?
如果可能,请给出一个在当前iOS版本上都稳健、在场景阶段变化时避免视觉伪影的模式。
解决方案
这个问题其实关乎iOS在将应用移到后台时拍摄快照的确切时刻。
我在模拟器上通过Device > App Switcher切换到另一个应用(Settings)再切回来时,能够重现这个问题。几点说明:
- 首次调出App Switcher时,场景阶段从
active变化到inactive。在这个状态下,动画仍在运行。 - 选择其他应用时,场景阶段从
inactive变化到background。在这个状态下,动画不会运行。 - 截图是在阶段变为
background之后拍摄的。 - 由于你看到时钟会往回跳一点,我猜在阶段仍然是
inactive、紧接着变为background之前就记录了与动画相关的状态。然后动画从这个记录的状态恢复。也可能动画是从重置位置开始。无论如何,动画重新开始时的位置都不是截图时的位置。
为了解决这个问题,可以考虑在场景阶段变为 background 时重置并冻结动画。这样,快照始终处于重置位置,应用再次活跃时动画总是从该位置开始。
下面给出一个按此方式工作的代码更新版本。并包含了一些其他简化:
- 我认为如果在
.onAppear中无条件启动动画就能正常工作。因此,hasAppeared这个标志不再需要。 - 当场景阶段为
background时显示冻结的时钟;否则显示动画时钟。不需要再通过.onChange修饰符来跟踪阶段变化。 - 在我的测试中,我尝试了用
Capsule形状绘制时钟的实现。我认为这个版本实际上比你原来使用的Canvas实现更省CPU。当在模拟器上运行时,胶囊版本的CPU占用在一段时间后降至3-5%,而你原始版本则始终高于10%。因此我保留了这个时钟实现,并且代码也更少。
struct ReproClockView: View {
@Environment(\.scenePhase) private var scenePhase
private let tickCount = 60
private let rotationDuration: Double = 120
private let clockSize: CGFloat = 340
@State private var rotationDegrees: Double = 0
private var clock: some View {
ZStack {
ForEach(0..<tickCount, id: \.self) { index in
let angleDegrees = Double(index) * (360.0 / Double(tickCount))
let isLongTick = index % 5 == 0
let length: CGFloat = isLongTick ? 24 : 16
Capsule()
.frame(width: 2.5, height: length)
.offset(y: (length / 2) - (clockSize / 2) + 7)
.rotationEffect(.degrees(angleDegrees))
}
}
.frame(width: clockSize, height: clockSize)
}
var body: some View {
ZStack {
if scenePhase == .background {
clock
} else {
clock
.drawingGroup()
.rotationEffect(.degrees(rotationDegrees))
.onAppear {
withAnimation(
.linear(duration: rotationDuration)
.repeatForever(autoreverses: false)
) {
rotationDegrees = 360
}
}
.onDisappear { rotationDegrees = 0 }
}
}
.foregroundStyle(.red)
}
}

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。