This blog post will explain alternatives for programmatically determining a presented sheet's detent in SwiftUI.
To present a bottom sheet on iOS is very easy with SwiftUI thanks to the sheet
and presentationDetents
modifiers.
To turn a normal sheet into a bottom sheet, we only need to define supported detents with
.presentationDetents([.medium, .large])
.
At work I was asked if it is possible to programmatically determine the presented sheet's detent and react to its changes. A possible use case might be to show UI controls (i.e., a large icon) on a large sheet but hide the controls on a bottom sheet.
Curt Clifton pointed out to me that SwiftUI offers a version of the modifier that takes a binding to the currently selected detent: presentationDetents(_:selection:)
That should be the preferred solution for most developers!
But what if the View in the bottom sheet is provided by a 3rd party SDK? Should the SDK offer to inject such binding? What if the app developer doesn't provide the binding? I was looking for alternatives that could work without any prerequisites for the consumer of the View.
The verticalSizeClass is unaffected by detent changes, so that's not an option π
I asked ChatGTP for a workaround, and here is the answer from OpenAI.
SheetDetentObserver: This class conforms to
ObservableObject
and publishes changes to the current detent.DetentSheetView: A custom
UIViewControllerRepresentable
that presents a sheet and usesUISheetPresentationController
to manage detents.Coordinator: Acts as the delegate for
UISheetPresentationControllerDelegate
, observing changes in the detent and updating theSheetDetentObserver
.
π€ Hmm, oookkkkk ... maybe it's possible, but the solution seems to have a lot of overhead. I was looking for a simpler solution that does not require using UIKit.
Do I need the detent, or would it be okay to know the sheet size? Then I could hide the unwanted UI controls if a certain threshold is exceeded for the height.
Let's try it! GeometryReader
to the rescue! Using GeometryReader
for this particular use case seems perfectly fine but be aware that this container view comes with some challenges.
We can get the container size and use a threshold (e.g. 500) to determine when to show certain controls.
struct SheetContentView: View {
var body: some View {
GeometryReader { proxy in
ScrollView {
SomeHeaderView(showImage: proxy.size.height > 500 ? true : false)
SomeSheetContentView()
}
}
}
}
We could also create a custom SwiftUI environment variable to make the value accessible for all subviews.
struct SheetContentViewSizeKey: EnvironmentKey {
static var defaultValue: CGSize = .zero
}
extension EnvironmentValues {
var mySheetContentViewSize: CGSize {
get { self[SheetContentViewSizeKey.self] }
set { self[SheetContentViewSizeKey.self] = newValue }
}
}
Then the mySheetcontentViewSize
can be accessed by any view in the view hierarchy.
struct SomeSheetContentView: View {
// built-in SwiftUI environment variable
@Environment(\.isPresented) var isPresented
// custom
@Environment(\.mySheetContentViewSize) var mySheetContentViewSize
var body: some View {
VStack {
Text("Height: \(mySheetContentViewSize.height)")
Text("Is Presented: \(isPresented)")
}
}
}
Complete code from my example:
struct ContentView: View {
@State var isSheetShown = false
var body: some View {
VStack {
Button("Open Sheet") { isSheetShown.toggle() }
.sheet(isPresented: $isSheetShown) {
SheetContentView()
.presentationDetents([.medium, .large])
}
}
.padding()
}
}
struct SheetContentView: View {
var body: some View {
GeometryReader { proxy in
ScrollView {
SomeHeaderView(showImage: proxy.size.height > 500 ? true : false)
SomeSheetContentView()
}
.environment(\.mySheetContentViewSize, proxy.size)
}
}
}
struct SomeHeaderView: View {
var showImage: Bool
var body: some View {
HStack {
Text("My Sheet Header")
Spacer()
if showImage {
Image(systemName: "moon.stars.fill")
.frame(width: 75, height: 75)
}
}
.background(.red)
}
}
struct SomeSheetContentView: View {
// built-in SwiftUI environment variable
@Environment(\.isPresented) var isPresented
// custom
@Environment(\.mySheetContentViewSize) var mySheetContentViewSize
var body: some View {
VStack {
Text("Height: \(mySheetContentViewSize.height)")
Text("Is Presented: \(isPresented)")
}
}
}
struct SheetContentViewSizeKey: EnvironmentKey {
static var defaultValue: CGSize = .zero
}
extension EnvironmentValues {
var mySheetContentViewSize: CGSize {
get { self[SheetContentViewSizeKey.self] }
set { self[SheetContentViewSizeKey.self] = newValue }
}
}
This blog post might not be helpful for everyone, but I thought it's a good idea to document this option. Not only for me but also for my co-workers π