Skip to content

Instantly share code, notes, and snippets.

@dankamel
Last active March 27, 2025 03:25
Show Gist options
  • Save dankamel/d7d66bea734ff1a2ea2a6f6abfd538ad to your computer and use it in GitHub Desktop.
Save dankamel/d7d66bea734ff1a2ea2a6f6abfd538ad to your computer and use it in GitHub Desktop.
Native iOS 15 Adjustable Bottom Sheet In SwiftUI (half bottom sheet)
import SwiftUI
struct BottomSheetDesign: View {
@State var showSheet: Bool? = nil
var body: some View {
Button(action: { showSheet = true }) {
HStack (spacing: 5) {
Image(systemName: "hand.tap.fill")
.font(.system(size: 20, weight: .regular, design: .rounded))
Text("me to bring up adjustable sheet")
.font(.system(size: 20, weight: .regular, design: .rounded))
}
}
.halfSheet(showSheet: $showSheet) {
ZStack {
Color.blue
ScrollView(.vertical, showsIndicators: false) {
VStack {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.frame(width: 50, height: 8)
.padding(.top, -20)
.foregroundColor(.black.opacity(0.2))
Text("Hello half sheet 🥹")
.font(.system(size: 25, weight: .regular, design: .rounded))
.foregroundColor(.white)
.padding(.top, 70)
.padding(.bottom, 10)
Button(action: { showSheet = false }) {
HStack(spacing: 5) {
Image(systemName: "hand.tap.fill")
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundColor(.white)
Text("me to close sheet")
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundColor(.white)
}
}
}
.padding(.top, 30)
}
}
.edgesIgnoringSafeArea(.bottom)
} onDismiss: {
print("sheet dismissed")
}
}
}
struct BottomSheetDesign_Previews: PreviewProvider {
static var previews: some View {
BottomSheetDesign()
}
}
import SwiftUI
// Custom Half Sheet Modifier....
extension View {
//binding show bariable...
func halfSheet<Content: View>(
showSheet: Binding<Bool?>,
@ViewBuilder content: @escaping () -> Content,
onDismiss: @escaping () -> Void
) -> some View {
return self
.background(
HalfSheetHelper(sheetView: content(), showSheet: showSheet, onDismiss: onDismiss)
)
}
}
// UIKit integration
struct HalfSheetHelper<Content: View>: UIViewControllerRepresentable {
var sheetView: Content
let controller: UIViewController = UIViewController()
@Binding var showSheet: Bool?
var onDismiss: () -> Void
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeUIViewController(context: Context) -> UIViewController {
controller.view.backgroundColor = .clear
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if let showSheet: Bool = showSheet {
if showSheet {
let sheetController = CustomHostingController(rootView: sheetView)
sheetController.presentationController?.delegate = context.coordinator
uiViewController.present(sheetController, animated: true)
}
}
}
//on dismiss...
final class Coordinator: NSObject, UISheetPresentationControllerDelegate {
var parent: HalfSheetHelper
init(parent: HalfSheetHelper) {
self.parent = parent
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
parent.showSheet = false
}
}
}
// Custom UIHostingController for halfSheet...
final class CustomHostingController<Content: View>: UIHostingController<Content> {
override func viewDidLoad() {
view.backgroundColor = .clear
if let presentationController = presentationController as? UISheetPresentationController {
presentationController.detents = [
.medium(),
.large()
]
//MARK: - sheet grabber visbility
presentationController.prefersGrabberVisible = false // i wanted to design my own grabber hehehe
// this allows you to scroll even during medium detent
presentationController.prefersScrollingExpandsWhenScrolledToEdge = false
//MARK: - sheet corner radius
presentationController.preferredCornerRadius = 30
// for more sheet customisation check out this great article https://sarunw.com/posts/bottom-sheet-in-ios-15-with-uisheetpresentationcontroller/#scrolling
}
}
}
public struct LazyView<Content: View>: View {
private let build: () -> Content
public init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
public var body: Content {
build()
}
}
@kirillpukhov2004
Copy link

Excellent, but it doesn't work with EnvironmentObjects.

@halpz
Copy link

halpz commented May 16, 2024

when I dismiss the sheet using @Environment(\.dismiss) var dismiss from the sheet content, it presents the sheet again immediately .

is there a way to fix this?

@bernardonigbinde
Copy link

bernardonigbinde commented Jun 15, 2024

Nice one @dankamel!

I got the onDismiss to work by doing this:

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
	if let showSheet: Bool = showSheet {
		if showSheet {
			let sheetController = CustomHostingController(rootView: sheetView)
			sheetController.presentationController?.delegate = context.coordinator
			uiViewController.present(sheetController, animated: true)
		} else {
			uiViewController.dismiss(animated: true)
			onDismiss()
			self.showSheet = nil
		}
	}
}

Thanks to hints from @Frodothedwarf & @bruijnesteijn

@nedimf
Copy link

nedimf commented Sep 13, 2024

Based on issues that I had, simply closing uiViewController, dismissed whole screen because nature of my screen was pretty complex and nested, what worked for me was, dismissing presented controller

    if showSheet {
                uiViewController.present(sheetController, animated: true)
            } else {
                uiViewController.presentedViewController?.dismiss(animated: true)
                onDismiss()
                DispatchQueue.main.async { //added because of issue on changing showSheet on none supported thread
                    self.showSheet = nil
                }
            }

@G97Prasanna
Copy link

But its not properly working in ipad.It does not completely occupy the whole width.So If you have any solution for this,kindly let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment