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
|
1 Answer
Reset to default 2The 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 ForEach
s 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 Dog
s.), 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 Dog
s have their own id
s, you should use that as the id:
parameter, i.e. id: \.element.id
.
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