FULL DISCLOSUREgroup
Cranking the usefulness of SwiftUI’s DisclosureGroup UP to eleven… with a lesson in style
One of the neatest components in SwiftUI is DisclosureGroup. The name becomes a bit more obvious after you see it in action, but essentially it’s a “drawer” that allows content to expand in and out of the View. Perfect for when you want to give users more info or controls, but only when they ask for it.
Good news is that they are simple to implement,
Even better news is that they are very customizable. For instance, did you know you could re-style a DisclosureGroup to expand upwards?
Yeah, we’re gonna get a little crazy with this one.
The Basics
A DisclosureGroup is relatively simple to implement. It takes in a Label and the Content to show/hide (both of which can be whatever View you want) as well as the option to binding bool for the expansion state:
// Annotized Interface for DisclosureGroup
public struct DisclosureGroup<Label, Content> : View where Label : View, Content : View {
public init(@ViewBuilder content: @escaping () -> Content, @ViewBuilder label: () -> Label)
public init(isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> Content, @ViewBuilder label: () -> Label)
}
Or with a simplified Label with Text:
extension DisclosureGroup where Label == Text {
public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content)
public init(_ titleKey: LocalizedStringKey, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> Content)
public init<S>(_ label: S, @ViewBuilder content: @escaping () -> Content) where S : StringProtocol
public init<S>(_ label: S, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) where S : StringProtocol
}
So all in all, we can simply declare:
DisclosureGroup("DisclosureGroup") {
Text("A disclosure group view consists of a label to identify the contents, and a control to show and hide the contents. Showing the contents puts the disclosure group into the “expanded” state, and hiding them makes the disclosure group “collapsed”.")
}
And get:
That’s really it! DisclosureGroup really is helpful for when you want to make users aware of something, but have them opt-in to displaying it. Whether it’s Text, information, or sub-functionality:
You could even put another DisclosureGroup inside of a DisclosureGroup (the basis of an OutlineGroup).
Exploring DisclosureGroupStyle
One way we can look under the hood of DisclosureGroup is through DisclosureGroupStyle. By default, there is only one style available to us (AutomaticDisclosureGroupStyle, marked as automatic). If we were to “recreate” the default behavior, it would look something like this:
// Note: This can be done a few ways, but roll with this for now
struct NormalDisclosureGroupStyle: DisclosureGroupStyle {
func makeBody(configuration: Configuration) -> some View {
VStack {
Button {
withAnimation {
configuration.isExpanded.toggle()
}
} label: {
HStack {
configuration.label
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(configuration.isExpanded ? Angle(degrees: 90) : .zero)
}
.contentShape(Rectangle())
}
configuration.content
.frame(height: configuration.isExpanded ? nil : 0, alignment: .top)
.clipped()
}
}
}
You’ll notice the configuration provides us what we already know: Label, Content, and isExpanded. We use those to compose our disclosure (label on top, content on bottom) and toggle behavior around the expansion state. Simple enough.
What if…
Here’s a thought, though. What if we made one simple change: place content on top, label on bottom?
struct FlippedDisclosureGroupStyle: DisclosureGroupStyle {
func makeBody(configuration: Configuration) -> some View {
VStack {
configuration.content
.frame(height: configuration.isExpanded ? nil : 0, alignment: .top)
.clipped()
Button {
withAnimation {
configuration.isExpanded.toggle()
}
} label: {
HStack {
configuration.label
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(configuration.isExpanded ? Angle(degrees: -90) : .zero)
}
.contentShape(Rectangle())
}
}
}
}
The result would be:
Mind. Blown.
Something so simple as changing the order can make a component feel like it’s taken on an entire new identity.
Looks like sheet, but not sheet at all
There are a few examples where we see something similar to a reversed disclosure. The Apple Stocks and Find My apps both have a “minimized view” that expands to a more robust feature. We’re also walking a fine line in almost recreating a sheet (which is probably what the Apple apps I mentioned use to get detents and such).
The big difference here is that sheet usually pulls up over everything else on the screen, including TabViews and such (the Find My app performs some hackery but that’s a topic for another day). Here, we have the ability for a “expand-up” drawer of sorts that we can place really anywhere.
A simple use case would be a shopping cart. We can have the Label display our total items and cost, and simply tap to see the contents of our cart:
Lessons in Style
Changing the expected experience of a component through its style implores us to take a step back and look for lessons learned. We shouldn’t be too taken aback: components like TabView and Picker offer styles that completely change the UX. However, they do so around the core functionality (switching between Views and reviewing/setting selections, respectively).
The same applies with DisclosureGroup: while the aesthetics and direction changed, the core functionality of expanding and contrasting a span is maintained.
This informs us of a few SwiftUI component composition principles:
Componentry should primarily focus on functionality and purpose
How componentry is presented (beyond the typical fonts, sizes, etc) could/should be considered as styles
This is different thinking, for sure. In UIKit this could mean pointing to a different XIB or Storyboard. And let’s be honest, a typical SwiftUI response is to build sibling components (e.g. creating an UpwardDisclosureGroup).
But by centering around functionality, SwiftUI is helping us think in this way:
What does an experience need to accomplish, then how do I make it look and feel appropriate.
We love to jump to the looks and it makes sense considering we live in a marketing driven culture. But if the user can’t actually accomplish something, then we’ve made fools gold.
Therefore, we need to consider composing components with a functionality first approach, and then determine how we can pivot aesthetics around that core, gracefully. Styles help to do this.
Conclusion
We started with DisclosureGroup and ended with a life lesson in SwiftUI composition and styling. What seemed like such a simple (yet useful) component turned out to be a great candidate to explore. Perhaps the Captain went a little overboard with this one, but what good is the experience if we can’t take a part of it with us?