I'm encountering the following issue:
When a sheet with multiple detents is displayed:
- resizing the sheet from the drag indicator will change the displayed view height accordingly and progressively
- resizing the sheet by changing the detent programmatically will change the displayed view to it's final height instantly
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
}
)
}
}
Dragging the sheet | Changing detent programmatically |
---|---|
I'm encountering the following issue:
When a sheet with multiple detents is displayed:
- resizing the sheet from the drag indicator will change the displayed view height accordingly and progressively
- resizing the sheet by changing the detent programmatically will change the displayed view to it's final height instantly
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
}
)
}
}
Dragging the sheet | Changing detent programmatically |
---|---|
1 Answer
Reset to default 2The 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.
- An easy way to measure the size of the sheet is to use a
GeometryReader
to wrap the sheet content. - The content inside the
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).
- An optional can be used for the content height.
- When set to
nil
, changes are not animated. This is the default state, so it applies for size changes using the drag indicator. - When the button is pressed, the content height is set to the current sheet height, in preparation for an animated change.
- An
.onChange
handler is used to detect the change of sheet height and this is used to update the content height, with animation. - The optional is reset to nil once the animations have completed. This is done after a short delay, because multiple
.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: