Floating bottom sheets are one of the cleanest modern UI patterns in iOS.
You see them in:
- Apple Maps
- Apple Music
- Stocks
- Reminders
- Shortcuts
…and almost every modern iOS app.
In this tutorial, we’ll build a smooth, draggable, snapping, blur-backed bottom sheet using pure SwiftUI.
- No UIKit.
- No hacks.
- No gesture bugs.
- Just clean 2026-style SwiftUI.
🎯 What We'll Build
A bottom sheet that:
- floats above your content
- has a blur background
- drags smoothly with your finger
- snaps to three positions (min / mid / full)
- has a soft spring animation
- supports scrollable content inside
Perfect for dashboards, media players, maps, tools, or any modern app section.
🧠 Key Concept
We track:
@State private var offset: CGFloat = 0
@State private var lastDrag: CGFloat = 0
Then we:
- Apply a DragGesture()
- Calculate the drag distance
- Snap to the closest position on gesture end
- Animate using .spring(...)
This creates a natural, Apple-like feel.
📦 Bottom Sheet Component
import SwiftUI
struct BottomSheet<Content: View>: View {
let maxHeight: CGFloat
let midHeight: CGFloat
let minHeight: CGFloat
@ViewBuilder var content: () -> Content
@State private var offset: CGFloat = 0
@State private var lastDrag: CGFloat = 0
var body: some View {
let drag = DragGesture()
.onChanged { value in
let newOffset = lastDrag + value.translation.height
if newOffset > 0 && newOffset < (maxHeight - minHeight) {
offset = newOffset
}
}
.onEnded { value in
let newOffset = lastDrag + value.translation.height
// Determine snap point
let snap: CGFloat
let top = 0.0
let middle = maxHeight - midHeight
let bottom = maxHeight - minHeight
if newOffset < middle / 2 {
snap = top
} else if newOffset < (bottom + middle) / 2 {
snap = middle
} else {
snap = bottom
}
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
offset = snap
lastDrag = snap
}
}
VStack {
Spacer()
VStack(spacing: 12) {
// Handle bar
RoundedRectangle(cornerRadius: 3)
.fill(Color.gray.opacity(0.4))
.frame(width: 40, height: 5)
.padding(.top, 8)
content()
.padding(.horizontal)
.padding(.bottom, 20)
}
.frame(width: UIScreen.main.bounds.width,
height: maxHeight,
alignment: .top)
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
.offset(y: offset)
.gesture(drag)
}
.ignoresSafeArea(edges: .bottom)
}
}
🖼 Example Usage
struct BottomSheetDemo: View {
var body: some View {
ZStack {
LinearGradient(colors: [.blue, .indigo],
startPoint: .top,
endPoint: .bottom)
.ignoresSafeArea()
BottomSheet(
maxHeight: 600,
midHeight: 350,
minHeight: 120
) {
VStack(alignment: .leading, spacing: 20) {
Text("Floating Bottom Sheet")
.font(.title2.bold())
Text("This bottom sheet snaps smoothly between three positions and supports any content inside. Try dragging it!")
.foregroundColor(.secondary)
.font(.body)
ForEach(0..<10) { i in
Text("Item \(i)")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
}
}
}
✔️ Final Result
You now have a fully interactive, snapping, floating bottom sheet with:
- blur
- spring animation
- multiple snap points
- scrollable content
- pure SwiftUI
This is the same pattern used across modern system apps — and now you can drop it into any of your projects.
Top comments (0)