Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI custom list with ForEach delete animation not working

Tags:

ios

swift

swiftui

I am creating a custom list displaying time information (minutes and seconds, not used in the snippet below to simplify the code). I managed to implement a nice animation when the user adds an entry to the list, but deleting an entry has no animation (1st GIF).

With iOS 14, the animation is working, however, the animation only removes the last rectangle from the list and then updates the text in each row (2nd GIF). That's not what I want - My goal is if a row has been deleted, the other rows should fill up that space and move accordingly - with an animation.

Probably something is wrong with the IDs of the rows but I just wasn't able to fix that. Thanks for helping!

enter image description here

enter image description here

struct ContentView: View {
    @State var minutes = [0]
    @State var seconds = [0]
    @State var selectedElement = 0
    
    var body: some View {
        ScrollView(){
            VStack{
                ForEach(minutes.indices, id: \.self){ elem in
                    
                    ZStack{
                        
                        EntryBackground()
                        
                        
                        Text("\(self.minutes[elem])")
                            .transition(AnyTransition.scale)
                        
                        HStack{
                            Button(action: {
                                withAnimation(.spring()){
                                    self.seconds.remove(at: elem)
                                    self.minutes.remove(at: elem)
                                }
                                
                            })
                            {
                                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: {
                withAnimation{
                    self.minutes.append(self.minutes.count)
                    self.seconds.append(0)
                }
                
            })
            {
                ZStack{
                    EntryBackground()
                    
                    Text("Add")
                    
                    HStack{
                        Image(systemName: "plus.circle.fill")
                            .foregroundColor(Color.green)
                            .font(.system(size: 22))
                            .padding(.leading, 10)
                        
                        Spacer()
                    }
                }.padding()
            }
        }
    }
}

struct EntryBackground: View {
    var body: some View {
        Rectangle()
            .cornerRadius(12)
            .frame(height: 40)
            .foregroundColor(Color.gray.opacity(0.15))
    }
}
like image 904
leonboe1 Avatar asked Aug 11 '20 18:08

leonboe1


1 Answers

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

enter image description here

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))
    }
}
like image 94
Asperi Avatar answered Nov 19 '22 00:11

Asperi