是什么导致SwiftUI在初始绘图后嵌套的View项目出现跳动动画?

时间:2020-05-16 00:57:59

标签: swiftui

我最近在容器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() {
    ...
  }
}

最后,通过一个小型视频演示来展示比文字更有价值的问题:

enter image description here

对于此特定示例,最后一项的持续时间是前两项持续时间的两倍,而前两项持续时间相同。尽管不管此示例状态如何,都会发生此问题。

要提及的是,开始时间或触发时间对于所有用户都是相同的,.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)

enter image description here

找到了一篇报道类似问题的文章: https://horberg.nu/2019/10/15/a-story-about-unstoppable-animations-in-swiftui/

我将不得不找到一种不同的方法,而不是使用.repeatForever

2 个答案:

答案 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倍,仅用于测试

所需结果:

enter image description here