Make sheet the exact size of the content inside

12.8k Views Asked by At

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?

5

There are 5 best solutions below

4
On BEST ANSWER

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()
    }
}
1
On

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)
        }
    }
}
5
On

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() }
}

1
On
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))
    }
}

}

0
On

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)])
        }
    }
}