SwiftUI - ForEach 删除转换始终仅应用于最后一项
SwiftUI - ForEach deletion transition always applied to last item only
我正在尝试向我的 ForEach
添加一个删除动画,以便其中的每个 Card
在删除时都向外扩展。这是我目前所拥有的:
问题是无论按下哪个Card
,总是最后一个激活。有时,每张卡片中的文字都有一个奇怪的 sliding/morphing 动画。这是我的代码:
/// Ran into this problem: "SwiftUI ForEach index out of range error when removing row"
/// `ObservableObject` solution from
class Card: ObservableObject, Identifiable {
let id = UUID()
@Published var name: String
init(name: String) {
self.name = name
}
}
struct ContentView: View {
@State var cards = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cards.indices, id: \.self) { index in
CardView(card: cards[index], removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.transition(.scale)
}
}
}
}
}
struct CardView: View {
@ObservedObject var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
如何横向扩展单击的 Card
而不是最后一个?
您看到此行为的原因是因为您将索引用作 ForEach
的 id
。因此,当从 cards
数组中删除一个元素时,ForEach
看到的唯一区别是最后一个索引消失了。
您需要确保 id
唯一标识 ForEach
的每个元素。
如果您必须使用索引并标识每个元素,您可以使用 enumerated
方法或 zip
数组及其索引一起使用。我喜欢后者:
ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in
//...
}
上面使用对象本身作为ID
,需要符合Hashable
。如果你不想这样,你可以直接使用id
属性:
ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}
为完整起见,这里是 enumerated
版本(从技术上讲,它不是索引,而是偏移量,但对于基于 0 的数组,它是相同的):
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
//...
}
很棒,但我还有其他需要的东西。在我的完整代码中,每个 Card
内都有一个按钮,可将 ScrollView
滚动到末尾。
/// the ForEach
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(
card: cards[index],
scrollToEndPressed: {
proxy.scrollTo(cards.count - 1, anchor: .center) /// trying to scroll to end... not working though.
},
removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
}
)
.transition(.scale)
}
/// CardView
struct CardView: View {
@ObservedObject var card: Card
var scrollToEndPressed: (() -> Void)?
var removePressed: (() -> Void)?
var body: some View {
VStack {
Button(action: {
scrollToEndPressed?() /// scroll to the end
}) {
VStack {
Text("Scroll to end")
}
}
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
}
使用上面的代码,“滚动到结束”按钮不起作用。
我通过为每个 CardView
分配一个明确的 ID
来解决这个问题。
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(card: cards[index], scrollToEndPressed: {
withAnimation(.easeOut) { /// also animate it
proxy.scrollTo(cards.last?.id ?? card.id, anchor: .center) /// scroll to the last card's ID
}
}, removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.id(card.id) /// add ID
.transition(.scale)
}
结果:
我建议您重新考虑并使用 Card 作为 struct 而不是 class 并确认 Identifiable and Equatable.
struct Card: Hashable, Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}
然后创建一个包含卡片的视图模型。
class CardViewModel: ObservableObject {
@Published
var cards: [Card] = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
}
迭代 cardViewModel.cards
并将 card
传递给 CardView
。使用 Array 的 removeAll
方法代替 remove
。这是安全的,因为 Card
是唯一的。
ForEach(viewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { [=12=] == card}
}
}
}
一个完整的工作示例。
struct ContentView: View {
@ObservedObject var cardViewModel = CardViewModel()
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cardViewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { [=13=] == card}
}
}
.transition(.scale)
}
}
}
}
}
struct CardView: View {
var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?()
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
如果出于某种原因您需要 ContentView
中的卡片索引,请执行此操作。
都和苹果的这个教程差不多
https://developer.apple.com/tutorials/swiftui/handling-user-input
我正在尝试向我的 ForEach
添加一个删除动画,以便其中的每个 Card
在删除时都向外扩展。这是我目前所拥有的:
问题是无论按下哪个Card
,总是最后一个激活。有时,每张卡片中的文字都有一个奇怪的 sliding/morphing 动画。这是我的代码:
/// Ran into this problem: "SwiftUI ForEach index out of range error when removing row"
/// `ObservableObject` solution from
class Card: ObservableObject, Identifiable {
let id = UUID()
@Published var name: String
init(name: String) {
self.name = name
}
}
struct ContentView: View {
@State var cards = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cards.indices, id: \.self) { index in
CardView(card: cards[index], removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.transition(.scale)
}
}
}
}
}
struct CardView: View {
@ObservedObject var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
如何横向扩展单击的 Card
而不是最后一个?
您看到此行为的原因是因为您将索引用作 ForEach
的 id
。因此,当从 cards
数组中删除一个元素时,ForEach
看到的唯一区别是最后一个索引消失了。
您需要确保 id
唯一标识 ForEach
的每个元素。
如果您必须使用索引并标识每个元素,您可以使用 enumerated
方法或 zip
数组及其索引一起使用。我喜欢后者:
ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in
//...
}
上面使用对象本身作为ID
,需要符合Hashable
。如果你不想这样,你可以直接使用id
属性:
ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}
为完整起见,这里是 enumerated
版本(从技术上讲,它不是索引,而是偏移量,但对于基于 0 的数组,它是相同的):
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
//...
}
Card
内都有一个按钮,可将 ScrollView
滚动到末尾。
/// the ForEach
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(
card: cards[index],
scrollToEndPressed: {
proxy.scrollTo(cards.count - 1, anchor: .center) /// trying to scroll to end... not working though.
},
removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
}
)
.transition(.scale)
}
/// CardView
struct CardView: View {
@ObservedObject var card: Card
var scrollToEndPressed: (() -> Void)?
var removePressed: (() -> Void)?
var body: some View {
VStack {
Button(action: {
scrollToEndPressed?() /// scroll to the end
}) {
VStack {
Text("Scroll to end")
}
}
Button(action: {
removePressed?() /// call the remove closure
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
}
使用上面的代码,“滚动到结束”按钮不起作用。
我通过为每个 CardView
分配一个明确的 ID
来解决这个问题。
ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in
CardView(card: cards[index], scrollToEndPressed: {
withAnimation(.easeOut) { /// also animate it
proxy.scrollTo(cards.last?.id ?? card.id, anchor: .center) /// scroll to the last card's ID
}
}, removePressed: {
withAnimation(.easeOut) {
_ = cards.remove(at: index) /// remove the card
}
})
.id(card.id) /// add ID
.transition(.scale)
}
结果:
我建议您重新考虑并使用 Card 作为 struct 而不是 class 并确认 Identifiable and Equatable.
struct Card: Hashable, Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}
然后创建一个包含卡片的视图模型。
class CardViewModel: ObservableObject {
@Published
var cards: [Card] = [
Card(name: "Apple"),
Card(name: "Banana "),
Card(name: "Coupon"),
Card(name: "Dog"),
Card(name: "Eat")
]
}
迭代 cardViewModel.cards
并将 card
传递给 CardView
。使用 Array 的 removeAll
方法代替 remove
。这是安全的,因为 Card
是唯一的。
ForEach(viewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { [=12=] == card}
}
}
}
一个完整的工作示例。
struct ContentView: View {
@ObservedObject var cardViewModel = CardViewModel()
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(cardViewModel.cards) { card in
CardView(card: card) {
withAnimation(.easeOut) {
cardViewModel.cards.removeAll { [=13=] == card}
}
}
.transition(.scale)
}
}
}
}
}
struct CardView: View {
var card: Card
var removePressed: (() -> Void)?
var body: some View {
Button(action: {
removePressed?()
}) {
VStack {
Text("Remove")
Text(card.name)
}
}
.foregroundColor(Color.white)
.font(.system(size: 24, weight: .medium))
.padding(40)
.background(Color.red)
}
}
如果出于某种原因您需要 ContentView
中的卡片索引,请执行此操作。
都和苹果的这个教程差不多
https://developer.apple.com/tutorials/swiftui/handling-user-input