Skip to content

Instantly share code, notes, and snippets.

@Amzd
Created October 17, 2019 10:37
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Amzd/9ec1ee39dca840420ce35505ef360632 to your computer and use it in GitHub Desktop.
Save Amzd/9ec1ee39dca840420ce35505ef360632 to your computer and use it in GitHub Desktop.
SwiftUI Sheet with height value
//
// SheetHeight.swift
// Created by Casper Zandbergen on 17/10/2019.
//
import SwiftUI
import UIKit
extension View {
/// Presents a sheet.
///
/// - Parameters:
/// - height: The height of the presented sheet.
/// - item: A `Binding` to an optional source of truth for the sheet.
/// When representing a non-nil item, the system uses `content` to
/// create a sheet representation of the item.
///
/// If the identity changes, the system will dismiss a
/// currently-presented sheet and replace it by a new sheet.
///
/// - onDismiss: A closure executed when the sheet dismisses.
/// - content: A closure returning the content of the sheet.
public func sheet<Item: Identifiable, Content: View>(height: SheetHeight, item: Binding<Item?>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping (Item) -> Content) -> some View {
self.sheet(item: item, onDismiss: onDismiss) { item in
SheetView(height: height, content: { content(item) })
}
}
/// Presents a sheet.
///
/// - Parameters:
/// - height: The height of the presented sheet.
/// - isPresented: A `Binding` to whether the sheet is presented.
/// - onDismiss: A closure executed when the sheet dismisses.
/// - content: A closure returning the content of the sheet.
public func sheet<Content: View>(height: SheetHeight, isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View {
self.sheet(isPresented: isPresented, onDismiss: onDismiss) {
SheetView(height: height, content: content)
}
}
}
public enum SheetHeight {
case points(CGFloat)
case percentage(CGFloat)
/// When the provided content's height can be infered it will show up as that. ScrollView and List don't have this by default so they will show as 50%. You can use .frame(height:) to change that.
case infered
fileprivate func emptySpaceHeight(in size: CGSize) -> CGFloat? {
switch self {
case .points(let height):
let remaining = size.height - height
return max(remaining, 0)
case .percentage(let percentage):
precondition(0...100 ~= percentage)
let remaining = 100 - percentage
return size.height / 100 * remaining
case .infered:
return nil
}
}
}
private struct SheetView<Content: View>: View {
var height: SheetHeight
var content: () -> Content
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
ParentInvisible {
GeometryReader { geometry in
VStack {
Spacer(minLength: self.height.emptySpaceHeight(in: geometry.size)).onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}
ZStack(alignment: .bottom) {
SafeAreaFillView(geometry: geometry)
self.content().clipShape(SheetShape(geometry: geometry))
}
}
}
}.edgesIgnoringSafeArea(.all)
}
}
private struct SafeAreaFillView: View {
var geometry: GeometryProxy
var body: some View {
Color(.systemBackground)
.frame(width: geometry.size.width, height: geometry.safeAreaInsets.bottom)
.offset(y: geometry.safeAreaInsets.bottom)
}
}
private struct ParentInvisible<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
func makeUIViewController(context: Context) -> UIHostingController<Content> {
let host = UIHostingController(rootView: content())
host.view.backgroundColor = .clear
return host
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
uiViewController.parent?.view.backgroundColor = .clear
}
}
private struct SheetShape: Shape {
var geometry: GeometryProxy
let radius = 8
func path(in rect: CGRect) -> Path {
var rect = rect
rect.size.height += geometry.safeAreaInsets.bottom
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
extension Spacer {
/// https://stackoverflow.com/a/57416760/3393964
public func onTapGesture(count: Int = 1, perform action: @escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
@Amzd
Copy link
Author

Amzd commented Oct 17, 2019

Issue: When swiping to close you can see a shadow being cut off (clearly visible in light mode, only barely in dark mode)

@easiwriter
Copy link

easiwriter commented Mar 26, 2020

Fantastic. Just what I was looking for! It works fine on the iPhone, but on iPad the Spacer is filled with an opaque colour of some sort and I haven't been able to find a way of getting rid of it. Any idea?

Here's a screen shot.
Screenshot 2020-03-26 at 12 41 27

@muhammadabbas001
Copy link

muhammadabbas001 commented Dec 15, 2020

Sir How to use it in SwiftUI I used like this

struct ContentView: View {
    @State var isPresented = false
    var body: some View {
        Button(action: {
            self.isPresented = true
        }, label: {
            Text("OpenSheet")
        }).sheet(height: .infered, isPresented: $isPresented){
            Text("That Sheet with Height.")
        }
    }
}

But the sheet showed same size not half size

@Amzd
Copy link
Author

Amzd commented Dec 15, 2020

This was just a toy project I did one afternoon when I was bored, it was not maintained or even bug free at any point. I’m sorry. I recommend Rideau instead.

https://github.com/muukii/Rideau

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