SwiftUI Tutorials: Creating an Expandable View with Long Press Gesture in SwiftUI
In this tutorial, we'll walk through the process of creating an expandable view using SwiftUI. This interactive UI component allows users to expand a small box into a full-screen view with a long press gesture, and collapse it back with a double tap. 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:
import SwiftUI
struct ContentView: View {
@State private var selectedIndex: Int? = nil
@Namespace private var namespace
var body: some View {
// Main content here
}
}
Creating the Main Layout
Our main layout consists of a ZStack
with a background color and a VStack
containing our expandable boxes. Here's how we set it up:
var body: some View {
ZStack {
Color(hex: 0xe4f6ff).ignoresSafeArea()
VStack(alignment: .leading, spacing: 12) {
VStack (spacing: 12) {
ForEach(0..<3) { index in
if selectedIndex != index {
smallBox(index: index)
.matchedGeometryEffect(id: "box\(index)", in: namespace)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: selectedIndex)
}
}
}
}
.frame(minWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .topLeading)
// Expanded view code here
}
}
Creating the Small Box View
Next, let's create the smallBox
function that returns our expandable box view:
func smallBox(index: Int) -> some View {
ZStack {
VStack(spacing:0) {
Image(systemName: "hand.tap.fill")
.font(.system(size: 22, weight: .regular))
.imageScale(.small)
.foregroundStyle(.white)
.frame(width: 32, height: 32)
Text("Long Press")
.font(.system(size: 17))
.foregroundStyle(.white)
.fixedSize(horizontal: false, vertical: true)
.clipped()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.clipped()
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 200, alignment: .center)
.background(Color(hex: 0x7fd2ff))
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(.black, style: StrokeStyle(lineWidth: 1, lineJoin: .round))
.opacity(0.1)
)
.gesture(
LongPressGesture(minimumDuration: 0.5)
.onEnded { _ in
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
selectedIndex = index
}
}
)
}
Creating the Expanded View
Now, let's create the expanded view that appears when a box is long-pressed:
var expandedView: some View {
VStack(spacing:0) {
Image(systemName: "hand.tap.fill")
.font(.system(size: 22, weight: .regular))
.imageScale(.small)
.foregroundStyle(Color(hex: 0x7fd2ff))
.frame(width: 32, height: 32)
Text("Double Tap")
.font(.system(size: 17))
.foregroundStyle(Color(hex: 0x7fd2ff))
.fixedSize(horizontal: false, vertical: true)
.clipped()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color(hex: 0xe4f6ff))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.ignoresSafeArea()
}
Implementing the Expansion Animation
To create a smooth transition between the small box and the expanded view, we use SwiftUI's matchedGeometryEffect
. Add this code to your body
:
if let selectedIndex = selectedIndex {
expandedView
.matchedGeometryEffect(id: "box\(selectedIndex)", in: namespace)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: selectedIndex)
.transition(.asymmetric(insertion: .opacity, removal: .opacity))
.zIndex(1)
.onTapGesture(count: 2) {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
self.selectedIndex = nil
}
}
}
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)
}
}