8

I'm really struggling to wrap the new iOS 15 UISheetPresentationController for use in SwiftUI (for a half-modal). I understand that I should inherit UIViewControllerRepresentable. Based upon an example I have for a custom ImagePicker, I've not been able to make this work.

Can anyone help? In particular I don't know to get a handle on the presentedViewController needed to init the UISheetPresentationController itself:

func makeUIViewController(context: UIViewControllerRepresentableContext<KitSheet>) -> UISheetPresentationController {
    let sheet = UISheetPresentationController(presentedViewController: <#T##UIViewController#>, presenting: <#T##UIViewController?#>)
    sheet.delegate = context.coordinator
    return sheet
}

https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

pkamb
  • 33,281
  • 23
  • 160
  • 191
pjc_1983
  • 83
  • 1
  • 3
  • 1
    I added it to the bottom of my original answer here. If it worked do you mind accepting the answer and maybe upvoting? – lorem ipsum Jun 24 '21 at 23:56
  • 1
    For other people searching for this: You don't need to use UISheetPresentationController if you only need a sheet with a large detent. In that case, using .sheet() on a View is a lot simpler. This UIKit method is useful if you also need a medium detent. – Bjørn Olav Jalborg Nov 08 '21 at 08:43

4 Answers4

10

If you want the Image Picker

import SwiftUI
///Sample usage
@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerParentView: View {
    @State var isPresented = false
    @State var selectedImage: UIImage? = nil
    var body: some View {
        print("ImagePickerParentView :: \(#function) :: isPresented == \(isPresented)")
        
        return VStack{
            if selectedImage != nil{
                Image(uiImage: selectedImage!)
                    .resizable()
                    .frame(width: 100, height: 100)
            }
            Button("present image picker", action: {
                isPresented.toggle()
            }).imagePicker(isPresented: $isPresented, uiImage: $selectedImage, detents: [.medium()], largestUndimmedDetentIdentifier: .large)
            
        }
    }
}
@available(iOS 15.0, macCatalyst 15.0,*)
extension View {
    func imagePicker(isPresented: Binding<Bool>, uiImage: Binding<UIImage?>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool = false,  preferredCornerRadius: CGFloat? = nil)-> some View {
        print("\(#function) :: isPresented == \(isPresented)")
        return modifier(ImagePickerViewModifier(isPresented: isPresented, uiImage: uiImage, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius))
    }
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerViewModifier: ViewModifier {
    
    @Binding var isPresented: Bool
    @Binding var uiImage: UIImage?
    let detents : [UISheetPresentationController.Detent]
    let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    let prefersGrabberVisible: Bool
    let preferredCornerRadius: CGFloat?
    
    func body(content: Content) -> some View {
        print("ImagePickerViewModifier :: \(#function) :: isPresented == \(isPresented)")
        return content.overlay(
            AdaptiveImagePicker_UI(isPresented: $isPresented, uiImage: $uiImage, detents: detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius).frame(width: 0, height: 0)
            
        )
        
            .onChange(of: isPresented, perform: { value in
                print("AdaptiveSheet :: onChange :: isPresented == \(value)")
            })
        
        //}
    }
}

@available(iOS 15.0, macCatalyst 15.0,*)
struct AdaptiveImagePicker_UI: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    @Binding var uiImage: UIImage?
    var detents : [UISheetPresentationController.Detent] = [.medium(), .large()]
    var largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium
    var prefersScrollingExpandsWhenScrolledToEdge: Bool = false
    var prefersEdgeAttachedInCompactHeight: Bool = true
    var prefersGrabberVisible: Bool = false
    var preferredCornerRadius: CGFloat?
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: Context) -> AdaptiveImagePickerViewController {
        print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
        let vc = AdaptiveImagePickerViewController(coordinator: context.coordinator, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge:  prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, preferredCornerRadius: preferredCornerRadius)
        return vc
    }
    
    func updateUIViewController(_ uiViewController: AdaptiveImagePickerViewController, context: Context) {
        print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
        print("CustomSheet_UI :: \(#function) :: context.coordinator.parent.isPresented == \(context.coordinator.parent.isPresented)")
        if isPresented {
            uiViewController.presentImagePicker()
        }else{
            uiViewController.dismissModalView()
        }
    }
    class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: AdaptiveImagePicker_UI
        var isPresented: Bool = false
        init(_ parent: AdaptiveImagePicker_UI) {
            print("CustomSheet_UI :: \(#function) :: parent.isPresented == \(parent.isPresented)")
            
            self.parent = parent
        }
        //Adjust the variable when the user dismisses with a swipe
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            print("CustomSheet_UI.Coordinator :: \(#function) :: parent.isPresented == \(parent.isPresented)")
            if parent.isPresented{
                parent.isPresented = false
            }
        }
        //Adjust the variable when the user cancels
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            if parent.isPresented{
                parent.isPresented = false
            }
        }
        //Get access to the selected image
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            
            if let image = info[.originalImage] as? UIImage {
                parent.uiImage = image
                parent.isPresented = false
            }
        }
        
    }
}

@available(iOS 15.0, macCatalyst 15.0,*)
class AdaptiveImagePickerViewController: UIViewController {
    var coordinator: AdaptiveImagePicker_UI.Coordinator
    let detents : [UISheetPresentationController.Detent]
    let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    let prefersGrabberVisible: Bool
    let preferredCornerRadius: CGFloat?
    private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
    init(coordinator: AdaptiveImagePicker_UI.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool, preferredCornerRadius: CGFloat?) {
        print("AdaptiveImagePickerViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
        self.coordinator = coordinator
        self.detents = detents
        self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
        self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
        self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
        self.prefersGrabberVisible = prefersGrabberVisible
        self.preferredCornerRadius = preferredCornerRadius
        super.init(nibName: nil, bundle: .main)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func dismissModalView(){
        print("AdaptiveImagePickerViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
        
        dismiss(animated: true, completion: nil)
    }
    
    //This is mostly code from the Apple sample
    //https://developer.apple.com/documentation/uikit/uiviewcontroller/customize_and_resize_sheets_in_uikit
    func presentImagePicker(){
        guard presentedViewController == nil else {
            dismiss(animated: true, completion: {
                self.presentImagePicker()
            })
            return
        }
        
        let imagePicker = UIImagePickerController()
        imagePicker.delegate = coordinator
        imagePicker.modalPresentationStyle = .popover
        //Added the presentation controller delegate to detect if the user swipes to dismiss
        imagePicker.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate
        
        if let hostPopover = imagePicker.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 =
            largestUndimmedDetentIdentifier
            sheet.prefersScrollingExpandsWhenScrolledToEdge =
            prefersScrollingExpandsWhenScrolledToEdge
            sheet.prefersEdgeAttachedInCompactHeight =
            prefersEdgeAttachedInCompactHeight
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
            sheet.prefersGrabberVisible = prefersGrabberVisible
            sheet.preferredCornerRadius = preferredCornerRadius
        }
        
        present(imagePicker, animated: true, completion: nil)
    }
    
    /// To compensate for l orientation
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        
        if UIDevice.current.orientation.isLandscape {
            isLandscape = true
            self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()]
        } else {
            isLandscape = false
            self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents
        }
        super.viewWillTransition(to: size, with: coordinator)
        
    }
}

@available(iOS 15.0, macCatalyst 15.0,*)
struct ImagePickerParentView_Previews: PreviewProvider {
    static var previews: some View {
        ImagePickerParentView()
    }
}

If you want one that takes any SwiftUI View it only needs a few changes.

//This is the sample usage
@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheetParentView: View {
    @State var isPresented = false
    var body: some View {
        print("CustomSheetParentView :: \(#function) :: isPresented == \(isPresented)")
        
        return VStack{
            Button("present sheet", action: {
                isPresented.toggle()
            }).adaptiveSheet(isPresented: $isPresented, detents: [.medium()], largestUndimmedDetentIdentifier: .medium,  disableSwipeToDismiss: false){
                Rectangle()
                    .frame(maxWidth: .infinity, maxHeight: 100, alignment: .center)
                    .foregroundColor(.clear)
                
                    .border(Color.blue, width: 3)
                    .overlay(
                        LazyVStack{
                            Text("Hello, World!")
                            Button("dismiss", action: {
                                print("dismiss button :: isPresented == \(isPresented)")
                                isPresented = false
                            })
                            CustomSheetParentView()
                        }
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .onTapGesture {
                                print("onTap :: isPresented == \(isPresented)")
                                
                                isPresented.toggle()
                            }
                    )
                
                    .background(Color(UIColor.systemBackground))
            }
            
        }
    }
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheetView_Previews: PreviewProvider {
    static var previews: some View {
        CustomSheetParentView()
    }
}


//EVERYTHING from here down is Reusable and can be pasted into a project and then use `.adaptiveSheet` just like `.sheet`
@available(iOS 15.0, macCatalyst 15.0,*)
extension View {
    func adaptiveSheet<T: View>(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool = false, disableSwipeToDismiss: Bool = false, preferredCornerRadius: CGFloat? = nil, @ViewBuilder content: @escaping () -> T)-> some View {
        print("\(#function) :: isPresented == \(isPresented)")
        return modifier(AdaptiveSheet<T>(isPresented: isPresented, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss, preferredCornerRadius: preferredCornerRadius, sheetContent: content))
    }
}
@available(iOS 15.0, macCatalyst 15.0,*)
struct AdaptiveSheet<T: View>: ViewModifier {
    
    @Binding var isPresented: Bool
    let detents : [UISheetPresentationController.Detent]
    let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    let prefersGrabberVisible: Bool
    let disableSwipeToDismiss: Bool
    let preferredCornerRadius: CGFloat?
    @ViewBuilder let sheetContent: T
    
    func body(content: Content) -> some View {
        print("AdaptiveSheet :: \(#function) :: isPresented == \(isPresented)")
        return content.overlay(
            CustomSheet_UI(isPresented: $isPresented, detents: detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss,preferredCornerRadius: preferredCornerRadius, content: {sheetContent}).frame(width: 0, height: 0)
            
        )
        
            .onChange(of: isPresented, perform: { value in
                print("AdaptiveSheet :: onChange :: isPresented == \(value)")
            })
        
        //}
    }
}

@available(iOS 15.0, macCatalyst 15.0,*)
struct CustomSheet_UI<T: View>: UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    var detents : [UISheetPresentationController.Detent] = [.medium(), .large()]
    var largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium
    var prefersScrollingExpandsWhenScrolledToEdge: Bool = false
    var prefersEdgeAttachedInCompactHeight: Bool = true
    var prefersGrabberVisible: Bool = false
    var disableSwipeToDismiss: Bool = false
    var preferredCornerRadius: CGFloat?
    @ViewBuilder let content: T
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: Context) -> CustomSheetViewController<T> {
        print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
        let vc = CustomSheetViewController(coordinator: context.coordinator, detents : detents, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersScrollingExpandsWhenScrolledToEdge:  prefersScrollingExpandsWhenScrolledToEdge, prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, prefersGrabberVisible: prefersGrabberVisible, disableSwipeToDismiss: disableSwipeToDismiss, preferredCornerRadius: preferredCornerRadius, content: {content})
        return vc
    }
    
    func updateUIViewController(_ uiViewController: CustomSheetViewController<T>, context: Context) {
        print("CustomSheet_UI :: \(#function) :: isPresented == \(isPresented)")
        print("CustomSheet_UI :: \(#function) :: context.coordinator.parent.isPresented == \(context.coordinator.parent.isPresented)")
        if isPresented {
            uiViewController.presentModalView()
        }else{
            uiViewController.dismissModalView()
        }
    }
    class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
        var parent: CustomSheet_UI
        var isPresented: Bool = false
        init(_ parent: CustomSheet_UI) {
            print("CustomSheet_UI :: \(#function) :: parent.isPresented == \(parent.isPresented)")
            
            self.parent = parent
        }
        //Adjust the variable when the user dismisses with a swipe
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            print("CustomSheet_UI.Coordinator :: \(#function) :: parent.isPresented == \(parent.isPresented)")
            if parent.isPresented{
                parent.isPresented = false
            }
        }
        
    }
}

@available(iOS 15.0, macCatalyst 15.0,*)
class CustomSheetViewController<Content: View>: UIViewController {
    let content: Content
    var coordinator: CustomSheet_UI<Content>.Coordinator
    let detents : [UISheetPresentationController.Detent]
    let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    let prefersGrabberVisible: Bool
    let disableSwipeToDismiss: Bool
    let preferredCornerRadius: CGFloat?
    private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
    init(coordinator: CustomSheet_UI<Content>.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, prefersGrabberVisible: Bool, disableSwipeToDismiss: Bool, preferredCornerRadius: CGFloat?, @ViewBuilder content: @escaping () -> Content) {
        print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
        self.content = content()
        self.coordinator = coordinator
        self.detents = detents
        self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
        self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
        self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
        self.prefersGrabberVisible = prefersGrabberVisible
        self.disableSwipeToDismiss = disableSwipeToDismiss
        self.preferredCornerRadius = preferredCornerRadius
        super.init(nibName: nil, bundle: .main)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func dismissModalView(){
        print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
        
        dismiss(animated: true, completion: nil)
    }
    func presentModalView(){
        print("CustomSheetViewController :: \(#function) :: isPresented == \(coordinator.parent.isPresented)")
        
        let hostingController = UIHostingController(rootView: content)
        //allows background color to be decided by SwiftUI content.
        // Incase you want to use a Material that gives transparency
        hostingController.view.backgroundColor = nil
        hostingController.modalPresentationStyle = .popover
        hostingController.presentationController?.delegate = coordinator as UIAdaptivePresentationControllerDelegate
        hostingController.modalTransitionStyle = .coverVertical
        hostingController.isModalInPresentation = disableSwipeToDismiss
        
        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 =
            largestUndimmedDetentIdentifier
            sheet.prefersScrollingExpandsWhenScrolledToEdge =
            prefersScrollingExpandsWhenScrolledToEdge
            sheet.prefersEdgeAttachedInCompactHeight =
            prefersEdgeAttachedInCompactHeight
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
            sheet.prefersGrabberVisible = prefersGrabberVisible
            sheet.preferredCornerRadius = preferredCornerRadius
        }
        if presentedViewController == nil{
            present(hostingController, animated: true, completion: nil)
        }
    }
    /// To compensate for l orientation
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        
        if UIDevice.current.orientation.isLandscape {
            isLandscape = true
            self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = [.large()]
        } else {
            isLandscape = false
            self.presentedViewController?.popoverPresentationController?.adaptiveSheetPresentationController.detents = detents
        }
        super.viewWillTransition(to: size, with: coordinator)
        
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • how can I add background color on behind view when sheet is open ?, I tried add it inside SwiftUI view but it was delay after I close it – Basel Feb 17 '22 at 09:49
  • 1
    @Basil I used the second option, the one that accepts any SwiftUiI View and basically placed a VStack with a .frame with hight and width to .infinity so it covers up the entire space of the modal – grenos Mar 23 '22 at 18:22
  • and then i used the background modifier on that Vstack – grenos Mar 23 '22 at 18:43
9

Found the options given here a wee bit complex, so here is an alternative on 3 steps:

1

Subclass UIHostingController and personalise

class HalfSheetController<Content>: UIHostingController<Content> where Content : View {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        if let presentation = sheetPresentationController {
            // configure at will
            presentation.detents = [.medium()]
        }
    }
}

2

Create an UIViewControllerRepresentable using your UIHostingController, we are using a ViewBuilder here for maximum flexibility.

struct HalfSheet<Content>: UIViewControllerRepresentable where Content : View {
    private let content: Content
    
    @inlinable init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    func makeUIViewController(context: Context) -> HalfSheetController<Content> {
        return HalfSheetController(rootView: content)
    }
    
    func updateUIViewController(_: HalfSheetController<Content>, context: Context) {

    }
}

3

Present as sheet on your SwiftUI View

struct Example: View {
    @State private var present = false
    
    var body: some View {
        Button("Present") {
            present = true
        }
        .sheet(isPresented: $present) {
            HalfSheet {
                Text("Hello, World!")
            }
        }
    }
}
vicegax
  • 4,709
  • 28
  • 37
  • 1
    I tried doing that but if you add `presentation.largestUndimmedDetentIdentifier = .medium` to keep the back undimmed the `View` behind, in this case the button never goes back to normal. Something doesn't get dismissed properly. – lorem ipsum Sep 17 '21 at 16:13
  • Thank you! Your answer is the only one that really works without random bugs :) For those, who're wondering how to get rid of insets in the bottom of the sheet — use `.ignoresSafeArea()` on the `HalfSheet` – alexandrov Jul 24 '22 at 20:02
  • This short solution actually help to customize the prefersEdgeAttachedInCompactHeight = true problem i was having with SwiftUI. Much easier than the adaptive sheet examples i've seen. – geekydevjoe Mar 27 '23 at 17:31
2

The way this API seems to work is to use a regular UIViewController and in viewDidLoad you can grab the UISheetPresentationController and configure it. By default all iOS 13+ modals are sheets automatically.

class SheetContentViewController: UIViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         if let sheetPresentationController = presentationController as? UISheetPresentationController {
            sheetPresentationController.detents = [.medium(), .large()]
            sheetPresentationController.prefersGrabberVisible = true
     }
}

What I am currently doing is using a UIHostingController that acts as the sheet.

Create a custom hosting controller class.

import UIKit
import SwiftUI

@available(iOS 15.0, *)
final class SheetHostingController<T: View>: UIHostingController<T>, UISheetPresentationControllerDelegate {
    
    // MARK: - Properties
    
    private let detents: [UISheetPresentationController.Detent]
    private let prefersEdgeAttachedInCompactHeight: Bool
    private let prefersScrollingExpandsWhenScrolledToEdge: Bool
    
    // MARK: - Initialization
    
    init(
        rootView: T,
        title: String? = nil,
        largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode  = .never,
        detents: [UISheetPresentationController.Detent] = [.medium(), .large()],
        prefersEdgeAttachedInCompactHeight: Bool = true,
        prefersScrollingExpandsWhenScrolledToEdge: Bool = true
    ) {
        self.detents = detents
        self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
        self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
        super.init(rootView: rootView)
        navigationItem.title = title
        navigationItem.largeTitleDisplayMode = largeTitleDisplayMode
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .green
        
        if let sheetPresentationController = presentationController as? UISheetPresentationController {
            sheetPresentationController.delegate = self
            sheetPresentationController.detents = detents
            sheetPresentationController.prefersGrabberVisible = true
            sheetPresentationController.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
            sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
        }
    }
    
    // MARK: - Public Methods
    
   func set(to detentIdentifier: UISheetPresentationController.Detent.Identifier?) {
        guard let sheetPresentationController = presentationController as? UISheetPresentationController else { return }
        sheetPresentationController.animateChanges {
            sheetPresentationController.selectedDetentIdentifier = detentIdentifier
        }
    }

    // MARK: - UISheetPresentationControllerDelegate
    
   func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
      // Currently not working?
    }
}

and than you can present it in your app flow

let swiftUIView = SomeSwiftUIView()
let sheetHostingController = SheetHostingController(rootView: swiftUIView)
someViewController.present(sheetHostingController, animated: true)

Currently the delegate is not firing for me to detect if the sheet was changed non-programatically e.g. drag gesture. Not sure this is an early beta bug. Also a bit of a shame they have not added a small detent setting, made the sheet non dismissible and the view behind interactive like in maps.

crashoverride777
  • 10,581
  • 2
  • 32
  • 56
  • I made a generic one that takes any SwiftUI View as a parameter for [this question](https://stackoverflow.com/questions/56700752/swiftui-half-modal/67994666#67994666) it works as expected – lorem ipsum Jun 20 '21 at 19:35
  • @loremipsum your example worked perfectly for me, in that link. – pjc_1983 Jun 22 '21 at 12:01
0

Nov, 11

Lorem Ipsum now provides the solution that work on iPad as popover

iPad Result

enter image description here

Nov, 10

From Lorem Ipsum Answer is work great on iPhone but not currently for iPad.

List

  • Background Color is clear
  • Content Size is wrong (not fit with the content)

iPad Result

enter image description here

Workaround

guard UIDevice.current.userInterfaceIdiom == .phone else { 
    viewModel.isPresentedPopOver 
    return
}

Chat Dp
  • 555
  • 5
  • 15