Reputation: 837
Let's say I have a custom view inside of a sheet, something like this
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.presentationDetents([.height(250)])
How can I get the exact height of the VStack and pass it to the presentationDetents modifier so that the height of the sheet is exactly the height of the content inside?
Upvotes: 35
Views: 27360
Reputation: 262
New in iOS 18
SwiftUI has a dedicated
presentationSizing()
modifier that gives us fine-grained control over how presented views are sized on the screen.
This is different from the
presentationDetents()
modifier that allows us to create bottom sheets and similar – presentationSizing() is for controlling the shape of the view.
That uses the .form setting, which is one of the built-in sizes – on iPhone it will just be a regular sheet, but on iPad it's a large square shape that's centered neatly.
Another great option for presentationSizing() is .fitted, which sizes the sheet according to its content. Even better, this can be added to other sizes: you can use .form.fitted(horizontal: true, vertical: false) to mean "start with the form size, but shrink horizontally to fit my content".
Upvotes: -1
Reputation: 2658
🐙 Here is an update to the implementations, with text auto-resizing and wrapping fixed!. I've corrected the placement of the sizing calculation to fix the issue where a ScrollView
in the content, would not size correctly.
Soo, now, if you do wrap the content in a ScrollView
then the text will wrap to its new size. 🤠
Seems the scrollview helps its content recalculate to grow.
Without the scrollview the text would just clip to its new value, and never resize.
Also, something to note, without the scrollview...., if you pull on the resize handle of the popup then it would resize the text content, but when you let go it will settle back to clipping the text.
Here's an update to the suggestions using all the good bits mentioned by everyone so far.....
//
// Popupsheet.swift
//
// Assembled by Leslie Godwin on 2025/01/20.
//
import SwiftUI
struct PopupSheetModifier<SheetContent: View>: ViewModifier
{
@State private var sheetHeight: CGFloat = .zero
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let content: () -> SheetContent
func body(content: Content) -> some View
{
content
.sheet(isPresented: $isPresented, onDismiss: onDismiss)
{
ScrollView
{
self.content()
.modifier(GetHeightModifier(height: $sheetHeight))
.presentationDetents([.height(sheetHeight)])
.presentationDragIndicator(.visible)
}
}
}
}
extension View {
func popupSheet<SheetContent: View>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> SheetContent
) -> some View {
self.modifier(PopupSheetModifier(isPresented: isPresented, onDismiss: onDismiss, content: content))
}
}
struct GetHeightModifier: ViewModifier
{
@Binding var height: CGFloat
func body(content: Content) -> some View {
content.background(
GeometryReader { geo -> Color in
height = geo.size.height
return .clear
}
)
}
}
struct TestPopup : View
{
@State var text = "Hello, this is the sheet content!"
@State var isSheetPresented = false
var body: some View
{
return VStack {
Button("Show Sheet") {
isSheetPresented = true
}
}
.popupSheet(isPresented: $isSheetPresented, onDismiss: {
text = "Hello, this is the sheet content!"
print("Sheet was dismissed")
})
{
Task
{
try await Task.sleep(for: .seconds(2))
withAnimation(.easeInOut.speed(0.1))
{
text = "some thing longer, some thing longer, some thing longer, some thing longer, some thing longer, some thing longer, "
}
}
return VStack {
Text(text)
.allowsTightening(true)
Image(systemName: "globe")
.resizable()
.frame(width: 100, height: 100)
Text("More text")
}
.padding(EdgeInsets(top: 30, leading: 10, bottom: 10, trailing: 10))
}
}
}
#Preview {
TestPopup()
}
Upvotes: 0
Reputation: 1626
Here is a variation of @jnpdx's answer wrapped into a view modifier, PopupSheetModifier. Then invoked on the sheet content via .popupSheet() view extension.
import SwiftUI
struct PopupSheetModifier<SheetContent: View>: ViewModifier {
@Binding var isPresented: Bool
@State private var sheetHeight: CGFloat = .zero
let onDismiss: (() -> Void)?
let content: () -> SheetContent
func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented, onDismiss: onDismiss) {
self.content()
.background(
GeometryReader { geo in
Color.clear
.preference(key: SizePreferenceKey.self, value: geo.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { newSize in
sheetHeight = newSize.height
}
.presentationDetents([.height(sheetHeight)])
.presentationDragIndicator(.visible)
}
}
}
extension View {
func popupSheet<SheetContent: View>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> SheetContent
) -> some View {
self.modifier(PopupSheetModifier(isPresented: isPresented, onDismiss: onDismiss, content: content))
}
}
#Preview {
@Previewable @State var isSheetPresented = false
VStack {
Button("Show Sheet") {
isSheetPresented = true
}
}
.popupSheet(isPresented: $isSheetPresented, onDismiss: {
print("Sheet was dismissed")
}) {
VStack {
Text("Hello, this is the sheet content!")
Image(systemName: "globe")
.resizable()
.frame(width: 100, height: 100)
Text("More text")
}
.padding(EdgeInsets(top: 30, leading: 10, bottom: 10, trailing: 10))
}
}
Upvotes: 5
Reputation: 837
Using the general idea made by @jnpdx including some updates such as reading the size of the overlay instead of the background, here is what works for me:
struct ContentView: View {
@State private var showSheet = false
@State private var sheetHeight: CGFloat = .zero
var body: some View {
Button("Open sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.overlay {
GeometryReader { geometry in
Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height)
}
}
.onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in
sheetHeight = newHeight
}
.presentationDetents([.height(sheetHeight)])
}
}
}
struct InnerHeightPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Upvotes: 29
Reputation: 967
1. Сreate a Сustom modifier that returns the heights of any view (this is a very useful modifier that you will most likely use elsewhere):
struct GetHeightModifier: ViewModifier {
@Binding var height: CGFloat
func body(content: Content) -> some View {
content.background(
GeometryReader { geo -> Color in
DispatchQueue.main.async {
height = geo.size.height
}
return Color.clear
}
)
}
}
2. Use the custom modifier to get the height.
struct ContentView: View {
@State private var showSheet = false
@State private var sheetHeight: CGFloat = .zero
var body: some View {
Button("Open sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.fixedSize(horizontal: false, vertical: true)
.modifier(GetHeightModifier(height: $sheetHeight))
.presentationDetents([.height(sheetHeight)])
}
}
}
Upvotes: 24
Reputation: 63
More reuseable
struct InnerHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() }
}
extension View {
func fixedInnerHeight(_ sheetHeight: Binding<CGFloat>) -> some View {
padding()
.background {
GeometryReader { proxy in
Color.clear.preference(key: InnerHeightPreferenceKey.self, value: proxy.size.height)
}
}
.onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in sheetHeight.wrappedValue = newHeight }
.presentationDetents([.height(sheetHeight.wrappedValue)])
}
}
struct ExampleView: View {
@State private var showSheet = false
@State private var sheetHeight: CGFloat = .zero
var body: some View {
Button("Open sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.fixedInnerHeight($sheetHeight)
}
}
}
Upvotes: 1
Reputation: 52367
You can use a GeometryReader
and PreferenceKey
to read the size and then write it to a state variable. In my example, I store the entire size, but you could adjust it to store just the height, since it's likely that that is the only parameter you need.
struct ContentView: View {
@State private var showSheet = false
@State private var size: CGSize = .zero
var body: some View {
Button("View sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
VStack {
Text("Title")
Text("Some very long text ...")
}
.padding()
.background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size.height = newSize.height
}
.presentationDetents([.height(size.height)])
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
Upvotes: 10
Reputation: 1
struct ContentView: View {
@State private var showingSheet = false
let heights = stride(from: 0.1, through: 1.0, by: 0.1).map { PresentationDetent.fraction($0) }
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
Text("Random text ")
.presentationDetents(Set(heights))
}
}
}
Upvotes: -5