SwiftUI Tutorial: Creating an Accordion Layout in SwiftUI

In this tutorial, we'll walk through the process of building an accordion layout using SwiftUI. This interactive UI component allows users to expand and collapse content sections, providing an efficient way to display information in a compact form. Let's break down the code and explain each part in detail.

Setting Up the Project

First, create a new SwiftUI project in Xcode. Replace the contents of your ContentView.swift file with the following code structure:

import SwiftUI

// We'll add our code here

struct ContentView: View {
    var body: some View {
        // Main content here
    }
}

Creating the AccordionItem Model

We'll start by defining a model for our accordion items:

struct AccordionItem: Identifiable {
    let id = UUID()
    let title: String
    let content: String
    var isExpanded: Bool = false
}

This structure represents each item in our accordion, with a unique identifier, title, content, and an isExpanded flag to track its state.

Building the Main ContentView

Now, let's set up our main ContentView:

struct ContentView: View {
    @State private var accordionItems: [AccordionItem] = [
        AccordionItem(title: "Accordion 1", content: "Content for Accordion 1"),
        AccordionItem(title: "Accordion 2", content: "Content for Accordion 2"),
        AccordionItem(title: "Accordion 3", content: "Content for Accordion 3"),
        AccordionItem(title: "Accordion 4", content: "Content for Accordion 4")
    ]

    var body: some View {
        VStack(spacing: 12) {
            Text("Tap to toggle an accordion's state")
                .font(.system(size: 17))
                .frame(maxWidth: .infinity, alignment: .center)
                .frame(height: 44, alignment: .center)
                .foregroundStyle(.white)
                .multilineTextAlignment(.center)
                .clipped()

            ForEach($accordionItems) { $item in
                AccordionView(item: $item)
            }
        }
        .frame(minWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .top)
        .background(Color(hex: 0x7fd2ff).ignoresSafeArea())
    }
}

This view creates a list of AccordionItems and displays them using a custom AccordionView.

Creating the AccordionView

The AccordionView is responsible for rendering each accordion item:

struct AccordionView: View {
    @Binding var item: AccordionItem

    var body: some View {
        VStack(spacing: 0) {
            Button(action: {
                withAnimation(.spring()) {
                    item.isExpanded.toggle()
                }
            }) {
                HStack(spacing: 0) {
                    Text(item.title)
                        .font(.system(size: 17))
                        .foregroundStyle(Color(hex: 0x7fd2ff))
                        .opacity(0.6)
                        .fixedSize(horizontal: false, vertical: true)
                        .clipped()
                    Spacer()
                    Image(systemName: "chevron.down.circle.fill")
                        .font(.system(size: 22, weight: .regular))
                        .symbolRenderingMode(.hierarchical)
                        .foregroundStyle(Color(hex: 0x7fd2ff))
                        .frame(width: 44, height: 44)
                        .rotationEffect(.degrees(item.isExpanded ? 0 : -90))
                }
                .padding(.trailing, 4)
                .padding(.leading, 16)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .frame(height: 56, alignment: .leading)
            .background(.white.opacity(0))

            if item.isExpanded {
                VStack(spacing: 0) {
                    Text(item.content)
                        .font(.system(size: 17))
                        .foregroundStyle(Color(hex: 0x7fd2ff))
                        .fixedSize(horizontal: false, vertical: true)
                        .clipped()
                        .padding()
                }
                .frame(maxWidth: .infinity)
                .background(Color.white.opacity(0.1))
                .transition(.opacity.combined(with: .move(edge: .top)))
            }
        }
        .frame(maxWidth: .infinity, alignment: .top)
        .background(.white)
        .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
        .shadow(color: .black.opacity(0.07), radius: 2, x: 0, y: 2)
        .animation(.spring(), value: item.isExpanded)
    }
}

This view creates a button for the accordion header and conditionally renders the content when the item is expanded.

Adding a Custom Color Extension

To use hex colors in our SwiftUI views, we'll add a custom extension to the Color struct:

extension Color {
    init(hex: Int, alpha: Double = 1.0) {
        let red = Double((hex & 0xff0000) >> 16) / 255.0
        let green = Double((hex & 0xff00) >> 8) / 255.0
        let blue = Double((hex & 0xff) >> 0) / 255.0
        self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
    }
}