最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - Animating content height when changing SwiftUI sheet detent programmatically - Stack Overflow

programmeradmin3浏览0评论

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
Share Improve this question edited 2 days ago Benzy Neez 21.2k3 gold badges14 silver badges36 bronze badges asked 2 days ago BenoîtBenoît 3031 silver badge10 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 2

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.

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

发布评论

评论列表(0)

  1. 暂无评论