From iOS 16.4 in SwiftUI we can use the .sheet
modifier with .presentationDetents
to specify the height of a bottom sheet. We can further use .presentationBackgroundInteraction
to allow interaction with the view behind the sheet.
I'm encountering a problem that looks to me like a SwiftUI bug.
You can paste this code into a playground to see the buggy behaviour in action (this is a simplified version of my actual app that has the issue).
The bug is that sometimes when changing from one selection to another, the sheet completely ignores the .presentationDetents
and .presentationBackgroundInteraction
modifiers. It just shows as a full height modal without any background interaction.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
let choices = [nil, "One", "Two", "Three"]
@State var selection: 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: $selection) { choice in
Text("Item selected: \(choice)")
.presentationDetents([.medium])
.presentationBackgroundInteraction(
.enabled(upThrough: .medium)
)
}
}
}
extension String: Identifiable {
public var id: String { self }
}
PlaygroundPage.current.setLiveView(ContentView().frame(width: 320, height: 568))
Here are some screenshots:
Observations
PickerView
, so it's not to do with that.presentationBackgroundInteraction
because it has gone way past .medium
and up to full height.So it seems to be an issue with a sheet's selection not passing through the nil state before changing to another selection, which to me is a perfectly valid thing to want to do (I don't want the sheet disappearing then re-appearing).
Assuming this is an actual bug, any ideas how can I work around it? I don't really want to write or import a third party library for something that should be working natively.
Thanks in advance for any insights!
I don't think you can use sheet(item:)
if you don't want the sheet disappearing and reappearing. Even if the presentationDetents
bug is fixed, I think the intended behaviour of sheet(item:)
is still going to be "dismiss the sheet then present another one" when the item changes.
Use sheet(isPresented:)
instead. Show the sheet as long as the selection is not nil.
.sheet(isPresented: isNotNil($selection)) {
if let selection {
Text("Item selected: \(selection)")
.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
}
}
}
If you don't mind the sheet disappearing and appearing again, you can add a separate @State
for presenting sheet. Set that state to an intermediary nil
value when selection
changes.
@State var sheetItem: String?
.onChange(of: selection) { x in
sheetItem = nil
DispatchQueue.main.async {
sheetItem = x
}
}
.sheet(item: $selection) { choice in ... }