Create a fully-collapsible Section for iOS using SwiftUI
Recently, I wanted to add in collapsible sections into one of my personal projects build in SwiftUI, something like this
Knowing that SwiftUI has a Section
component, I immediately looked into this, to find out how I’d make a Section
collapsible.
I found collapsible in the official docs and got excited until I saw:
macOS 10.15+ 🤦🏻♂️
Then I decided to think about how I could make this myself for iOS, as lean as possible.
The first version that I wrote required arrays of typed data to be fed-in to populate list rows of text and images which made the whole thing far too rigid, so I went back to the drawing board. I wanted it to be flexible enough to accept any content. But I didn’t know how this was achieved, but I know it could be done, as I’ve done this so many times:
VStack {
Text("I am child content")
Text("So am I")
}
Knowing the above very familiar pattern led me to the SwiftUI source code for VStack
:
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
What’s this @ViewBuilder
thingy here? That’s the key that unlocks the above way of providing content to the VStack
s trailing closure in its init.
The collapsible Section code
struct CollapsibleSection<Content: View>: View {
private let title: String
private let content: Content
private let alignment: HorizontalAlignment
private let spacing: CGFloat
@State private var isExpanded = false
init(title: String, alignment: HorizontalAlignment = .leading, spacing: CGFloat = 16, @ViewBuilder content: () -> Content) {
self.title = title
self.alignment = alignment
self.spacing = spacing
self.content = content()
}
var body: some View {
Section {
if isExpanded {
VStack(alignment: alignment, spacing: spacing) {
content
}
}
} header: {
HStack {
Text(title)
Spacer()
Image(systemName: "chevron.up")
.rotationEffect(.degrees(isExpanded ? 180 : 0))
}
.onTapGesture {
withAnimation { isExpanded.toggle() }
}
}
}
}
Usage
Once you’ve copied the CollapsibleSection
into your codebase, use it like this:
Form { // Form will give us a lot of free, consistent styling!
CollapsibleSection(title: "Vehicles") { // @ViewBuilder at work here
Text("Millennium Falcon")
Text("AT-AT")
Text("Landspeeder")
Text("TIE Fighter")
Text("Slave I")
Text("AT-ST")
Text("X-Wing")
Text("Dreadnought")
}
}
And here is what the above code yields:
You’ve got a very lean collapsible section to use perhaps on a Form
. I chose to keep this demo as lean as possible, else I’d have got carried away animating the content fading in or even better, Star Wars movies transitions 😁
In summary
- Create a struct that will represent your section. It should have a generic type for the content conforming to
View (<Content: View>)
, as well as conforming toView
itself - Have a trailing closure in the
init
, tagged as@ViewBuilder
and of type() -> Content
.