19

I have a simple app in SwiftUI that shows a List, and each item is a VStack with two Text elements:

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)
}

The .animate() is in there because I want to animate changes to the list when the elements array changes. Unfortunately, SwiftUI also animates any changes to content, leading to weird behaviour. For example, the second Text in each item updates quite frequently, and an update will now shortly show the label truncated (with ... at the end) before updating to the new content.

So how can I prevent this weird behaviour when I update the list's content, but keep animations when the elements in the list change?

In case it's relevant, I'm creating a watchOS app.

BlackWolf
  • 5,239
  • 5
  • 33
  • 60

4 Answers4

22

The following should disable animations for row internals

VStack(alignment: .leading) {
    Text(item.name) 
    Text(self.distanceString(for: item.distance))
}
.animation(nil)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 2
    I'm using an `List` within an `ForEach` and the sentence `.animation(nil)` doesn't work for rows – iGhost Sep 12 '20 at 01:18
  • `animation(_:)` is deprecated in iOS 15 in favour of `animation(_:value:)`, so `.animation(nil, value: 0)` seems to behave the same as `.animation(nil)`! – Aaron Christiansen Dec 22 '22 at 22:20
13

The answer by @Asperi fixed the issue I was having also (Upvoted his answer as always).

I had an issue where I was animating the whole screen in using the below: AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))

And all the Text() and Button() sub views where also animating in weird and not so wonderful ways. I used animation(nil) to fix the issue after seeing Asperi's answer. However the issue was that my Buttons no longer animated on selection, along with other animations I wanted.

So I added a new State variable to turn on and off the animations of the VStack. They are off by default and after the view has been animated on screen I enable them after a small delay:

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
            }
        }
    }
}

Just adding this for anyone who has a similar issue to me and needed to build on Asperi's excellent answer.

Brett
  • 1,647
  • 16
  • 34
  • 8
    The .animation(:value:) call allows you to tie an animation only to a value change, rather than to disable the view for magic seconds. – Beginner Oct 05 '20 at 14:58
  • 3
    Just discovered this one recently too @Beginner Don't tell everyone though as it detracts from my excellent answer above ;-) – Brett Nov 03 '20 at 05:37
  • 4
    Conspiracy: Upvote the answer and comment and everyone wins and learns. hehe... The .default though is good to know as a tool to stop a repeating animation. – Beginner Nov 03 '20 at 19:24
3

Thanks to @Brett for the delay solution. My code needed it in several places, so I wrapped it up in a ViewModifier.

Just add .delayedAnimation() to your view.

You can pass parameters for defaults other than one second and the default animation.

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))
  }
}
Jim Haungs
  • 184
  • 10
0

In my case any of the above resulted in strange behaviours. The solution was to animate the action that triggered the change in the elements array instead of the list. For example:


@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 { $0.name < $1.name }
    } else {
        return sortedElements.sorted { $0.name > $1.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) 
            }
        }
    }

}
aone
  • 57
  • 8