Exploring SwiftUI: Detecting and Controlling Bottom Sheet Position
I recently wrote about bottom sheets in SwiftUI while working on my Meshing app. I realized I had been experimenting with hardcoded fractions to control when to show "detailed" controls. It felt clunky and inflexible. Also, I needed more control over the bottom sheet, especially the presentation detents detection.
0:00
/0:16
1×
I randomly stumbled upon another modifier for it: presentationDetents(**_** detents: Set<PresentationDetent>, selection: Binding<PresentationDetent>) and excited to share what I have learned. Let's explore how to detect and control bottom sheet positions in SwiftUI!
Creating an Enum for Sheet Positions
I like enumerations, so I created a custom enum for sheet positions. This approach keeps the code clean and makes it easy to adjust fraction values in one place:
enum SheetPosition: CGFloat, CaseIterable {
case peek = 0.1
case detailed = 0.5
var detent: PresentationDetent {
.fraction(rawValue)
}
static let detents = Set(SheetPosition.allCases.map { $0.detent })
}In this enum, peek represents the minimized sheet (10% of the screen height), and detailed is the expanded view (50% of the screen height). The detent computed property converts the cases to PresentationDetent values.
Implementing the Selection Modifier
With the enum in place, I used the newly discovered presentationDetents(_:selection:) modifier to control the sheet position. This modifier gave me programmatic control over the currently selected detent:
@State private var selectedDetent: PresentationDetent = SheetPosition.peek.detent
.sheet(isPresented: .constant(true)) {
ControlPanel(viewModel: viewModel, showDetailedControls: $showDetailedControls)
.presentationDetents(SheetPosition.detents,
selection: $selectedDetent
)
.presentationDragIndicator(.visible)
}I have the static allDetents set and binding the selection to a selectedDetent state variable.
Listening for Position Changes
To respond to changes in the sheet position, I use the onChange modifier to update the UI based on the selected detent. I ran into a tricky problem where the selectedDetent binding updated and refreshed the ControlPanel view, it interfered with the actual detent change. This led to a situation where showDetailedControls would update, but the sheet position would not, resulting in a mismatched view state. To fix this, I implemented a small delay:
.onChange(of: selectedDetent) {
Task {
// Add a small delay to ensure the detent change completes
try? await Task.sleep(for: .milliseconds(50))
// Update UI on the main thread
await MainActor.run {
switch selectedDetent {
case SheetPosition.peek.detent:
showDetailedControls = false
case SheetPosition.detailed.detent:
showDetailedControls = true
default: break
}
}
}
}This delay gives the sheet time to complete its position change before updating showDetailedControls. By running this in a separate task and updating the UI on the main thread, I ensure smooth transitions without blocking other UI updates.
Adding Smooth Animations
To make the UI changes feel smooth and polished, let's add some animations. I directly apply the animation modifier to the view:
.animation(.easeInOut, value: showDetailedControls)Updating the Control Panel For Disclosure Groups
In the ControlPanel view, I added a constant for showDetailedControls:
struct ControlPanel: View {
@ObservedObject var viewModel: MeshViewModel
let showDetailedControls: Bool
var body: some View {
// Control panel content
}
}This allows the ControlPanel to update itself to changes in the sheet position to show the detailed controls. Also, I encountered a small bug where I wanted the disclosure group to close when the sheet was in its peek state:
.onChange(of: showDetailedControls) { oldValue, newValue in
if !newValue {
isTemplatesExpanded = false
}
}This code closes the templates disclosure group whenever the detailed controls are hidden!
Finally, here is how it looks:
0:00
/0:15
1×
Moving Forward
The discloure group closing is still a bit glitchy but I have invested (or wasted) enough time that it is time to move forward with what I have.
I hope you find it as useful as I have! Feel free to adapt them to your own projects and let me know how it goes. Happy coding!