New tools available to us with iOS 15 can be used to enhance DisclosureGroups.

This year I found myself in the exact same scenario I was in last year. I was building a new SwiftUI project when the new changes introduced at WWDC were released. And once again I’ve found myself so appreciative of some of the changes that were made to SwiftUI.

For the project I’m working on I have an object that contains an array of objects.

The structures I’m populating.

I wanted a screen such as this. It’s a List that has collapsable sections.

A list with multiple collapsable sections.

SidebarListStyle

I achieved this in SwiftUI by using the listStyle of SidebarListStyle. First for this example I populated the structs.

The structures I’m populating.

Then I added a List with a style of SidebarListStyle.

List with a SidebarListStyle.

I eventually changed my mind about using this implementation. Initially I was really confused why using SidebarListStyle in two different projects (both with a target of the iPhone) could cause the Lists to look so different.

After a lot of research I finally figured out that my Lists looked different because in my “real” project I had used a navigationViewStyle of “.stack” in the View that linked to my View with a List. This was done to get around an issue where an “isActive” binding passed to a NavigationLink will stop working if you pass it to a few Views.

Another thing that wasn’t great was that if you collapse a section, hit the “back” button, and then navigate again to the View the section didn’t remain collapsed.

An example where the section does not remain collapsed.

It was this Tweet by famous iOS developer Emmanuel Crouvisier that really made me pause.

When someone who has been featured in the App Store gives you a warning it’s time to listen! I hadn’t even considered the fact that I wasn’t using this in an iOS Sidebar! Based off of a suggestion from Emmanuel I looked at using a DisclosureGroup.

DisclosureGroup

We’ll still use a List with the DisclosureGroup. The listStyle has been removed and Section has been replaced with DisclosureGroup.

List with DisclosureGroup.

When all of the DisclosureGroups are expanded the View looks similar (actually better in my opinion).

The app running with all parts expanded.

The issue is that by default DisclosureGroup is not expanded. So the View initially looks like this.

The app running with all parts collapsed.

DisclosureGroup lets us pass in a binding called “isExpanded” that controls whether or not it is expanded.

Changing the code to use “isExpanded”.

The problem with this code is that collapsing or expanding one section also does it to the others!

Collapsing or expanding one section does the same to all the others.

The outer ForEach loop is using the same “expandMe” binding for each DisclosureGroup so it makes sense that changing that variable’s value also affects the other DisclosureGroups that are using it. Clearly we need a different binding to pass to each DisclosureGroup in the loop. In this simple example we created a State variable to control whether or not to expand. But how do you control multiple DisclosureGroups in the same List? It’s not so easy to add a State variable for each of them. My project is dealing with a dynamically created List where the number of DisclosureGroups to display could always be changed by the user. How do we create a binding for each of them? This immediately made me wonder if the approach I came up with in my earlier post “Moving items in multiple sections of a SwiftUI List” would work (spoiler alert: it does).

As with my previous post the idea is to move the variable that controls expanding each DisclosureGroup into the outer Struct.

Added the expanded variable to the outer Country struct.

How do we get a binding to these multiple variables? That’s when I remembered something quite interesting from Paul Hudson’s list of SwiftUI changes for iOS 15. SwiftUI now lets us use a List based on a binding. That lets us modify each entry the List is showing. In order to prepare our project for this I moved the State variable to the root view so that it will survive navigating back from the View with the List.

The State variable moved to the root View.

Now look carefully at how the code has been updated to use these bindings. We pass a binding to the ForEach used to build the contents of the List. The closure can then modify the “expandMe” variable in each individual struct to control whether that particular DisclosureGroup is collapsed or expanded.

Code changed to use the new bindings.

Now when the View is displayed for the first time all of the parts are expanded by default. If we collapse a DisclosureGroup, go back, and then navigate to the View again the DisclosureGroups that were collapsed by the user remain collapsed.

All parts are expanded by default. Collapsing or expanding one part doesn’t affect the others.

Addendum

After I posted this blog entry I continued experimenting. Here’s an example if you’d rather have the DisclosureGroups controlled via a State variable in the same View.

Here are structs I set up for this example to support an enum. I’m basing this off of my recent adventures in parsing JSON emoji files. In this example we know that for the data we’re using every country has animals associated with it.

Structs used to set up this example.

In this example we’re setting up a State variable called controlIt that is an array that will be used to control whether a DisclosureGroup is expanded or not. I’ve added on onAppear closure that will populate an entry for each of our countries when the View appears. The ForEach used to build the List is still being passed a Binding and when the user expands or collapses a DisclosureGroup it updates the appropriate member of the array. I forgot to mention that Jordi Bruin came up with that nice use of “filter” in the inner ForEach that I’m using for matching the corresponding country.

Code example where the DisclosureGroup control is via a local State variable array.

Because we now have control over whether each DisclosureGroup is expanded or collapsed the onAppear logic could be changed. Here I’ve made it so that Canada defaults to collapsed while the other countries are expanded.

Canada is set to default to collapsed.

When the View first appears Canada is shown as collapsed while the other countries are expanded.

Screen showing Canada collapsed while the other countries are expanded.

Update January 18, 2022

There is a potential problem with this approach. I realized that if you delete the last entry in the source array it will cause a crash.

If I remove the Binding in the ForEach the crash goes away. I submitted feedback FB9850253 about this potential SwiftUI issue.

Wrap Up

What a handy addition to SwiftUI this is!

If this post was helpful to you I’d love to hear about it! Find me on Twitter.