什么';在初始图形之后,是否会导致SwiftUI嵌套视图项跳跃动画?

什么';在初始图形之后,是否会导致SwiftUI嵌套视图项跳跃动画?,swiftui,Swiftui,我最近在一个容器视图中遇到了一个问题,该容器有一个嵌套的视图项列表,这些项使用repeatForever动画,第一次绘制时效果良好,动态添加同级项后会出现跳跃 视图的列表是从ObservableObject属性动态生成的,它在这里表示为循环。它是在后台线程(AVAudioPlayerNodeImpl.CompletionHandlerQueue)中进行计算后生成的 循环视图动画具有一个持续时间,该持续时间等于其传递参数播放器的动态持续时间属性值。每个循环都有自己的值,每个同级循环中的值可能相同,

我最近在一个容器
视图
中遇到了一个问题,该容器有一个嵌套的
视图
项列表,这些项使用
repeatForever
动画,第一次绘制时效果良好,动态添加同级项后会出现跳跃

视图的列表
是从
ObservableObject
属性动态生成的,它在这里表示为
循环
。它是在后台线程(AVAudioPlayerNodeImpl.CompletionHandlerQueue)中进行计算后生成的

循环
视图
动画具有一个
持续时间
,该持续时间等于其传递参数
播放器
的动态
持续时间
属性值。每个
循环都有自己的值,每个同级循环中的值可能相同,也可能不同

当创建第一个
循环
视图
时,动画工作正常,但在列表中包含新项目后,动画会变得跳跃。这意味着,动画对尾部项目(列表中的最后一个项目或最新成员)和前一个项目的效果是正确的

从我的角度来看,它似乎与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)
                }
            }
        }
    }
}
最后,一个小的视频演示,展示了价值超过文字的问题:

对于这个特定示例,最后一个项目的持续时间是前两个项目的两倍,前两个项目的持续时间相同。尽管这个问题的发生与这个典型的状态无关

我想提到的是,所有的开始时间或触发时间都是相同的,
.play
是一种同步调用的方法

编辑 遵循
@Ralf Ebert
提供的良好实践之后的另一个测试,根据我的要求,稍微做了一些更改,切换
播放
状态,不幸的是,这会导致相同的问题,到目前为止,这似乎与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()
    }
}
以下演示是通过以下步骤创建的(演示仅在停止所有操作后显示,以使其在堆栈溢出中的最大图像上载量低于2mb):

发现一篇报道类似问题的文章:


我必须找到一种不同的方法,而不是使用
。repeatForever

您需要确保没有视图更新(通过添加新播放器等更改触发)导致再次重新计算“循环”,因为这可能会重置动画

在本例中,我将:

  • 使播放器
    可识别
    以便SwiftUI可以跟踪对象(
    var id=UUID()
    足够),然后您可以使用
    ForEach(self.engine.players)
    并且SwiftUI可以跟踪
    player->View
    关联
  • 使播放器本身成为一个
    可观察对象
    ,并创建一个
    PlayerLoopView
    ,而不是示例中的
    循环
    函数:
struct PlayerLoopView:视图{
@观察对象变量播放器:播放器
var body:一些观点{
ZStack{
圈()
// ...
}
}
这是防止状态更新干扰动画的最可靠方法


请参见此处以获取一个可运行的示例:

此问题似乎是在原始实现中产生的,在原始实现中,
.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倍,仅用于测试

结果如下:


看起来这里需要的是基于值的动画制作进度,而不仅仅是无结尾的动画。我在中的解决方案可能会有所帮助。在提供的代码中,动画只需在每个
主体上重置,而不必存储当前状态-这就是效果。@Asperi,太好了,谢谢!当前
播放器的实现不可更改Atly目前没有
进度
事件处理程序,但将来会研究它,或者任何其他可以增量计算进度的用例;)感谢您的时间;)非常感谢您花时间和精力研究这个问题!再次感谢您在提供的示例中分享的良好实践在状态发生变化的情况下进行测试,不幸的是,它具有相同的缺陷;相应地更新了帖子;我将继续研究以找出答案,并希望很快与大家分享我的发现!谢谢。我可以用更新的代码重现此问题。它有助于将持续时间设置为10秒左右,以使其更可见。而状态,身体在闪烁过程中没有被重新评估。动画代码对我来说很好。我猜这是一个SwiftUI错误。可能值得在Apple反馈助手中报告它,以及示例代码和复制步骤。是的,当然,很遗憾,我原来的帖子太长了;但我已经传达了信息sue向SwiftUI团队致意。感谢您的支持!找到解决方案,请查看!
- 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)
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()