How to make a list with collapsible sections in SwiftUI
Collapsable Sections on Variable List in iOS

How to make a list with collapsible sections in SwiftUI

Lists are a great way to show lots of data. Sections are useful for grouping them. How can you build variable, programmatic, and collapsible sections?

One way to group lists is to have a sidebar where you can hide and show different sections. This helps you and your users only see the lists that are useful for your app.

Simple way

iOS14 introduced an optional binding parameter for the section state variable. This, together with the .liststyle sidebar, automatically hides and shows the section.

@State private var isExpandedAllSeas: Bool = true
....

VStack {
             List {
                    Section(
                          isExpanded: $isExpandedAllSeas,
                          content: {
                              ForEach(allSea) { sea in
                                 HStack {
                                    Image(systemName: "water.waves")
                                    Text(sea.name)
                                 }
                            }
                            .onDelete(perform: allSeaDelete)
                            .onMove(perform: allSeaMove)
                         },
                         header: {
                            Text("All seas")
                         }
                    )
                 
             }
             .listStyle(.sidebar)        

The initial setting of the state variable allows one to start with collapsed or expanded sections.

The tougher way

What if you have a two-dimensional data array and need different numbers of sections?

Using a state variable as a bool doesn't solve the problem. It makes all sections either closed or open.

We need a Binding<Bool> because this is the type which isExpanded expects. So we must take care of the value with getter and setter properties.

Let us start with the two dimensional data for example some oceans.

@State private var oceanRegions: [OceanRegion] = [
        OceanRegion(name: "Pacific",
                    seas: [Sea(name: "Australasian Mediterranean"),
                           Sea(name: "Philippine"),
                           Sea(name: "Coral"),
                           Sea(name: "South China")]),
        OceanRegion(name: "Atlantic",
                    seas: [Sea(name: "American Mediterranean"),
                           Sea(name: "Sargasso"),
                           Sea(name: "Caribbean")]),
        OceanRegion(name: "Indian",
                    seas: [Sea(name: "Bay of Bengal")])
	]        

Then we need a slightly different state variable.

@State private var isExpanded: Set<String> = []        

I'm using a set of strings with all the names of ocean regions of expanded sections.

Now we can build our List with section from OceanRegions with some Seas in every section.

VStack {
             List {
                 ForEach(oceanRegions) { region in
                     Section(
                        isExpanded: Binding<Bool> (
                            get: {
                                return isExpanded.contains(region.name)
                            },
                            set: { isExpanding in
                                if isExpanding {
                                    isExpanded.insert(region.name)
                                } else {
                                    isExpanded.remove(region.name)
                                }
                            }
                        ),
                        content: {
                            ForEach(region.seas) { sea in
                                Text(sea.name)
                            }.onDelete(perform: { indexSet in
                                delete(indexSet: indexSet, region: region.id)
                            })
                        },
                        header: {
                            Text(region.name)
                        }
                    )
                }
             	.listStyle(.sidebar)        

We can't use our State variable isExpanded directly because we need to insert and remove region names when the collapse state of the sections changes.

To do this I'm using a Generic Binding of type Bool because that's what isExpanded is expecting.

isExpanded: Binding<Bool> (...)        

Now I need my getter and setter logic. This is quite simple I'm using the region.name of the section because it is unique. Of cource one can use an uuid in a more complex scenario.

The getter returns a boolean and because this boolean came from a State variable it's a Binding<Bool>.

The setter on the other hand add a region.name as String to the Set when a Section is expanded and remove the region.name when the Section is collapsed.

isExpanded: Binding<Bool> (
                            get: {
                                return isExpanded.contains(region.name)
                            },
                            set: { isExpanding in
                                if isExpanding {
                                    isExpanded.insert(region.name)
                                } else {
                                    isExpanded.remove(region.name)
                                }
                            }        

With this setting there is one open point that with this setting all Sections are initially collapsed because the initial state of isExpanded is a empty Set:

@State private var isExpanded: Set<String> = []        

But all necessary data is in place and we only have to connect it with an own init function.

init() {
        _isExpanded = State(initialValue: Set(oceanRegions.map { $0.name }))
    }        

To fill the initial state of isExpanded, we just need to map data from the oceanRegions array.

And with this last all of the section from oceanRegions array are displayed expanded.


Article released first on medium.com

https://switch2mac.medium.com/swiftui-list-with-collapsible-sections-beb58760ef2c

要查看或添加评论,请登录

David Krcek的更多文章

  • Docker Ansible Environment

    Docker Ansible Environment

    We've all been there: your own development environment doesn't match the live system, either because the target system…

社区洞察

其他会员也浏览了