SwiftUI Bottom Sheet: How to Hide Unwanted UI Components

SwiftUI Bottom Sheet: How to Hide Unwanted UI Components

Β·

4 min read

This blog post will explain alternatives for programmatically determining a presented sheet's detent in SwiftUI.

To present a bottom sheet on iOS is very easy with SwiftUI thanks to the sheet and presentationDetents modifiers.

To turn a normal sheet into a bottom sheet, we only need to define supported detents with .presentationDetents([.medium, .large]).

At work I was asked if it is possible to programmatically determine the presented sheet's detent and react to its changes. A possible use case might be to show UI controls (i.e., a large icon) on a large sheet but hide the controls on a bottom sheet.

Curt Clifton pointed out to me that SwiftUI offers a version of the modifier that takes a binding to the currently selected detent: presentationDetents(_:selection:)

That should be the preferred solution for most developers!

But what if the View in the bottom sheet is provided by a 3rd party SDK? Should the SDK offer to inject such binding? What if the app developer doesn't provide the binding? I was looking for alternatives that could work without any prerequisites for the consumer of the View.

The verticalSizeClass is unaffected by detent changes, so that's not an option πŸ™

I asked ChatGTP for a workaround, and here is the answer from OpenAI.

  • SheetDetentObserver: This class conforms to ObservableObject and publishes changes to the current detent.

  • DetentSheetView: A custom UIViewControllerRepresentable that presents a sheet and uses UISheetPresentationController to manage detents.

  • Coordinator: Acts as the delegate for UISheetPresentationControllerDelegate, observing changes in the detent and updating the SheetDetentObserver.

πŸ€” Hmm, oookkkkk ... maybe it's possible, but the solution seems to have a lot of overhead. I was looking for a simpler solution that does not require using UIKit.

Do I need the detent, or would it be okay to know the sheet size? Then I could hide the unwanted UI controls if a certain threshold is exceeded for the height.

Let's try it! GeometryReader to the rescue! Using GeometryReader for this particular use case seems perfectly fine but be aware that this container view comes with some challenges.

We can get the container size and use a threshold (e.g. 500) to determine when to show certain controls.

struct SheetContentView: View {
    var body: some View {
        GeometryReader { proxy in
                ScrollView {
                    SomeHeaderView(showImage: proxy.size.height > 500 ? true : false)
                    SomeSheetContentView()
                }
            }
    }
}

We could also create a custom SwiftUI environment variable to make the value accessible for all subviews.

struct SheetContentViewSizeKey: EnvironmentKey {
    static var defaultValue: CGSize = .zero
}

extension EnvironmentValues {
    var mySheetContentViewSize: CGSize {
        get { self[SheetContentViewSizeKey.self] }
        set { self[SheetContentViewSizeKey.self] = newValue }
    }
}

Then the mySheetcontentViewSize can be accessed by any view in the view hierarchy.

struct SomeSheetContentView: View {
    // built-in SwiftUI environment variable
    @Environment(\.isPresented) var isPresented
    // custom
    @Environment(\.mySheetContentViewSize) var mySheetContentViewSize

    var body: some View {
        VStack {
            Text("Height: \(mySheetContentViewSize.height)")
            Text("Is Presented: \(isPresented)")
        }
    }
}

Complete code from my example:

struct ContentView: View {
    @State var isSheetShown = false

    var body: some View {
        VStack {
            Button("Open Sheet") { isSheetShown.toggle() }
            .sheet(isPresented: $isSheetShown) {
                SheetContentView()
                    .presentationDetents([.medium, .large])
            }
        }
        .padding()
    }
}

struct SheetContentView: View {    
    var body: some View {
        GeometryReader { proxy in
                ScrollView {
                    SomeHeaderView(showImage: proxy.size.height > 500 ? true : false)
                    SomeSheetContentView()
                }
                .environment(\.mySheetContentViewSize, proxy.size)
            }
    }
}


struct SomeHeaderView: View {
    var showImage: Bool
    var body: some View {
        HStack {
            Text("My Sheet Header")
            Spacer()
            if showImage {
                Image(systemName: "moon.stars.fill")
                    .frame(width: 75, height: 75)
            }
        }
        .background(.red)
    }
}

struct SomeSheetContentView: View {
    // built-in SwiftUI environment variable
    @Environment(\.isPresented) var isPresented
    // custom
    @Environment(\.mySheetContentViewSize) var mySheetContentViewSize

    var body: some View {
        VStack {
            Text("Height: \(mySheetContentViewSize.height)")
            Text("Is Presented: \(isPresented)")
        }
    }
}

struct SheetContentViewSizeKey: EnvironmentKey {
    static var defaultValue: CGSize = .zero
}

extension EnvironmentValues {
    var mySheetContentViewSize: CGSize {
        get { self[SheetContentViewSizeKey.self] }
        set { self[SheetContentViewSizeKey.self] = newValue }
    }
}

This blog post might not be helpful for everyone, but I thought it's a good idea to document this option. Not only for me but also for my co-workers πŸ˜€

Did you find this article valuable?

Support Swifty Tech by Marco Eidinger by becoming a sponsor. Any amount is appreciated!