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.
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
}
}
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
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With