I'd like to have a SwiftUI sheet that either shows the header or complete content. Requiring iOS 16 is ok.
I already get the correct two measured heights a presentationDetents
import Foundation
import SwiftUI
struct ContentView: View {
@State private var showSheet = false
@State private var headerSize: CGSize = .zero
@State private var overallSize: CGSize = .zero
var body: some View {
Button("View sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
Group {
VStack(alignment: .leading) {
Text("Header")
.background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: HeaderSizePreferenceKey.self, value: geometryProxy.size)
}
)
Text("")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
} // VStack
.padding()
.background(
//measure without spacer
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
Spacer()
} // Group
.onPreferenceChange(SizePreferenceKey.self) { newSize in
overallSize.height = newSize.height
}
.onPreferenceChange(HeaderSizePreferenceKey.self) { newSize in
headerSize.height = newSize.height
}
.presentationDetents([
.height(headerSize.height),
.height(overallSize.height)
]
)
} // sheet content
}
}
struct HeaderSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
struct MySheet_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This code is based on ideas from
This is the header content size:
What can I do that the last case shows the top of the content ("Header") instead of the center of the content?
CodePudding user response:
So you want this:
The sheet content's intrinsic height includes both the header and the body (the “Some very long text” lines). The problem to solve: when the sheet is at the header detent, the content doesn't fit in the height, and draws itself center-aligned. One way to solve this is to put the content inside a container that draws its children top-aligned.
I tried ZStack(alignment: .top)
, and I tried adding a .frame(alignment: .top)
modifier, but neither worked. I discovered that GeometryReader
does what we need.
I also restructured the sheet content so the header's height is measured including vertical padding.
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Button("View sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContent()
}
}
}
struct SheetContent: View {
@State private var heights = HeightRecord()
var body: some View {
// Outermost GeometryReader exists only to draw its content top-aligned instead of center-aligned.
GeometryReader { _ in
// spacing: 0 here so only the standard padding separates the
// header from the body.
VStack(alignment: .leading, spacing: 0) {
Text("Header")
.padding([.top, .bottom])
.recordHeight(of: \.header)
// Standard spacing here for the body's subviews.
VStack(alignment: .leading) {
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
Text("Some very long text ...")
}
}
// No top padding here because the header has top padding.
.padding([.leading, .trailing, .bottom])
.recordHeight(of: \.all)
}
.onPreferenceChange(HeightRecord.self) {
heights = $0
}
.presentationDetents([
.height(heights.header ?? 10),
.height(heights.all ?? 10)
])
}
}
struct HeightRecord: Equatable {
var header: CGFloat? = nil
var all: CGFloat? = nil
}
extension HeightRecord: PreferenceKey {
static var defaultValue = Self()
static func reduce(value: inout Self, nextValue: () -> Self) {
value.header = nextValue().header ?? value.header
value.all = nextValue().all ?? value.all
}
}
extension View {
func recordHeight(of keyPath: WritableKeyPath<HeightRecord, CGFloat?>) -> some View {
return self.background {
GeometryReader { g in
var record = HeightRecord()
let _ = record[keyPath: keyPath] = g.size.height
Color.clear
.preference(
key: HeightRecord.self,
value: record)
}
}
}
}