SwiftUI:在不对内容更改进行动画处理的情况下对列表中的更改进行动画处理

SwiftUI: Animate changes in List without animating content changes

我在 SwiftUI 中有一个简单的应用程序,它显示 List,每个项目都是一个 VStack,带有两个 Text 元素:

var body: some View {
    List(elements) { item in
        NavigationLink(destination: DetailView(item: item)) {
            VStack(alignment: .leading) {
                Text(item.name) 
                Text(self.distanceString(for: item.distance))
            }
        }
    }
    .animation(.default)
}

.animate() 在那里,因为我想在 elements 数组更改时对列表的更改进行动画处理。不幸的是,SwiftUI 还会对内容的任何更改进行动画处理,从而导致奇怪的行为。例如,每个项目中的第二个 Text 更新非常频繁,并且在更新到新内容之前,更新现在会很快显示标签被截断(末尾有 ...)。

那么,当我更新列表的内容时如何防止这种奇怪的行为,但当列表中的元素发生变化时保持动画?

如果相关,我正在创建一个 watchOS 应用程序。

以下应禁用行内部的动画

VStack(alignment: .leading) {
    Text(item.name) 
    Text(self.distanceString(for: item.distance))
}
.animation(nil)

@Asperi 的回答解决了我也遇到的问题(一如既往地赞成他的回答)。

我在使用以下命令为整个屏幕设置动画时遇到了问题:AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))

所有 Text() 和 Button() 子视图也以奇怪而不是那么美妙的方式进行动画处理。看到 Asperi 的回答后,我使用 animation(nil) 解决了这个问题。然而,问题是我的按钮在选择时不再动画,连同我想要的其他动画。

所以我添加了一个新的状态变量来打开和关闭 VStack 的动画。默认情况下它们是关闭的,在屏幕上显示动画后我会在一小段延迟后启用它们:

struct QuestionView : View {
    @State private var allowAnimations : Bool = false

    var body : some View {
        VStack(alignment: .leading, spacing: 6.0) {
            Text("Some Text")

            Button(action: {}, label:Text("A Button")
        }
        .animation(self.allowAnimations ? .default : nil)
        .onAppear() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                self.allowAnimations = true
            }
        }
    }
}

只需为与我有类似问题并需要在 Asperi 的出色回答基础上进一步发展的任何人添加此内容。

感谢@Brett 提供延迟解决方案。我的代码在多个地方需要它,所以我将它包装在一个 ViewModifier 中。

只需将 .delayedAnimation() 添加到您的视图即可。

您可以为除一秒和默认动画之外的默认值传递参数。

import SwiftUI

struct DelayedAnimation: ViewModifier {
  var delay: Double
  var animation: Animation

  @State private var animating = false

  func delayAnimation() {
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
      self.animating = true
    }
  }

  func body(content: Content) -> some View {
    content
      .animation(animating ? animation : nil)
      .onAppear(perform: delayAnimation)
  }
}

extension View {
  func delayedAnimation(delay: Double = 1.0, animation: Animation = .default) -> some View {
    self.modifier(DelayedAnimation(delay: delay, animation: animation))
  }
}

在我的案例中,以上任何一项都导致了奇怪的行为。解决方案是 为触发元素数组 而不是列表中的更改的动作设置动画。例如:


@State private var sortOrderAscending = true

// Your list of elements with some sorting/filtering that depends on a state
// In this case depends on sortOrderAscending
var elements: [ElementType] {
    let sortedElements = Model.elements
    if (sortOrderAscending) {
        return sortedElements.sorted { [=10=].name < .name }
    } else {
        return sortedElements.sorted { [=10=].name > .name }
    }
}

var body: some View {

    // Your button or whatever that triggers the sorting/filtering
    // Here is where we use withAnimation
    Button("Sort by name") {
        withAnimation {
            sortOrderAscending.toggle()
        }
    }

    List(elements) { item in
        NavigationLink(destination: DetailView(item: item)) {
            VStack(alignment: .leading) {
                Text(item.name) 
            }
        }
    }

}