最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - Why do I get inconsistent animations in this list view? - Stack Overflow

programmeradmin3浏览0评论

I'm digging into the weeds of SwiftUI and learning about view Identity for the first time. When playing around with some code and following along with WWDC talks I'm hitting some confusing animations.

Playing around with this bit of code, the top bit fails to animate smoothly, while the uncommented-out code does animate smoothly. This makes sense based on what I've learned about how SwiftUI handles view identity and state changes.

import SwiftUI

struct SwitchView: View {
  enum SwitchLocation {
    case top, bottom

    mutating func toggle() {
      switch self {
      case .top:
        self = .bottom
      case .bottom:
        self = .top
      }
    }
  }
  @State var location: SwitchLocation = .top

  var body: some View {
    VStack {
      Text("TOP")

      // This version does not smoothly animate the view from top to bottom and back. The other
//      if self.location == .top {
//        Text("My Position")
//          .onTapGesture {
//            withAnimation {
//              self.location.toggle()
//            }
//          }
//        Spacer()
//      } else {
//        Spacer()
//        Text("My Position")
//          .onTapGesture {
//            withAnimation {
//              self.location.toggle()
//            }
//          }
//      }

      // This version DOES animate smoothly between locations, and it makes sense to me.
      Text("My Position")
        .frame(maxHeight: .infinity, alignment: self.location == .top ? .top : .bottom)
        .onTapGesture {
          withAnimation {
            self.location.toggle()
          }
        }

      Text("BOTTOM")
    }
  }
}

However, playing around with this other example, I was expecting that tapping on a dog row would animate it smoothly from wherever it is in its section, to the bottom of the other section and visa-versa. In testing, this happens about 50% of the time, while the other 50% the rows simply fade in and out simultaneously.

import SwiftUI

struct Dog {
  var id = UUID()
  var name: String
}

class BestDogRoster: ObservableObject {
  private var rescueDogNames: [String] = ["Milo", "Archer", "Theo", "Talula", "Zavvi"]
  private var adoptedDogNames: [String] = ["Henry", "Auggie"]
  @Published var rescueDogs: [Dog] = []
  @Published var adoptedDogs: [Dog] = []
  
  init() {
    self.rescueDogs = rescueDogNames.map({
      Dog(name: $0)
    })
    self.adoptedDogs = adoptedDogNames.map({
      Dog(name: $0)
    })
  }
  
  func adoptDog(at index: Int) {
    adoptedDogs.append(rescueDogs.remove(at: index))
  }
  
  func giveUpDog(at index: Int) {
    rescueDogs.append(adoptedDogs.remove(at: index))
  }
}

struct BestContentView: View {
  @ObservedObject var dogRoster = BestDogRoster()
  
  var body: some View {
    List {
      Section {
        ForEach(Array(dogRoster.rescueDogs.enumerated()), id: \.offset) { index, dog in
          Text(dog.name)
            .onTapGesture {
              withAnimation {
                dogRoster.adoptDog(at: index)
              }
            }
        }
      }
      Section("Adopted Dogs") {
        ForEach(Array(dogRoster.adoptedDogs.enumerated()), id: \.offset) { index, dog in
          Text(dog.name)
            .onTapGesture {
              withAnimation {
                dogRoster.giveUpDog(at: index)
              }
            }
        }
      }
    }
  }
}```


I have also tried generating arrays of Text(dogName) views instead of arrays of Dogs, and that gave similar results. I'm wondering if there is a better best practice for "moving" one element to the other list section other than `oneArray.append(secondArray.remove(at: index))`, or if I'm missing some other SwiftUI trick? I appreciate any feedback.

I'm digging into the weeds of SwiftUI and learning about view Identity for the first time. When playing around with some code and following along with WWDC talks I'm hitting some confusing animations.

Playing around with this bit of code, the top bit fails to animate smoothly, while the uncommented-out code does animate smoothly. This makes sense based on what I've learned about how SwiftUI handles view identity and state changes.

import SwiftUI

struct SwitchView: View {
  enum SwitchLocation {
    case top, bottom

    mutating func toggle() {
      switch self {
      case .top:
        self = .bottom
      case .bottom:
        self = .top
      }
    }
  }
  @State var location: SwitchLocation = .top

  var body: some View {
    VStack {
      Text("TOP")

      // This version does not smoothly animate the view from top to bottom and back. The other
//      if self.location == .top {
//        Text("My Position")
//          .onTapGesture {
//            withAnimation {
//              self.location.toggle()
//            }
//          }
//        Spacer()
//      } else {
//        Spacer()
//        Text("My Position")
//          .onTapGesture {
//            withAnimation {
//              self.location.toggle()
//            }
//          }
//      }

      // This version DOES animate smoothly between locations, and it makes sense to me.
      Text("My Position")
        .frame(maxHeight: .infinity, alignment: self.location == .top ? .top : .bottom)
        .onTapGesture {
          withAnimation {
            self.location.toggle()
          }
        }

      Text("BOTTOM")
    }
  }
}

However, playing around with this other example, I was expecting that tapping on a dog row would animate it smoothly from wherever it is in its section, to the bottom of the other section and visa-versa. In testing, this happens about 50% of the time, while the other 50% the rows simply fade in and out simultaneously.

import SwiftUI

struct Dog {
  var id = UUID()
  var name: String
}

class BestDogRoster: ObservableObject {
  private var rescueDogNames: [String] = ["Milo", "Archer", "Theo", "Talula", "Zavvi"]
  private var adoptedDogNames: [String] = ["Henry", "Auggie"]
  @Published var rescueDogs: [Dog] = []
  @Published var adoptedDogs: [Dog] = []
  
  init() {
    self.rescueDogs = rescueDogNames.map({
      Dog(name: $0)
    })
    self.adoptedDogs = adoptedDogNames.map({
      Dog(name: $0)
    })
  }
  
  func adoptDog(at index: Int) {
    adoptedDogs.append(rescueDogs.remove(at: index))
  }
  
  func giveUpDog(at index: Int) {
    rescueDogs.append(adoptedDogs.remove(at: index))
  }
}

struct BestContentView: View {
  @ObservedObject var dogRoster = BestDogRoster()
  
  var body: some View {
    List {
      Section {
        ForEach(Array(dogRoster.rescueDogs.enumerated()), id: \.offset) { index, dog in
          Text(dog.name)
            .onTapGesture {
              withAnimation {
                dogRoster.adoptDog(at: index)
              }
            }
        }
      }
      Section("Adopted Dogs") {
        ForEach(Array(dogRoster.adoptedDogs.enumerated()), id: \.offset) { index, dog in
          Text(dog.name)
            .onTapGesture {
              withAnimation {
                dogRoster.giveUpDog(at: index)
              }
            }
        }
      }
    }
  }
}```


I have also tried generating arrays of Text(dogName) views instead of arrays of Dogs, and that gave similar results. I'm wondering if there is a better best practice for "moving" one element to the other list section other than `oneArray.append(secondArray.remove(at: index))`, or if I'm missing some other SwiftUI trick? I appreciate any feedback.
Share Improve this question asked Jan 20 at 8:25 Charles FiedlerCharles Fiedler 111 silver badge1 bronze badge 1
  • ForEach is not a for loop, id: \.offset isn't valid, if 0 moves to 1 it is still 0. if 10 is last and is deleted it'll crash. – malhal Commented Jan 20 at 11:19
Add a comment  | 

1 Answer 1

Reset to default 2

The id: parameter of ForEach is very important! It gives an identity to each view that ForEach creates. For move animations to work as expected, views that represent the "same dog" should always have the same id - this how ForEach finds which views should be moved. Both of your ForEachs are using \.offset as the id, which can change when dogs are moved around.

Consider the case when you move "Archer" to the adopted dogs list, The Text that says "Archer" originally has id 1 (Note that I'm talking about the id of the views, given by ForEach, not the id properties of the Dogs.), because it is the second item in the rescue dogs array. After moving it to the adopted dogs sections, the Text that says "Archer" now has id 2, because it is now the third item in the adopted dogs array.

On the other hand, "Theo" now has id 1 in the first ForEach, so this is interpreted as the original "Archer" text changing its text to "Theo", and the second section getting a new row that just so happens to say "Archer".

Since your Dogs have their own ids, you should use that as the id: parameter, i.e. id: \.element.id.

发布评论

评论列表(0)

  1. 暂无评论