Insert, Update and Delete Animations with Foreach in Swiftui

Insert, update and delete animations with ForEach in SwiftUI

Luckily this is actually really easy to do. Simply remove .animation(.spring()) on your Row, and wrap any changes in withAnimation(.spring()) { ... }.

So the add button will look like this:

private var AddButton: some View {
Button(action: {
withAnimation(.spring()) {
self.items.insert(Item(name: "Jeff"), at: 0)
}
}) {
Text("Add")
}
}

and your Row will look like this:

struct Row: View {

@State var name: String

var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.transition(.move(edge: .leading))
}
}

SwiftUI custom list with ForEach delete animation not working

You need to make each row uniquely identified, so animator know what is added and what is removed, so animate each change properly.

Here is possible approach. Tested with Xcode 12 / iOS 14

Sample Image

struct TimeItem: Identifiable, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}

let id = UUID() // << identify item
let minutes: Int
let seconds: Int = 0
}

struct ContentView: View {
@State var items = [TimeItem]()
@State var selectedElement: TimeItem?

var body: some View {
ScrollView(){
VStack{
ForEach(items){ elem in // << work by item

ZStack{

EntryBackground()

Text("\(elem.minutes)")
.transition(AnyTransition.scale)

HStack{
Button(action: {
self.items.removeAll { $0.id == elem.id }
})
{
Image(systemName: "minus.circle.fill")
.foregroundColor(Color.red)
.font(.system(size: 22))
.padding(.leading, 10)
}
Spacer()
}

}
.padding(.horizontal)
.padding(.top)
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.spring()){
self.selectedElement = elem
}
}
}
}
Spacer()

Button(action: {
self.items.append(TimeItem(minutes: self.items.count))
})
{
ZStack{
EntryBackground()

Text("Add")

HStack{
Image(systemName: "plus.circle.fill")
.foregroundColor(Color.green)
.font(.system(size: 22))
.padding(.leading, 10)

Spacer()
}
}.padding()
}
}.animation(.spring(), value: items) // << animate changes
}
}

struct EntryBackground: View {
var body: some View {
Rectangle()
.cornerRadius(12)
.frame(height: 40)
.foregroundColor(Color.gray.opacity(0.15))
}
}

Add animations to ForEach loop elements (SwiftUI)

It looks like this problem is still up to day (Xcode 11.4), because by just copy-pasting the observed effect is the same. So, there are a couple of problems here: first, it needs to setup combination of animation and transition correctly; and, second, the ForEach container have to know which exactly item is removed, so items must be identified, instead of indices, which are anonymous.

As a result we have the following effect (transition/animation can be others):

demo

struct TestAnimationInStack: View {
@State var ContentArray = ["A","B","C", "D", "E", "F", "G", "I", "J"]
var body: some View {
ScrollView{
VStack{
ForEach(Array(ContentArray.enumerated()), id: \.element){ (i, item) in // << 1) !
ZStack{
// Object
Text(item)
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
withAnimation { () -> () in // << 2) !!
self.ContentArray.remove(at: i)
}
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}.transition(AnyTransition.scale) // << 3) !!!
}
}
}
}
}

How to animate the removal of a view created with a ForEach loop getting its data from an ObservableObject in SwiftUI

It is not clear which effect do you try to achieve, but on remove you should animate not view internals, but view itself, ie. in parent, because view remove there and as-a-whole.

Something like (just direction where to experiment):

var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
.transition(.move(edge: .leading)) // << here !! (maybe asymmetric needed)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}
.animation(Animation.easeInOut(duration: 1)) // << here !! (parent animates subview removing)

SwiftUI - ForEach deletion transition always applied to last item only

The reason you're seeing this behavior is because you use an index as an id for ForEach. So, when an element is removed from the cards array, the only difference that ForEach sees is that the last index is gone.

You need to make sure that the id uniquely identifies each element of ForEach.

If you must use indices and have each element identified, you can either use the enumerated method or zip the array and its indices together. I like the latter:

ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in 
//...
}

The above uses the object itself as the ID, which requires conformance to Hashable. If you don't want that, you can use the id property directly:

ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}

For completeness, here's the enumerated version (technically, it's not an index, but rather an offset, but for 0-based arrays it's the same):

ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in 
//...
}

SwiftUI using ForEach and onTapGesture to update selected element causing crash

Your problem is likely due to the view diffing algorithm having a different outcome if you have the text value present, since that would be a difference in the root view. Since you have two pieces of state that represent the sheet, and you are force unwrapping one of them, you're leaving yourself open to danger.

You should get rid of showingSheet, then use the item binding of sheet instead. Make Cards conform to Identifiable, then rewrite the sheet modifier as:

.sheet(item: $selectedCard) { SpecificCardView(card: $0) } 

This will also reset your selected card to nil when the sheet is dismissed.



Related Topics



Leave a reply



Submit