For iOS 16+ use DisclosureGroupStyle :
import SwiftUI
struct CustomDisclosureGroupStyle<Label: View>: DisclosureGroupStyle {
let button: Label
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .center, spacing: 0) {
HStack(alignment: .top, spacing: 0) {
configuration.label
Spacer()
button
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
configuration.isExpanded.toggle()
}
}
if configuration.isExpanded {
configuration.content
.disclosureGroupStyle(self)
}
}
}
}
Here's a replacement for DisclosureGroup
that allows customisation of the toggle image, size & color dynamically according to isExpanded
:
/// Toggle Expand-Collapse section with:
/// * Label - header ending with Image button
/// * Image button - customisable expand-collapse toggle control
/// * Content - whatever you want to contain (expand to show, collapse to hide)
///
/// NOTE:
/// * Works with both 'ForEach {' and 'List { ForEach {' as Content.
/// * If a List is used, expands downwards to fill remainder of screen.
/// * If no List is used, expands just large enough to fit content height.
struct ExpandCollapse<Content: View, Label: View>: View {
typealias makeContent = () -> Content
typealias makeLabel = () -> Label
typealias dynamicImageColor = (Bool) -> Color
typealias dynamicRotation = (Bool) -> Double
typealias dynamicSystemName = (Bool) -> String
@Binding var isExpanded: Bool
@ViewBuilder var content: makeContent
@ViewBuilder var label: makeLabel
let imageSize: CGFloat
let imageRotation: dynamicRotation
let imageColor: dynamicImageColor
let imageName: dynamicSystemName
init(
isExpanded: Binding<Bool>,
content: @escaping makeContent,
label: @escaping makeLabel,
imageSize: CGFloat = 18,
imageColor: @escaping dynamicImageColor = { isExpanded in isExpanded ? Color.blue: Color.red },
imageRotation: @escaping dynamicRotation = { _ in 0 },
imageName: @escaping dynamicSystemName = { isExpanded in isExpanded ? "chevron.up" : "chevron.down" }
) {
_isExpanded = isExpanded
self.content = content
self.label = label
self.imageSize = imageSize
self.imageColor = imageColor
self.imageRotation = imageRotation
self.imageName = imageName
}
var expandCollapseButton: some View {
Image(systemName: imageName(isExpanded))
.resizable()
.scaledToFit()
.frame(width: imageSize, height: imageSize, alignment: .center)
.foregroundColor(imageColor(isExpanded))
.rotationEffect(.degrees(imageRotation(isExpanded)))
.pad(left: 5, right: 10)
}
var body: some View {
DisclosureGroup(isExpanded: $isExpanded, content: content, label: label)
.disclosureGroupStyle(CustomDisclosureGroupStyle(button: expandCollapseButton))
}
}
The definition for .pad
is:
import SwiftUI
@inlinable func insets(top: CGFloat = 0,
bot bottom: CGFloat = 0,
left leading: CGFloat = 0,
right trailing: CGFloat = 0) -> EdgeInsets {
EdgeInsets(top: top, leading: leading, bottom: bottom, trailing: trailing)
}
extension View {
@inlinable func pad(top: CGFloat = 0,
bot: CGFloat = 0,
left: CGFloat = 0,
right: CGFloat = 0) -> some View {
self.padding(insets(top: top, bot: bot, left: left, right: right))
}
@inlinable func zeropad() -> some View { pad() }
}
extension EdgeInsets {
static let zero = insets()
}
Example usage:
struct SimpleExampleView<T: MyData>: View {
@State isExpanded: Bool
@Binding items: [T]
func buildRow(item: T) -> some view { Text(item.title) }
var body: some View {
ExpandCollapse(isExpanded: $isExpanded) {
//List { // uncomment this & others to use List variant
ForEach($items) { index, item in
self.buildRow(item: item)
//.listRowBackground(Color.clear)
}
//}.listStyle(.plain)
} label: {
Text("\(items.count) Items")
}
}
With the example run as shown above, the ExpandCollapse
will expand just enough to fit its content height.
Alternatively, you can uncomment the indicated lines so a List
is used. Then the ExpandCollapse
will expand downwards to fill the remainder of the screen. This produces as large an area as possible for list interaction such as drag 'n' drop.