swiftxcodeswiftui

Dynamic Presentation Sheet Heights in SwiftUI


I have NavigationStack presented as a sheet and I intend to dynamically adjust its height while pushing views within it. I'm utilizing a global observable variable to manage the height and everything works fine except that the height changes abruptly without any animation. It abruptly transitions from one height to another. The issue can be reproduced using the following code:

#Preview {
  @Previewable @State var height: CGFloat = 200
  
  Text("Parent View")
    .sheet(isPresented: .constant(true)) {
      NavigationStack {
        Form {
          NavigationLink("Button") {
            RoundedRectangle(cornerRadius: 20)
              .fill(Color.blue)
              .frame(height: 200)
              .navigationTitle("Child")
              .onAppear {
                withAnimation {
                  height = 300
                }
              }
          }
        }
        .navigationTitle("Parent")
        .navigationBarTitleDisplayMode(.inline)
        .presentationDetents([.height(height)])
        .onAppear {
          withAnimation {
            height = 150
          }
        }
      }
    }
}

Solution

  • The height of the sheet changes smoothly if you use the modifier presentationDetents(_:selection:) to set the selected detent. The Set should include all the possible sizes that you wish to use. With this approach, it is not even necessary to use withAnimation when updating the selected detent.

    Getting the child content to adjust height in an animated way is a bit more difficult. One technique is as follows:

    Here is the updated example with all changes applied:

    struct ContentView: View {
        let detents: Set<PresentationDetent> = [.height(150), .height(300)]
        @State private var selectedDetent: PresentationDetent = .height(150)
        @State private var actualHeight: CGFloat?
        
        var body: some View {
            Text("Parent View")
                .sheet(isPresented: .constant(true)) {
                    NavigationStack {
                        Form {
                            NavigationLink("Button") {
                                GeometryReader { proxy in
                                    RoundedRectangle(cornerRadius: 20)
                                        .fill(.blue)
                                        .navigationTitle("Child")
                                        .frame(maxHeight: actualHeight)
                                        .animation(.default, value: actualHeight)
                                        .onChange(of: proxy.size.height) { _, newVal in
                                            actualHeight = newVal
                                        }
                                }
                                .onAppear {
                                    selectedDetent = .height(300)
                                }
                            }
                        }
                        .navigationTitle("Parent")
                        .navigationBarTitleDisplayMode(.inline)
                        .presentationDetents(detents, selection: $selectedDetent)
                        .onAppear {
                            selectedDetent = .height(150)
                        }
                    }
                }
        }
    }
    

    Animation