Presenting and Managing Expandable Sections in SwiftUI

Posted in SwiftUI

February 14th, 2025

⏱ Reading Time: 7 mins

Expanding or collapsing views to show or hide content is often necessary for a better user experience. The Section container in SwiftUI is a well-suited view for this purpose, which, as of iOS 17, provides initializers that enable expanding and collapsing, making it easier to present and hide content on demand. While it might not be obvious at first glance, there are two ways to achieve that behavior; programmatically and through user interaction.

In this post, we'll get to know what it takes to put both in motion. In addition to that, we'll meet an approach on how to manage multiple sections efficiently, so you can expand and collapse while avoiding repetitive code and unnecessary work.

Presenting an expandable section

The fastest way to present an expandable Section is by using the following initializer:

The Section(_:isExpanded:content:) initializer gets as arguments:

  • The title of the header that will appear above the section.
  • The binding value of a state property that indicates the expanded state of the section.
  • A closure with the content of the section.

In this example, isExpanded is the state property that you see right before the view's body:

You should also notice that, although I'm embedding the Section in a List here, it could be perfectly fit into a Form too.


It's obvious that the expanded state of the section can be controlled only programmatically, as there's no direct way to interact with it. If you would run the above code you would see a normal section; no way to collapse it.

However, and for the sake of the tutorial, let's add a toolbar button at the bottom bar that toggles the isExpanded state, so we can actually view the section expanding and collapsing:

Running the above now and tapping or clicking on the eye button will make the Section hide and show its content on demand:

With almost no effort we managed to display an expandable Section. But managing the expanded either programmatically or by providing a button in a different location is not always a suitable solution. Users should be able to interact with the Section on demand, so let's get to that.

Initializing an expandable section with customizable header

The Section initializer of the previous part does not allow any customization. To customize the Section title, and most importantly, to make it interactive, then you'd better use the Section(isExpanded:content:header:) initializer that's shown next:

Note here that the header is a custom view, and instead of a plain Text view I used a button that toggles the expanded state of the section.

Even though it's now possible to expand and collapse on demand, it's not obvious at all -nor implied by any means- that the title is a button. In fact, users would expect to see a familiar indicator that would identify an expandable area; that is the arrow button (chevron icon) to the trailing edge of the header, similar to the DisclosureGroup view.

To achieve that, we'll change the header a bit, and instead of having a single button, we'll add an HStack with the title on one edge and a button with the chevron icon on the other. Furthermore, the icon will rotate by 90 degrees when the section is expanded so it points towards bottom:

Note: The frame is set to avoid an unwanted movement of the content during the animation.

This time isExpanded is updated using an animation, while the chevron icon indicates that the content can be expanded or collapsed. That's definitely something that users are acquainted with.

Creating reusable views

In cases of multiple sections, repeating the button's code is counter-productive and against the DRY (Don't Repeat Yourself) principle. Additionally, if it ever becomes necessary to make the slightest modification, that should happen to all button occurrences. It would be much better to have the button implemented only once, and manage it as a reusable view whenever necessary.

For that purpose, we can define the following view that contains the button, with the expanded state property existing as a binding property:

We can update the Section's header now as shown in the next code:

Whenever the button is tapped, the isExpanded binding will update the respective property in the main view, and the Section's content will either expand or collapse.


We could go one step further, and move the entire HStack into another reusable view, just like we did for the button. Doing so is meaningful when a title and the chevron button are the only elements that form the content of the headers in our sections, and once again we avoid repetitive code that way.

So, let's implement one more view:

In the SectionHeader brand new view, the displayed section title and the expanded state will be given as arguments upon initialization. The value of the isExpanded binding property especially, is provided as argument to the ExpandButton(isExpanded:) instance.

For one more time we can update the header of the Section like so:

See that the header content is a single line now, while users can interact with it (the chevron button in particular) and expand or collapse the Section's content. Of course, if you are not happy with the default appearance of the title, you can always change it as you like in the SectionHeader view. For instance, you can change the font style, or update the view so it accepts the name of an SF Symbol to show as icon along with the text.

Presenting multiple sections

If you have a small number of sections to present in your lists or forms, then repeating the above a few times and having a matching number of state properties to control the expanded states is not a big deal. However, if you'd rather avoid that repetition, or you have a series of sections to show, then there are better paths to take. Right next I'm demonstrating a technique, though you could definitely come up with a different approach. Note that what follows is adapted to the example used in this tutorial; displaying sections with Text views as content. In your own implementation you should adapt accordingly.

That said, let's suppose that we want to display all habitable continents of the world as sections, with some of their countries as the content of each section. The purpose is to add one Section view only in the list (as we met it previously), embedded in a ForEach container. The latter will show multiple sections eventually.

In order to make that happen, we need to have a collection of data that will feed ForEach, and each Section subsequently during the iteration. Before we get to that however, let's define the distinct pieces of data we need to specify for each section. They are the following three:

  1. The section title.
  2. The collection of countries.
  3. The expanded state.

We can represent all these in a struct that we'll name ExpandableSection. Note that it conforms to Identifiable protocol and it contains the additional required id property (for simplicity, it's a UUID value). That's necessary for proper iteration in the ForEach container we'll implement in a while:

Next, we can add the necessary collection of data; an array of ExpandableSection instances that will contain the continent names as section titles, countries as the section content, and the initial expanded state for each section. In the example here, this can be a state property in the view:

We can now go inside the List and define a ForEach container, using the sections collection as the data source -specifically, its binding value ($sections), since each Section should be able to update the isExpanded state of its corresponding ExpandableSection element.

In the closure of the ForEach we'll define a Section as we did in the previous part. The section parameter value is an ExpandableSection instance, so we'll get the values to display through its properties:

As for the Section's content, we'll add an inner ForEach that will go through the countries array and present them in a Text view:

That's all the implementation we need in order to show a series of expandable sections, capable of being expanded and collapsed with user interaction:

The entire view implementation is the following:

Conclusion

Creating an expandable Section in SwiftUI is not difficult as of iOS 17, and it can often be a useful way to present content in our apps. In this post, you've learned how to manage the expanded state programmatically and how to let users expand and collapse sections on demand. The reusable views we defined will help you avoid repeating code. But in addition, you've also met a way to present multiple sections using a custom type, a collection of data, and a ForEach container. Hopefully, all of this has been useful to you. As always, thank you for reading!

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.