skip to Main Content

I’m working on a SwiftUI sheet that can resize itself based on the content’s height. The sheet works well until I introduce multi-line text.

To handle the dynamic height, I added .fixedSize(horizontal: false, vertical: true) to the SheetHeightModifier, which fixes the height issue. However, when I do this, I notice an odd animation behavior with the button. It jumps up when you tap it. Here’s my code.

Why would the button exhibit this behavior with the fixedSize modifier?

struct SheetHeightModifier: ViewModifier {
    @Binding var height: CGFloat

    func body(content: Content) -> some View {
        content
            .fixedSize(horizontal: false, vertical: true)
            .background(
            GeometryReader { reader -> Color in
                height = reader.size.height
                print(height)
                return Color.clear
            }
        )
    }
}

struct PresentationDetentModifier: ViewModifier {
    @Binding var height: CGFloat
    
    func body(content: Content) -> some View {
        content
            .modifier(SheetHeightModifier(height: $height))
            .presentationDetents([.height(height)])
    }
}

extension View {
    func flexiblePresentationDetents(height: Binding<CGFloat>) -> some View {
        self.modifier(PresentationDetentModifier(height: height))
    }
}

struct ContentView: View {
    @State var showSheet = false
    @State var sheetHeight: CGFloat = 0
    
    var body: some View {
        VStack {
            Button("Present Sheet") {
                showSheet = true
            }
        }
        .sheet(isPresented: $showSheet) {
            VStack {
                Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
            }
            .padding()
            .flexiblePresentationDetents(height: $sheetHeight)
        }
        .padding()
    }
}

2

Answers


  1. Looking at the logs, the height read by the geometry reader is initially more than 8000pt for a split second, then changes to about 200, which is correct.

    Because of this large height, the view presenting the sheet shrinks a bit (similar to when you use the .large detent), then very quickly after that the height goes back down and so the view moves back to the initial position. This causes the views to jiggle a bit because they are being laid out again.

    A simple way to work around this is to just filter out these nonsensical heights from the geometry reader. If you are sure that the content size will never exceed some value, then you can ignore heights above that value. For example, here I have used 2000

    // I also changed this to use a preference value instead of
    // directly assigning to 'height' in the GeometryReader
    struct HeightKey: PreferenceKey {
        static var defaultValue: CGFloat? { nil }
        
        static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
            if let next = nextValue() {
                value = next
            }
        }
    }
    
    struct SheetHeightModifier: ViewModifier {
        @Binding var height: CGFloat
    
        func body(content: Content) -> some View {
            content
                .fixedSize(horizontal: false, vertical: true)
                .background {
                    GeometryReader { reader in
                        Color.clear
                            .preference(key: HeightKey.self, value: reader.size.height)
                    }
                }
                .onPreferenceChange(HeightKey.self) {
                    // the max height is 2000 - assume everything above that is nonsensical
                    guard let newHeight = $0, newHeight < 2000 else {
                        return
                    }
                    height = newHeight
                }
        }
    }
    

    Or you can use min to limit the height to a value that won’t cause the presenting view to move:

    .onPreferenceChange(HeightKey.self) {
        guard let newHeight = $0 else {
            return
        }
        height = min(newHeight, UIScreen.main.bounds.height * 0.88)
    }
    

    Unfortunately, unlike in this post, you cannot use a .custom detent, because they cannot take any parameters. While they do have access to EnvironmentValues, they are of the environment of the presenting view, not the sheet.

    Login or Signup to reply.
  2. I suspect that the reason why the height of the Text starts off so large is because the width is very small. This may be because it is performing some kind of zoom animation as the sheet appears.

    It helps if you apply a min width to the content inside SheetHeightModifier. The minimum width is not necessarily the width of the screen, because a sheet on a large iPad does not occupy the full screen width. But it works quite well to use the width of an iPod (= 320) as min width:

    // SheetHeightModifier
    
    content
        .frame(minWidth: 320) // 👈 HERE
        // other modifiers as before
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search