In my app I have a sheet that has relatively small content inside. I’d like to fit the height of the sheet to the content of it. As I of course also want to support large type and don’t know how text will break to new lines, I can’t simply set a fixed pixel height.
Currently I’m using this trick I saw in some blog post to get it to work. This is the view I’m presenting inside my .sheet:
struct SheetContentView: View {
@State private var contentHeight: CGFloat = .zero
var body: some View {
VStack {
Text("Foo")
Text("Bar")
// …
}
.background(
// Required for proper calculation of content height
GeometryReader { geo in
Color.clear
.onChange(of: geo.size.height, initial: true) { _, newValue in
contentHeight = newValue
}
}
)
.presentationDetents([.height(contentHeight)])
}
}
Is this a valid way to do it? What are the best practices to handle this properly or is there even a native way to do this?
This is a perfectly good pattern to have your sheet set to the custom detent of the height of the view it contains. This can get tricky with ScrollViews in sheets in which case you need to measure the size of the view inside the ScrollView since the ScrollView itself doesn't have a preferred size when it's height proposal is nil.
For slightly better performance you can use on geometry change instead:
import SwiftUI
@main struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State private var showingSheet = false
@State private var swatchHeight: CGFloat = 100
@State private var measuredSheetHeight: CGFloat = 100
@State private var selectedDetent: PresentationDetent = .height(100)
var body: some View {
Button("Show Sheet") {
showingSheet = true
}
.sheet(isPresented: $showingSheet) {
AutoSizingSheet(
swatchHeight: $swatchHeight,
measuredSheetHeight: $measuredSheetHeight,
selectedDetent: $selectedDetent
)
}
}
}
private struct AutoSizingSheet: View {
@Binding var swatchHeight: CGFloat
@Binding var measuredSheetHeight: CGFloat
@Binding var selectedDetent: PresentationDetent
var body: some View {
VStack(spacing: 20) {
Color.blue
.frame(height: swatchHeight)
Stepper(
"Color height: \(Int(swatchHeight))",
value: $swatchHeight,
in: 100...500,
step: 100
)
Divider()
VStack(alignment: .leading, spacing: 8) {
Text("Debug")
.font(.headline)
Text("Measured content height: \(Int(measuredSheetHeight))")
Text("Selected detent: .height(\(Int(measuredSheetHeight)))")
}
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding()
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.height
} action: { newHeight in
let roundedHeight = newHeight.rounded(.up)
guard abs(roundedHeight - measuredSheetHeight) > 0.5 else {
return
}
measuredSheetHeight = roundedHeight
selectedDetent = .height(roundedHeight)
}
.presentationDetents(
[ .height(measuredSheetHeight)],
selection: $selectedDetent
)
}
}
#Preview {
ContentView()
}