11

I am trying to animate the expansion of cells in a swiftUI list when the user taps on the cell. However the animation is faulty.

I came across this answer (https://stackoverflow.com/a/60873883/13296730) to help with the animation however it requires already knowing the expected height of the cell after it is expanded. In my case this would depend on the data in note.text:

List{
    ForEach(notes){ note in
        VStack{
            HStack{
                Text(note.title)
                    .fontWeight(.bold)
                    .foregroundColor(.gray)
                Spacer()
                Image(systemName: "chevron.right.circle")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .foregroundColor(.gray)
                    .frame(width: 20, height: 20)
            }
            .padding(.vertical)
            .contentShape(Rectangle())
            .onTapGesture{
                selectedNote = note
            }
            if isSelected(note) {
                Text(note.text)
            }
        }
        .padding([.bottom, .leading, .trailing], 20)
        .background(
            RoundedRectangle(cornerRadius: 25, style: .continuous)
                .foregroundColor(isSelected(noteSection) ? .red : .clear)
        )
        .animation(.default)
    }
    .onMove(perform: move)
}

As you can see from the code above, I need to use a list so that I can make use of the .onMove functionality.

Is there a way to get the same smooth list animations in the linked answer but for dynamic expanded sizes?

Thanks.

2
  • I am struggling with the exact same challenge... disheartened to see a year old question unanswered. Commented Feb 28, 2022 at 21:26
  • @spentag maybe this approach might work for you: stackoverflow.com/a/60890312/1016508 Commented Mar 28, 2022 at 8:55

3 Answers 3

4

I struggled with this for a while, and visited a bunch of the existing solutions on Stack Overflow, but they all seemed too complicated, especially now in 2023.

What I eventually stumbled on was instead of putting the content inside of a List you can just put it inside of a ScrollView. Then you will get nice, clean animations without any weird hacks with custom height modifiers or having to know the height of your container before hand. You'll just need to add a bit of extra styling to get it to look as nice as a List does by default.

struct Cell: View {
    @ObservedObject var item: Item
    @State var showExtra = false

    var body: some View {
        VStack {
            Text("tap me")
                .onTapGesture {
                    withAnimation{
                        showExtra.toggle()
                    }
                }
            if showExtra {
                Text("I'm extra text")
            }
        }
    }
}

struct ContenetView: View {
    var items = [ ... ] // your items
    var body: some View {
        ScrollView {
            ForEach(items) { item in
                Cell(item: item)
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Agree, I looked a bunch as well. It's crazy that in 2023 List rows still can't be easily expanded like this. SwiftUI is suppose to "work" with the expected case of the code and it falls down badly in this area. Thanks for the pointer for using a custom ScrollView.
ScrollView doesn't reuse subviews. If it has a lot of cells, there could be performance issues. Reusing cells is the one of the main reason to use ListView, and also the cause of unexpected layout behaviors of ListView.
3

This answer is based off Oliver Rider's answer. I believe it works because SwiftUI Lists respond to the changed ID as if the list row has been replaced with a new one, and by default SwiftUI animates the creation or insertion of new rows. It seems to work as well with a dynamic list as with a fixed one.

You only need to alter the ID of the row item, and you can keep the ID of your object the same. In this example I just concatenate the object ID with the stringified bool value for isExpanded to make the changing List Row id.

import SwiftUI

struct SOAnswerEditedView: View {
    @State private var elements = [Element(id: UUID()),
                                   Element(id: UUID()),
                                   Element(id: UUID())]
    
    var body: some View {
        List($elements) { element in
            ListRowEdited(element: element)
        }
        .listStyle(.plain)
    }
}

struct ListRowEdited: View {
    @Binding var element: Element
    @State var isExpanded: Bool = false
    
    var body: some View {
        VStack (alignment: .leading) {
            Text("\(element.id.uuidString)")
                .font(.caption2)
            Circle().fill(.red)
            
            Button {
                withAnimation {
                    isExpanded.toggle()
                }
            } label: {
                Image(systemName: isExpanded ? "arrow.up" : "arrow.down")
            }
            .buttonStyle(.bordered)
        }
        .frame(maxHeight: isExpanded ? 200 : 100)
        .id(element.id.uuidString + String(isExpanded))
    }
}

struct Element: Identifiable {
    var id: UUID
}

4 Comments

Thank you so much. This works perfectly and is by far the only one that works on the Internet. Thanks to Oliver too!
If you're satisfied with this answer, would you please make it the accepted answer?
I'm not the OP so I can't mark it as accepted, but again thank you shodak
This should be higher up... The current top answer overlooks the fact that list comes with a bunch of nice features like swipe actions and animations. This works beautifully and imo just changing the id is relatively clean and logical.
2

im still new to SwiftUi but managed to get something working by using the below code. I can't get multiple of the listRow's to animation if I just ForEach loop over list row but in this example but this approach does seam to work. Maybe someone could take it the next step to work out why the animations work in this method.

I feel its something todo with the list not knowing which row is which or something, hence me adding the id property. I also couldn't get any animations on anything in the view until the id property was changed. hence the onChange modifier.

love some feedback and ideas on it.

struct testViewTwo: View {

@State private var id1 = UUID()
@State private var id2 = UUID()

var body: some View {
    List {
        listRow(id: $id1)
        listRow(id: $id2)
    }
    .listStyle(.plain)
}}


struct listRow: View {
@Binding var id: UUID
@State private var isShowingRow: Bool = false
@State private var animateView: Bool = false

var body: some View {
    Group {
        VStack (alignment: .leading) {
            Text("\(id.uuidString)")
                .font(.caption2)
            Text("Animation test")
            Button {
                withAnimation {
                    //MARK: tirggers animation OF view
                    id = UUID()
                }
                isShowingRow.toggle()
            } label: {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(animateView ? .blue : .red)
                    .animation(.easeInOut(duration: 2), value: animateView)
            }
            .buttonStyle(.bordered)
            VStack {
                if isShowingRow {
                    Text("Hello, world!")
                    Text("Hello, world!")
                    Text("Hello, world!")
                }
                else {
                    EmptyView()
                }
            }
        }
        .padding()
    }
    .id(id)
    .onChange(of: id) { newValue in
        //MARK: triggers animations IN view
        animateView.toggle()
    }
}}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.