Building a Custom Selection View in SwiftUI

In this tutorial, we'll create a custom selection view using SwiftUI. This view will allow users to select from multiple options, with a visually appealing design and smooth animations. Let's break down the code and explain each part step by step.

1. Setting Up the Project

First, we'll start by importing SwiftUI and setting up some initial variables:

import SwiftUI

let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
let safeAreaTop = window?.safeAreaInsets.top

These lines help us get the safe area insets of the device, which we'll use later for proper padding.

2. Creating the ContentView

Next, we'll define our main ContentView struct:

struct ContentView: View {
    @State private var selectedIndex: Int? = 0

    var body: some View {
        // Main view content
    }
}

We use a @State property wrapper for selectedIndex to keep track of the currently selected item.

3. Building the Main Layout

Inside the body property, we'll create our main layout:

VStack(alignment: .leading, spacing: 12) {
    Text("Tap to change selection")
        .font(.system(size: 22, weight: .semibold))
        .frame(maxWidth: .infinity, alignment: .center)
        .foregroundStyle(Color(hex: 0x7fd2ff))
        .multilineTextAlignment(.center)
        .padding(.top, safeAreaTop ?? 20)
        .padding(.bottom, 10)

    VStack(spacing: 12) {
        ForEach(0..<5) { index in
            selectionItem(index: index)
        }
    }
}
.padding(.horizontal, 16)
.frame(minWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .topLeading)
.background(Color(hex: 0xdff4ff).ignoresSafeArea())

This creates a vertical stack with a title and five selection items.

4. Implementing the Selection Item

The selectionItem function creates each individual selectable item:

private func selectionItem(index: Int) -> some View {
    let isSelected = selectedIndex == index

    return HStack(spacing: 0) {
        VStack {}
            .frame(width: 48, height: 48, alignment: .topLeading)
            .background(Color(hex: 0x7fd2ff))
            .cornerRadius(24)
            .opacity(isSelected ? 1 : 0.2)
    }
    .padding(16)
    .frame(maxWidth: .infinity, alignment: .leading)
    .background(.white)
    .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
    .overlay(
        RoundedRectangle(cornerRadius: 24, style: .continuous)
            .stroke(Color(hex: 0x7fd2ff), style: StrokeStyle(lineWidth: 2, lineJoin: .round))
            .opacity(isSelected ? 1 : 0)
    )
    .scaleEffect(isSelected ? 1.04 : 1.0)
    .onTapGesture {
        withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
            selectedIndex = index
        }
    }
}

This function creates a rounded rectangle with a circular indicator. When selected, the item scales up slightly and shows a border.

5. Adding a Custom Color Extension

To make working with hex colors easier, we'll add an 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)
    }
}

This allows us to use hex values for colors throughout our code.

6. Preview

Finally, we'll add a preview for our ContentView:

#Preview {
    ContentView()
}

This allows us to see our view in the Xcode preview pane.