iosswiftuiswiftui-sheetdetent

Animating content height when changing SwiftUI sheet detent programmatically


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
            }
        )
    }
}
Dragging the sheet Changing detent programmatically
enter image description here enter image description here

Solution

  • 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:

    Animation


    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.

    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).

    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:

    Animation