我最近在容器View
中遇到了一个问题,该容器包含View
个项目的嵌套列表,这些项目使用repeatForever
动画,当第一次绘制并在跳转后跳动时效果很好兄弟项目会动态添加。
View
的列表是根据ObservableObject
属性动态生成的,在这里用Loop
表示。它是在后台线程(AVAudioPlayerNodeImpl.CompletionHandlerQueue)中进行计算之后生成的。
Loop
View
动画的duration
等于其传递的参数duration
的动态player
属性值。每个Loop
都有自己的值,每个同级值可以相同也可以不相同。
创建第一个Loop
View
时,动画可以完美工作,但在列表中包含新项目后,动画会变得跳动。这意味着,动画对于尾部项目(列表中的最后一项或最新成员)正确运行,而对前一个项目错误地运行。
从我的角度来看,这似乎与SwiftUI的重绘方式有关,并且我的知识有所欠缺,这导致实现导致动画状态分散的实现。问题是什么原因导致了这种情况,或者如何防止这种情况将来发生?
我已最小化实现,以提高清晰度并专注于主题。
让我们看一下容器视图:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var engine: Engine
fileprivate func Loop(duration: Double, play: Bool) -> some View {
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 10.0))
.foregroundColor(Color.purple)
.opacity(0.3)
.overlay(
Circle()
.trim(
from: 0,
to: play ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 10.0,
lineCap: .round,
lineJoin: .round)
)
.animation(
self.audioEngine.isPlaying ?
Animation
.linear(duration: duration)
.repeatForever(autoreverses: false) :
.none
)
.rotationEffect(Angle(degrees: -90))
.foregroundColor(Color.purple)
)
}
.frame(width: 100, height: 100)
.padding()
}
var body: some View {
VStack {
ForEach (0 ..< self.audioEngine.players.count, id: \.self) { index in
HStack {
self.Loop(duration: self.engine.players[index].duration, play: self.engine.players[index].isPlaying)
}
}
}
}
}
在Body
中,您会找到一个ForEach
,它监视着Players
的列表和@Published
中的Engine
属性。
看看Engine
类:
Class Engine: ObservableObject {
@Published var players = []
func record() {
...
}
func stop() {
...
self.recorderCompletionHandler()
}
func recorderCompletionHandler() {
...
let player = self.createPlayer(...)
player.play()
DispatchQueue.main.async {
self.players.append(player)
}
}
func createPlayer() {
...
}
}
最后,通过一个小型视频演示来展示比文字更有价值的问题:
对于此特定示例,最后一项的持续时间是前两项持续时间的两倍,而前两项持续时间相同。尽管不管此示例状态如何,都会发生此问题。
要提及的是,开始时间或触发时间对于所有用户都是相同的,.play
是一种称为同步的方法!
按照@Ralf Ebert
提供的良好做法进行的另一项测试,根据我的要求稍有变化,切换到play
状态,不幸的是导致了同样的问题,因此到目前为止,这似乎与在SwiftUI中有一些值得学习的原则。
由@Ralf Ebert
提供的版本的修改版本:
// SwiftUIPlayground
import SwiftUI
struct PlayerLoopView: View {
@ObservedObject var player: MyPlayer
var body: some View {
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 10.0))
.foregroundColor(Color.purple)
.opacity(0.3)
.overlay(
Circle()
.trim(
from: 0,
to: player.isPlaying ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 10.0, lineCap: .round, lineJoin: .round)
)
.animation(
player.isPlaying ?
Animation
.linear(duration: player.duration)
.repeatForever(autoreverses: false) :
.none
)
.rotationEffect(Angle(degrees: -90))
.foregroundColor(Color.purple)
)
}
.frame(width: 100, height: 100)
.padding()
}
}
struct PlayersProgressView: View {
@ObservedObject var engine = Engine()
var body: some View {
NavigationView {
VStack {
ForEach(self.engine.players) { player in
HStack {
Text("Player")
PlayerLoopView(player: player)
}
}
}
.navigationBarItems(trailing:
VStack {
Button("Add Player") {
self.engine.addPlayer()
}
Button("Play All") {
self.engine.playAll()
}
Button("Stop All") {
self.engine.stopAll()
}
}.padding()
)
}
}
}
class MyPlayer: ObservableObject, Identifiable {
var id = UUID()
@Published var isPlaying: Bool = false
var duration: Double = 1
func play() {
self.isPlaying = true
}
func stop() {
self.isPlaying = false
}
}
class Engine: ObservableObject {
@Published var players = [MyPlayer]()
func addPlayer() {
let player = MyPlayer()
players.append(player)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
player.isPlaying = true
}
}
func stopAll() {
self.players.forEach { $0.stop() }
}
func playAll() {
self.players.forEach { $0.play() }
}
}
struct PlayersProgressView_Previews: PreviewProvider {
static var previews: some View {
PlayersProgressView()
}
}
按照以下步骤创建了以下演示(演示仅在停止所有操作后显示,以使其在Stack Overflow中上传的最大图像保持在2mb以下):
- Add player
- Add player
- Add player
- Stop All (*the animations played well this far)
- Play All (*same issue as previously documented)
- Add player (*the tail player animation works fine)
找到了一篇报道类似问题的文章: https://horberg.nu/2019/10/15/a-story-about-unstoppable-animations-in-swiftui/
我将不得不找到一种不同的方法,而不是使用.repeatForever
答案 0 :(得分:2)
您需要确保没有视图更新(例如通过添加新播放器等更改触发)会重新评估“循环”,因为这可能会重置动画。
在此示例中,我将:
Identifiable
,以便SwiftUI可以跟踪对象(var id = UUID()
就足够了),然后可以使用ForEach(self.engine.players)
和SwiftUI来跟踪Player -> View
关联。ObservableObject
,并创建一个PlayerLoopView
而不是Loop
函数:struct PlayerLoopView: View {
@ObservedObject var player: Player
var body: some View {
ZStack {
Circle()
// ...
}
}
这是防止状态更新干扰动画的最可靠方法。
有关可运行示例,请参见此处:https://github.com/ralfebert/SwiftUIPlayground/blob/master/SwiftUIPlayground/Views/PlayersProgressView.swift
答案 1 :(得分:0)
这个问题似乎是由最初的实现产生的,其中.animation
方法带有条件,这就是导致跳转的原因。
如果我们决定不这样做,而是保留所需的Animation声明,并且仅切换动画时长就可以了!
如下:
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 10.0))
.foregroundColor(Color.purple)
.opacity(0.3)
Circle()
.trim(
from: 0,
to: player.isPlaying ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 10.0, lineCap: .round, lineJoin: .round)
)
.animation(
Animation
.linear(duration: player.isPlaying ? player.duration : 0.0)
.repeatForever(autoreverses: false)
)
.rotationEffect(Angle(degrees: -90))
.foregroundColor(Color.purple)
}
.frame(width: 100, height: 100)
.padding()
Obs:第三个元素的持续时间长了4倍,仅用于测试
所需结果: