This posting is based on Kavsoft video:
https://youtu.be/CyMtjSspJZA?si=IxbsPgyOmR5OM9HD
SwiftUI presentationDetents on iOS 15 and below
As you know, Apple made presentationDetents for developers.
However, developers can use this function from iOS 16.
https://developer.apple.com/documentation/swiftui/view/presentationdetents(_:)
presentationDetents(_:) | Apple Developer Documentation
Sets the available detents for the enclosing sheet.
developer.apple.com
Including me, many developers have no right to deployment target. Just follow the rule of company. 😭
So, I made a wheel. I want to share with developers suffered like me. 🙏
As you see,
the content VStack is filled with 4 Rectangles,
each has a height of 50.
The height of the handle area is 16, and the height of the bottom safe area is 34,
so the total contentHeight is 250.
And now,
the content VStack is filled with 6 Rectangles,
each has a height of 50.
The height of the handle area is 16, and the height of the bottom safe area is 34,
so the total contentHeight is 350.
You can config content closure with your own content.
(1) ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var showBottomSheet: Bool = false
@State private var bottomSafeAreaHeight: CGFloat = UIApplication.shared.rootViewController?.view.safeAreaInsets.bottom ?? 0
var body: some View {
ZStack {
VStack {
Spacer().frame(height: 50)
Button(action: {
withAnimation {
showBottomSheet = true
}
}, label: {
Text("Show Bottom Sheet")
.foregroundStyle(Color.white)
.font(.system(size: 16, weight: .bold))
.frame(width: 300, height: 50)
.background(Color.blue)
.clipped()
.clipShape(Capsule())
})
Spacer()
}
.zIndex(0)
BottomSheetDrawerView(
showBottomSheet: $showBottomSheet,
threshold: 3,
content: { // MARK: Config this closure with your customized content.
VStack(spacing: 0) {
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.red)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.blue)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.green)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.yellow)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.orange)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.brown)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.green)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.red)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.blue)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.green)
Rectangle()
.frame(height: 50)
.foregroundStyle(Color.yellow)
Spacer()
.frame(height: self.bottomSafeAreaHeight)
}
}
)
.zIndex(1)
}
}
}
#Preview {
ContentView()
}
(2) BottomSheetDrawerView.swift
import SwiftUI
struct BottomSheetDrawerView<Content: View>: View {
@State private var bottomSheetOffset: CGFloat = 0
@State private var lastOffset: CGFloat = 0
@GestureState private var gestureOffset: CGFloat = 0
@State private var contentHeight: CGFloat = 0
@Binding var showBottomSheet: Bool
let content: () -> Content
private let threshold: CGFloat
init(
showBottomSheet: Binding<Bool>,
threshold: CGFloat = 3,
@ViewBuilder content: @escaping () -> Content
) {
self._showBottomSheet = showBottomSheet
self.threshold = threshold
self.content = content
}
var body: some View {
Group {
if showBottomSheet {
// MARK: Dimmed Background
Color.black
.opacity(0.5)
.ignoresSafeArea()
.transition(.opacity)
.animation(.easeInOut, value: showBottomSheet)
.onTapGesture {
withAnimation {
showBottomSheet = false
}
}
}
// MARK: Bottom Sheet
GeometryReader { proxy in
let screenHeight = proxy.frame(in: .global).height
ZStack {
Color.white
.clipShape(
CustomCorner(
corners: [.topLeft, .topRight],
radius: 20
)
)
VStack(spacing: 0) {
Capsule()
.fill(Color.gray)
.frame(width: 80, height: 4)
.padding(.top, 12)
content()
}
.background(
GeometryReader { contentProxy in
Color.clear
.preference(key: ContentHeightKey.self, value: contentProxy.size.height)
}
)
.onPreferenceChange(ContentHeightKey.self) { contentHeight in
self.contentHeight = contentHeight
print("### contentHeight: \(self.contentHeight)")
}
.frame(maxHeight: .infinity, alignment: .top)
}
.offset(y: screenHeight - contentHeight)
.offset(y: {
if showBottomSheet {
if -bottomSheetOffset >= 0 {
if bottomSheetOffset <= screenHeight {
return bottomSheetOffset
} else {
return screenHeight
}
} else {
return bottomSheetOffset
}
} else {
return (screenHeight) - (screenHeight - contentHeight)
}
}())
.gesture(
DragGesture()
.updating(
$gestureOffset,
body: { value, state, transaction in
state = value.translation.height
onChangeBottomSheetOffset()
}
)
.onEnded({ value in
let thresholdSize: CGFloat = (contentHeight / threshold)
withAnimation {
if bottomSheetOffset < 0 {
bottomSheetOffset = 0
} else {
if bottomSheetOffset > thresholdSize {
bottomSheetOffset = 0
showBottomSheet = false
} else {
bottomSheetOffset = 0
}
}
}
lastOffset = bottomSheetOffset
})
)
}
.ignoresSafeArea(.all, edges: .bottom)
.onChange(of: showBottomSheet) { showBottomSheet in
if !showBottomSheet {
bottomSheetOffset = 0
lastOffset = 0
}
}
}
}
private func onChangeBottomSheetOffset() {
DispatchQueue.main.async {
bottomSheetOffset = gestureOffset + lastOffset
}
}
private struct CustomCorner: Shape {
let corners: UIRectCorner
let radius: CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(
width: radius,
height: radius
)
)
return Path(path.cgPath)
}
}
}
fileprivate struct ContentHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
extension UIApplication {
var rootViewController: UIViewController? {
return UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.flatMap({ $0.windows })
.first(where: { $0.isKeyWindow })?
.rootViewController
}
}
you can download source code here:
'iOS > SwiftUI' 카테고리의 다른 글
[iOS / SwiftUI] Staggered Animation View (0) | 2025.04.01 |
---|---|
[iOS / SwiftUI] VisionOS Style Menu Bar (0) | 2025.03.22 |
[iOS / SwiftUI] DownsizedImage (0) | 2025.03.03 |
[iOS / SwiftUI] SwiftUI Notification Deep Linking (0) | 2025.02.16 |
[iOS / SwiftUI] SwiftUI Custom Alerts (0) | 2025.02.08 |