SwiftUI - Half modal?
I've written a Swift Package that includes a custom modifier that allows you to use the half modal sheet.
Here is the link: https://github.com/AndreaMiotto/PartialSheet
Feel free to use it or to contribute
You can make your own and place it inside of a zstack:https://www.mozzafiller.com/posts/swiftui-slide-over-card-like-maps-stocks
struct SlideOverCard<Content: View> : View { @GestureState private var dragState = DragState.inactive @State var position = CardPosition.top var content: () -> Content var body: some View { let drag = DragGesture() .updating($dragState) { drag, state, transaction in state = .dragging(translation: drag.translation) } .onEnded(onDragEnded) return Group { Handle() self.content() } .frame(height: UIScreen.main.bounds.height) .background(Color.white) .cornerRadius(10.0) .shadow(color: Color(.sRGBLinear, white: 0, opacity: 0.13), radius: 10.0) .offset(y: self.position.rawValue + self.dragState.translation.height) .animation(self.dragState.isDragging ? nil : .spring(stiffness: 300.0, damping: 30.0, initialVelocity: 10.0)) .gesture(drag) } private func onDragEnded(drag: DragGesture.Value) { let verticalDirection = drag.predictedEndLocation.y - drag.location.y let cardTopEdgeLocation = self.position.rawValue + drag.translation.height let positionAbove: CardPosition let positionBelow: CardPosition let closestPosition: CardPosition if cardTopEdgeLocation <= CardPosition.middle.rawValue { positionAbove = .top positionBelow = .middle } else { positionAbove = .middle positionBelow = .bottom } if (cardTopEdgeLocation - positionAbove.rawValue) < (positionBelow.rawValue - cardTopEdgeLocation) { closestPosition = positionAbove } else { closestPosition = positionBelow } if verticalDirection > 0 { self.position = positionBelow } else if verticalDirection < 0 { self.position = positionAbove } else { self.position = closestPosition } }}enum CardPosition: CGFloat { case top = 100 case middle = 500 case bottom = 850}enum DragState { case inactive case dragging(translation: CGSize) var translation: CGSize { switch self { case .inactive: return .zero case .dragging(let translation): return translation } } var isDragging: Bool { switch self { case .inactive: return false case .dragging: return true } }}
In Swift 5.5 iOS 15+ and Mac Catalyst 15+ there is a
There is a new solution with adaptiveSheetPresentationController
@available(iOS 15.0, *)struct CustomSheetParentView: View { @State private var isPresented = false var body: some View { VStack{ Button("present sheet", action: { isPresented.toggle() }).adaptiveSheet(isPresented: $isPresented, detents: [.medium()], smallestUndimmedDetentIdentifier: .large){ Rectangle() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .foregroundColor(.clear) .border(Color.blue, width: 3) .overlay(Text("Hello, World!").frame(maxWidth: .infinity, maxHeight: .infinity) .onTapGesture { isPresented.toggle() } ) } } }}@available(iOS 15.0, *)struct AdaptiveSheet<T: View>: ViewModifier { let sheetContent: T @Binding var isPresented: Bool let detents : [UISheetPresentationController.Detent] let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? let prefersScrollingExpandsWhenScrolledToEdge: Bool let prefersEdgeAttachedInCompactHeight: Bool init(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> T) { self.sheetContent = content() self.detents = detents self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge self._isPresented = isPresented } func body(content: Content) -> some View { ZStack{ content CustomSheet_UI(isPresented: $isPresented, detents: detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: {sheetContent}).frame(width: 0, height: 0) } }}@available(iOS 15.0, *)extension View { func adaptiveSheet<T: View>(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> T)-> some View { modifier(AdaptiveSheet(isPresented: isPresented, detents : detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: content)) }}@available(iOS 15.0, *)struct CustomSheet_UI<Content: View>: UIViewControllerRepresentable { let content: Content @Binding var isPresented: Bool let detents : [UISheetPresentationController.Detent] let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? let prefersScrollingExpandsWhenScrolledToEdge: Bool let prefersEdgeAttachedInCompactHeight: Bool init(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> Content) { self.content = content() self.detents = detents self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge self._isPresented = isPresented } func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> CustomSheetViewController<Content> { let vc = CustomSheetViewController(coordinator: context.coordinator, detents : detents, smallestUndimmedDetentIdentifier: smallestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, content: {content}) return vc } func updateUIViewController(_ uiViewController: CustomSheetViewController<Content>, context: Context) { if isPresented{ uiViewController.presentModalView() }else{ uiViewController.dismissModalView() } } class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate { var parent: CustomSheet_UI init(_ parent: CustomSheet_UI) { self.parent = parent } //Adjust the variable when the user dismisses with a swipe func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if parent.isPresented{ parent.isPresented = false } } }}@available(iOS 15.0, *)class CustomSheetViewController<Content: View>: UIViewController { let content: Content let coordinator: CustomSheet_UI<Content>.Coordinator let detents : [UISheetPresentationController.Detent] let smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? let prefersScrollingExpandsWhenScrolledToEdge: Bool let prefersEdgeAttachedInCompactHeight: Bool private var isLandscape: Bool = UIDevice.current.orientation.isLandscape init(coordinator: CustomSheet_UI<Content>.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], smallestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> Content) { self.content = content() self.coordinator = coordinator self.detents = detents self.smallestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge super.init(nibName: nil, bundle: .main) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func dismissModalView(){ dismiss(animated: true, completion: nil) } func presentModalView(){ let hostingController = UIHostingController(rootView: content) hostingController.modalPresentationStyle = .popover hostingController.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate hostingController.modalTransitionStyle = .coverVertical if let hostPopover = hostingController.popoverPresentationController { hostPopover.sourceView = super.view let sheet = hostPopover.adaptiveSheetPresentationController //As of 13 Beta 4 if .medium() is the only detent in landscape error occurs sheet.detents = (isLandscape ? [.large()] : detents) sheet.largestUndimmedDetentIdentifier = smallestUndimmedDetentIdentifier sheet.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge sheet.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true } if presentedViewController == nil{ present(hostingController, animated: true, completion: nil) } } /// To compensate for orientation as of 13 Beta 4 only [.large()] works for landscape override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) if UIDevice.current.orientation.isLandscape { isLandscape = true self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()] } else { isLandscape = false self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents } }}@available(iOS 15.0, *)struct CustomSheetView_Previews: PreviewProvider { static var previews: some View { CustomSheetParentView() }}