A customizable slide-up bottom sheet presentation controller for UIKit. Works pre-iOS 15, supporting arbitrary height detents, a draggable grabber, optional dimming scrim, keyboard avoidance, rotation, and full layout-metric overrides.
- iOS 13.0+
- Swift 6.0+
Swift Package Manager:
.package(url: "https://github.com/jonikay89/BottomShelfer-ios.git", from: "1.0.0")import UIKit
import BottomShelfer
final class MySheetViewController: UIViewController, BottomShelferPresentable {
let bottomShelferPresentationManager = BottomShelferPresentationManager()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
}
// Present it
let sheet = MySheetViewController()
sheet.bottomShelferPresentationManager.detents = [.medium(), .large()]
sheet.bottomShelferPresentationManager.selectedDetentIndex = 0
sheet.presentAsBottomShelfer(from: self, animated: true)Predefined or custom-height snap points. The sheet always settles on one.
manager.detents = [.small(), .medium(), .large()] // screen‑ratio
manager.detents = [.custom(height: 320)] // exact points
manager.detents = BottomShelferDetent.detents(forContentHeight: 420) // auto‑sized
manager.selectedDetentIndex = 1 // start on mediumVisual drag affordance at the top of the sheet. Size, offset, and corner radius
are all configurable via BottomShelferLayoutConfiguration.
var layout = BottomShelferLayoutConfiguration()
layout.grabberPillSize = CGSize(width: 56, height: 6)
layout.grabberPillCornerRadius = 3
manager.layoutConfiguration = layoutThe pill animates on drag — scales up slightly and fades — then returns to
identity when released. Disable the pill entirely by setting its size to .zero
while keeping the drag gesture active.
Optional semi-transparent backdrop behind the sheet. Tap to dismiss (behavior
controlled by dismissOnHide).
manager.isDimmingViewEnabled = false // no scrim
manager.dimmingColor = .black.withAlphaComponent(0.4) // custom colorThe sheet coordinates with embedded UIScrollViews — when the scroll view
is pinned to the top, a downward drag transfers control from the scroll view
to the sheet for dismiss / shrink.
manager.allowGrabbingNonScrollViews = trueSet isDraggingEnabled = false to disable dragging entirely (button‑dismiss
only).
The sheet lifts out of the way when the keyboard appears.
var cancellables = Set<AnyCancellable>()
startObservingKeyboardForBottomShelfer(cancellables: &cancellables)Closures on BottomShelferPresentationManager fire during key lifecycle moments.
manager.onDismiss = { print("sheet dismissed") }
manager.onGrabberDragBegan = { print("grabber drag started") }
manager.onGrabberDragEnded = { print("grabber drag ended") }
manager.onContentDragBegan = { print("content drag started") }
manager.onContentDragEnded = { print("content drag ended") }
manager.onDetentChanged = { index, height in
print("snapped to detent \(index) at \(height)pt")
}Drive the sheet to any detent from code.
(presentationController as? BottomShelferPresentationController)?
.snapToHeight(320)The sheet re‑derives its detent when the container size changes (device rotation or iPad multitasking). Frame clamping prevents off‑screen positions.
Override the default metrics through BottomShelferLayoutConfiguration.
| Property | Default | Description |
|---|---|---|
maxSheetWidth |
430 | Clamps sheet width on iPad |
maxHeightFraction |
0.9 | Caps sheet height as fraction of container |
grabberHitAreaHeight |
44 | Height of the draggable band |
grabberPillSize |
36 × 5 | Pill dimensions |
grabberPillBottomOffset |
12 | Distance from sheet edge |
grabberPillCornerRadius |
2.5 | Pill corner radius |
Sheet corner radius and dimming color are set directly on the manager.
manager.cornerRadius = 24
manager.dimmingColor = .systemIndigo.withAlphaComponent(0.2)Embed SwiftUI content inside a bottom sheet via UIHostingController.
let hosting = UIHostingController(rootView: MySwiftUIView())
addChild(hosting)
view.addSubview(hosting.view)
hosting.didMove(toParent: self)MIT — see LICENSE.
jonikay89 — @jonikay89
