Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI animating expand and collapse of list rows

Tags:

swiftui

I'm using SwiftUI to animate an expand and collapse in a list.

How can I get the height expansion of the section to animate smoothly like it would in UIKit with a tableview?

struct Rows: View {
    let rows = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]

    var body: some View {
        Section {
            ForEach(rows.identified(by: \.self)) { name in
                Text(name)
                    .lineLimit(nil)
            }
        }
    }
}

struct Header: View {

    @State var isExpanded: Bool = false

    var body: some View {

        VStack(alignment: .leading) {
            Button(action: {
                self.isExpanded.toggle()

            }) {
                Text(self.isExpanded ? "Collapse Me" : "Expand Me")
                    .font(.footnote)
            }

            if self.isExpanded {
                Rows().animation(.fluidSpring())
            }
        }
    }
}

struct ContentView : View {

    var body: some View {
        List(0...4) { _ in
            Header()
        }
    }
}

The animation seems to only apply to the text in the rows not the actual height or separator line growing to accommodate the new rows. The row text also seems to start animating from the very top of the row rather than where it appears in the view hierarchy. I need a smooth animation.

like image 383
jeh Avatar asked Jun 28 '19 07:06

jeh


3 Answers

I implemented it like this: (It is with proper animation)

struct ExpandCollapseList : View {
    @State var sectionState: [Int: Bool] = [:]

    var body: some View {
        NavigationView {
            List {
                ForEach(1 ... 6, id: \.self) { section in
                    Section(header: Text("Section \(section)").onTapGesture {
                        self.sectionState[section] = !self.isExpanded(section)
                    }) {
                        if self.isExpanded(section) {
                            ForEach(1 ... 4, id: \.self) { row in
                                Text("Row \(row)")
                            }
                        }
                    }
                }
            }
            .navigationBarTitle(Text("Expand/Collapse List"))
            .listStyle(GroupedListStyle())
        }
    }

    func isExpanded(_ section: Int) -> Bool {
        sectionState[section] ?? false
    }
}
like image 142
Aakash Jaiswal Avatar answered Nov 16 '22 13:11

Aakash Jaiswal


Thanks to Aakash Jaiswal's answer I was able to expand upon this implementation to suit my need to expand to three tiers, i.e., Section, Subsection, and Lesson. The compiler failed to compile the whole implementation in a single View, which is why I separated it out.

import SwiftUI

struct MenuView: View {
    var body: some View {
        HStack {
            List {
                ToggleableMenuItemsView(sections: menuItems)
                .padding()
            }
        }
        .background(Color("Gray"))
        .cornerRadius(30)
        .padding(.top, 30)
        .padding(.trailing, bounds.width * 0.2)
        .padding(.bottom, 30)
        .shadow(radius: 10)
    }

    @State var menuItemState = [String: Bool]()
    private var bounds: CGRect { UIScreen.main.bounds }

    private func isExpanded(_ menuItem: MenuItem) -> Bool {
        menuItemState[menuItem.id] ?? false
    }
}

struct ToggleableMenuItemsView: View {
    let sections: [MenuItem]

    var body: some View {
        ForEach(sections) { section in
            Section(
                header: Text(section.title)
                    .font(.title)
                    .onTapGesture { self.menuItemState[section.id] = !self.isExpanded(section) },
                content: {
                    if self.isExpanded(section) {
                        ForEach(section.children) { subsection in
                            Section(
                                header: Text(subsection.title)
                                    .font(.headline)
                                    .onTapGesture { self.menuItemState[subsection.id] = !self.isExpanded(subsection) },
                                content: {
                                    if self.isExpanded(subsection) {
                                        LessonsListView(lessons: subsection.children)
                                    }
                                }
                            )
                        }
                    }
                }
            )
        }
    }

    @State var menuItemState = [String: Bool]()

    private func isExpanded(_ menuItem: MenuItem) -> Bool {
        menuItemState[menuItem.id] ?? false
    }
}

struct LessonsListView: View {
    let lessons: [MenuItem]

    var body: some View {
        ForEach(lessons) { lesson in
            Text(lesson.title)
                .font(.subheadline)
        }
    }
}

class MenuItem: Identifiable {
    var id: String
    let title: String
    var children: [MenuItem]

    init(id: String, title: String, children: [MenuItem] = []) {
        self.id = id
        self.title = title
        self.children = children
    }
}

let menuItems = [
    MenuItem(
        id: "01",
        title: "The Land in its World",
        children: [
            MenuItem(
                id: "01A",
                title: "North and South",
                children: [
                    MenuItem(
                        id: "01A01",
                        title: "Between Continents"
                    ),
                    MenuItem(
                        id: "01A02",
                        title: "The Wet North"
                    ),
                    MenuItem(
                        id: "01A03",
                        title: "The Dry South"
                    ),
                    MenuItem(
                        id: "01A04",
                        title: "Between Wet and Dry"
                    )
                ]
            ),
            MenuItem(
                id: "01B",
                title: "East and West",
                children: [
                    MenuItem(
                        id: "01B01",
                        title: "Sea and Desert"
                    ),
                    MenuItem(
                        id: "01B02",
                        title: "Exchange in Aram"
                    ),
                    MenuItem(
                        id: "01B03",
                        title: "Exchange in Egypt"
                    ),
                    MenuItem(
                        id: "01B04",
                        title: "A Bypass Between"
                    )
                ]
            ),
            MenuItem(
                id: "01C",
                title: "Between Empires",
                children: [
                    MenuItem(
                        id: "01C01",
                        title: "Imperial Dreams"
                    ),
                    MenuItem(
                        id: "01C02",
                        title: "Egypt Marches"
                    ),
                    MenuItem(
                        id: "01C03",
                        title: "Taking Egypt's Wealth"
                    ),
                    MenuItem(
                        id: "01C04",
                        title: "The Land Between"
                    )
                ]
            )
        ]
    )
]

struct MenuView_Previews: PreviewProvider {
    static var previews: some View {
        MenuView()
    }
}

Here's a demo

like image 10
Scott Gardner Avatar answered Nov 16 '22 12:11

Scott Gardner


try to implement it like this:

struct ContentView : View {

    @State var expanded:[Int:Bool] = [:]

    func isExpanded(_ id:Int) -> Bool {
        expanded[id] ?? false
    }

    var body: some View {
        NavigationView{
            List {
                ForEach(0...80) { section in
                    Section(header: CustomeHeader(name: "Section \(section)", color: Color.white).tapAction {
                        self.expanded[section] = !self.isExpanded(section)
                    }) {
                        if self.isExpanded(section) {
                            ForEach(0...30) { row in
                                Text("Row \(row)")
                            }
                        }
                    }
                }
            }
        }.navigationBarTitle(Text("Title"))
    }
}

struct CustomeHeader: View {
    let name: String
    let color: Color

    var body: some View {
        VStack {
            Spacer()
            HStack {
                Text(name)
                Spacer()
            }
            Spacer()
            Divider()
        }
        .padding(0)
            .background(color.relativeWidth(1.3))
            .frame(height: 50)
    }
}
like image 1
Zasuk Avatar answered Nov 16 '22 11:11

Zasuk