iOS 16.4, SwiftUI bottom sheet is sometimes ignoring presentationDetents – Ios

by
Liam Thompson
axios bottom-sheet swiftui-ontapgesture

Quick Fix: Utilize ‘sheet(isPresented:)’ instead of ‘sheet(item:)’. Define a function ‘isNotNil’ to bind the selection to a Boolean value. Set the sheet state to an intermediate ‘nil’ value when the selection changes so the sheet properly disappears and reappears.

The Problem:

In iOS 16.4’s SwiftUI, the ".sheet" modifier’s ".presentationDetents" and ".presentationBackgroundInteraction" are occasionally ignored. This behavior is inconsistent–sometimes presenting as a full-height modal with no background interaction when changing selections. The bug doesn’t occur when hiding the sheet before selecting another choice, and it’s not limited to a specific view, so it’s not a PickerView issue. The root cause is assumed to be the sheet’s selection not passing through the nil state. The workaround is to hide the sheet, change your selection, then show the sheet again.

The Solutions:

Solution 1: Use ‘sheet(isPresented:)’ if ‘sheet(item:)’ causes a bug

  • Instead of using .sheet(item:), consider using .sheet(isPresented:).
  • This way, the sheet will remain visible as long as the isPresented binding is true, regardless of the selected item.
  • To achieve this, you can create an additional @State variable, sheetItem, to track the currently selected item and bind sheetItem to sheet(isPresented:).
  • When the selection changes, assign nil to sheetItem and then, using DispatchQueue.main.async, assign the new selection to sheetItem.
  • This will cause the sheet to disappear briefly before reappearing with the new selection.

Code Example:

struct ContentView: View {
    @State var selection: String?
    @State var sheetItem: String?

    var body: some View {
        VStack {
            Picker("Choose", selection: $selection) {
                ForEach(choices, id: \.self) { choice in
                    Text(choice ?? "None")
                        .tag(choice)
                }
            }
            .padding()

            Spacer()
        }
        .pickerStyle(.segmented)
        .sheet(item: $sheetItem) { choice in
            Text("Item selected: \(choice)")
                .presentationDetents([.medium])
                .presentationBackgroundInteraction(.enabled(upThrough: .medium))
        }
    }

    func isNotNil<T>(_ binding: Binding<T?>) -> Binding<Bool> {
        Binding {
            binding.wrappedValue != nil
        } set: { notNil in
            if !notNil {
                binding.wrappedValue = nil
            }
        }
    }

    private let choices = [nil, "One", "Two", "Three"]
}

In this example:

  • The sheet will no longer disappear and reappear when the selection changes.
  • The sheet will remain visible as long as a selection is made.
  • When the selection changes, the sheet will update to display the newly selected item.

Solution 2: Use `.id()` to Specify a Different Sheet

To work around the bug where the bottom sheet sometimes ignores the `.presentationDetents` and `.presentationBackground Interaction`, add the `.id()` modifier to the sheet. This lets the system know that the sheet you are presening is different from the previous one, so it can assign the `.presentationDetents` property again.

.sheet(item: $selection) {choice in 
  Text("Item selected: \(choice)")
      .id(choice)  // Add the .id() modifier
      .presentationDetents([.medium])
      .presentationBackground Interaction(
        .enabled(upThrough: .medium)
      )
}

With this added `.id()` modifier, the system will recognize the sheet as a new instance and correctly assign the `.presentationDetents` property, ensuring that the sheet is presented with the desired height and background interaction.

Q&A

From iOS 16.4 in SwiftUI we can use the modifier with to specify the height of a bottom sheet. But sometimes it ignores and just shows as a full height modal without any background interaction. How to fix it?

Use sheet(isPresented:) instead of sheet(item:). Show the sheet as long as the selection is not nil.

What is suggested to be used instead of the sheet(item:) modifier?

To avoid disappearing and reappearing of the sheet use sheet(isPresented:).

In the code, what modifier should be added to the sheet to fix the issue?

Add the .id() modifier to let SwiftUI know the sheet you are presenting is different from the previous one.