8

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?

Rahul Bir
  • 347
  • 2
  • 11

5 Answers5

9

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 var defaultValue: CGFloat = .zero
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
Rahul Bir
  • 347
  • 2
  • 11
  • 1
    Why is overlay preferred over background? – dalton_c Jun 21 '23 at 15:23
  • I was facing an issue with my TextView because my text was too long and this solution was forcing it to one line. Simple fix is to add: `.fixedSize(horizontal: false, vertical: true)` to any TextView you might have that needs to calculate it's height automatically – Caio Ambrosio Aug 21 '23 at 13:58
5

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

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thanks! Hadn't used preference keys before. Under the reduce function you need to set value otherwise the defaultValue will always be used. Also, use overlay rather than background. – Rahul Bir Nov 18 '22 at 21:26
  • Oh yeah, I don't know how I missed that. An upvote if the answer was helpful would be nice. – jnpdx Nov 18 '22 at 21:28
  • I am getting error for the target machine! PresentationDetents are supported for the IOS 16+. Is there any other way to do the same thing? – Hitesh Patil Jan 18 '23 at 14:27
  • What's the advantage of using a `PreferenceKey` over just writing to the `@State` var directly? – dalton_c Jun 21 '23 at 15:11
  • How would you write to it directly here? Keep in mind that you can't (or shouldn't) do side effects like assignment from within the `body` render pass. – jnpdx Jun 21 '23 at 15:32
1

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)
        }
    }
}
mevmev
  • 13
  • 3
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 02 '23 at 03:02
0

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()
            .modifier(GetHeightModifier(height: $sheetHeight))
            .presentationDetents([.height(sheetHeight)])
        }
    }
}

kimigori
  • 651
  • 6
  • 7
-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))
    }
}

}

  • I think you need to explain the answer, to let people understand what stride do, and how this is gonna work. But at first try, doesn't work. – Alessandro Pace Jun 16 '23 at 21:27