'Using UISheetPresentationController in SwiftUI

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



Solution 1:[1]

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)
        
    }
}

Solution 2:[2]

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!")
            }
        }
    }
}

Solution 3:[3]

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.

Solution 4:[4]

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
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4