Reading time: 2 min

This recipe shows how to implement an accordion view in SwiftUI. An accordion view is a layout that consists of a series of linked disclosure groups, only one of which can be expanded at the time.

The end result looks like this:

preview

This solution works for SwiftUI 2+ (iOS 14+, macOS 11+).

Here's the recipe:

  1. An accordion is a series of vertically stacked DisclosureGroups, which we'll refer to as sections.
  2. We'll provide a way to customize the title label and the content view of each section.
  3. Assing a custom Binding to each DisclosureGroup will make sure only one group is expanded at the time.
struct AccordionView<Label, Content>: View
where Label : View, Content : View {
  @Binding var expandedIndex: Int?
  let sectionCount: Int
  @ViewBuilder let label: (Int) -> Label
  @ViewBuilder let content: (Int) -> Content

  var body: some View {
    VStack {
      ForEach(0..<sectionCount, id: \.self) { index in
        DisclosureGroup(isExpanded: .init(get: {
          expandedIndex == index
        }, set: { value in
          expandedIndex = value ? index : nil
        }), content: {
          content(index)
        }, label: {
          label(index)
        })
      }
    }
  }
}

And here's some sample code, showcasing it in action:

struct AccordionViewTest: View {
  @State private var expandedIndex: Int? = nil
  private let sections = [
    SymbolGroup(title: "Weather",
                symbols: [
                  "sun.min", "sun.min.fill", "sun.max", "sunrise", "moon", "cloud.fog.fill", "cloud.hail"
                ]),
    SymbolGroup(title: "Devices",
                symbols: [
                  "keyboard", "airtag", "display", "pc", "macpro.gen2", "macmini", "flipphone"
                ]),
    SymbolGroup(title: "Transport",
                symbols: [
                  "airplane", "airplane.circle", "car.2", "tram.fill", "car.ferry", "bicycle", "sailboat.fill"
                ]),
    SymbolGroup(title: "Fitness",
                symbols: [
                  "figure.walk", "figure.boxing", "figure.golf", "figure.roll", "figure.outdoor.cycle", "dumbbell", "baseball.fill"
                ]),
    SymbolGroup(title: "Time",
                symbols: [
                  "clock", "clock.fill", "alarm", "deskclock", "timer.circle", "timer.square", "hourglass"
                ]),
  ]

  var body: some View {
    VStack {
      Text("Expanded index: \((expandedIndex == nil) ? "none" : "\(expandedIndex!)")")
      AccordionView(expandedIndex: $expandedIndex,
                    sectionCount: sections.count,
                    label: { index in
        Text(sections[index].title)
      },
                    content: { index in
        HStack {
          ForEach(sections[index].symbols, id: \.self) { symbol in
            Image(systemName: symbol)
              .frame(width: 32, height: 32)
          }
        }
      })
      Spacer()
    }
    .padding()
  }

  struct SymbolGroup {
    let title: String
    let symbols: [String]
  }
}

Previous Post