Search code examples
swiftui

Customize DisclosureGroup header only


I'm trying to customize a DisclosureGroup via a custom DisclosureGroupStyle to move the expand/collapse caret to the very left (as opposed to the standard placement on the right).

I've got the header part down but I can't get the content to render the same as it does when using the standard AutomaticDisclosureGroupStyle.

Here is the definition of my style:

struct MyDisclosureStyle: DisclosureGroupStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack {
            Button {
                withAnimation {
                    configuration.isExpanded.toggle()
                }
            } label: {
                HStack(alignment: .firstTextBaseline) {
                    Image(systemName: "chevron.right")
                        .foregroundColor(.accentColor)
                        .rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
                        .animation(
                            .easeInOut(duration: 0.3),
                            value: configuration.isExpanded
                        )
                    configuration.label
                }
                .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            if configuration.isExpanded {
                configuration.content
            }
        }
    }
}

Here is a comparison of how the automatic and custom styles render: enter image description here

How can I get configuration.content to "resolve its appearance automatically based on the current context" as AutomaticDisclosureGroupStyle does to maintain the List styling that I'm placing the DisclosureGroup in?

Here is the usage code:

struct ContentView: View {
    @State private var isExpanded = true
    var body: some View {
        VStack {
            List {
                DisclosureGroup(isExpanded: $isExpanded) {
                    ForEach(1..<3) {
                        Text($0.formatted())
                    }
                } label: {
                    Text("AutomaticDisclosureGroupStyle")
                }
                .disclosureGroupStyle(.automatic)
            }
            Divider()
            List {
                DisclosureGroup(isExpanded: $isExpanded) {
                    ForEach(1..<3) {
                        Text($0.formatted())
                    }
                } label: {
                    Text("MyDisclosureStyle")
                }
                .disclosureGroupStyle(MyDisclosureStyle())
            }
        }
    }
}

Solution

  • Try removing the outer VStack from the body of MyDisclosureStyle. This way, the body delivers a collection of separate views (in conformance with ViewBuilder) instead of a container view with the views inside:

    func makeBody(configuration: Configuration) -> some View {
        // VStack {
            Button {
                // ...
            } label: {
                // ...
            }
            .buttonStyle(.plain)
            if configuration.isExpanded {
                configuration.content
            }
        // }
    }
    

    This expands to separate rows inside the List. You may want to make more adaptions to the styling, but it essentially works the way you want it to:

    Screenshot

    To fix the alignment of the row separator, try applying .alignmentGuide to the button label:

    Button {
        withAnimation {
            configuration.isExpanded.toggle()
        }
    } label: {
        HStack(alignment: .firstTextBaseline) {
            // ...
        }
        .contentShape(Rectangle())
        .alignmentGuide(.listRowSeparatorLeading) { dimensions in
            dimensions[.leading]
        }
    }
    .buttonStyle(.plain)
    

    Screenshot