I'm encountering the following issue:
When a sheet with multiple detents is displayed:
How can I get a progressive resizing of the view presented when changing the detend programmatically?
Here is a code example and video to demonstrate
struct TestView: View {
@State var selectedDetent: PresentationDetent = .medium
@State var detents: Set<PresentationDetent> = [.large, .medium]
@State var height: CGFloat = 0
var body: some View {
VStack {}
.sheet(isPresented: .constant(true)) {
Button {
selectedDetent = selectedDetent == .large ? .medium : .large
} label: {
Text("\(Int(height))")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding()
.buttonStyle(.borderedProminent)
.modifier(GetHeightModifier(height: $height))
.presentationDetents(detents, selection: $selectedDetent)
.interactiveDismissDisabled()
}
}
}
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
}
)
}
}
The change can be made more animated by adding a .transaction
modifier to the Button
:
Button {
// ...
} label: {
// ...
}
.transaction { trans in
trans.disablesAnimations = false
trans.animation = .easeInOut(duration: 1)
}
// ... + other modifiers, as before
It doesn't give the most sophisticated animation though:
EDIT Regarding your comment:
that is not the expected result (the label is still jumpy), and it induces some regressions (when using the drag indicator)
A similar animation can be achieved by tracking the height of the sheet and then applying this to the content, with animation.
GeometryReader
to wrap the sheet content.GeometryReader
can then use the sheet size directly, without having to go via a state variable.To avoid what you described as the regression problem when re-sizing with the drag indicator, the change to the content height only needs to be animated when the change is applied programmatically (with a button press).
nil
, changes are not animated. This is the default state, so it applies for size changes using the drag indicator..onChange
handler is used to detect the change of sheet height and this is used to update the content height, with animation..onChange
updates are triggered by the native animation.struct TestView: View {
let detents: Set<PresentationDetent> = [.large, .medium]
@State var selectedDetent: PresentationDetent = .medium
@State var contentHeight: CGFloat?
var body: some View {
VStack {}
.sheet(isPresented: .constant(true)) {
GeometryReader { proxy in
let sheetHeight = proxy.size.height
Button {
contentHeight = sheetHeight
selectedDetent = selectedDetent == .large ? .medium : .large
Task { @MainActor in
try? await Task.sleep(for: .seconds(1))
contentHeight = nil
}
} label: {
Text("\(Int(sheetHeight))")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.buttonStyle(.borderedProminent)
.padding()
.frame(height: contentHeight)
.onChange(of: sheetHeight) { oldVal, newVal in
if contentHeight != nil {
withAnimation(.spring(duration: 0.5)) {
contentHeight = newVal
}
}
}
}
.presentationDetents(detents, selection: $selectedDetent)
.interactiveDismissDisabled()
}
}
}
With this approach, the animation is smoother and the label no longer jumps. However, the animation for a programmatic change still lags the change in sheet height: