简体   繁体   English

iOS 15:在 SwiftUI 中使用 UISheetPresentationController

[英]iOS 15: using UISheetPresentationController in SwiftUI

I'm really struggling to wrap the new iOS 15 UISheetPresentationController for use in SwiftUI (for a half-modal).我真的很难包装新的 iOS 15 UISheetPresentationController以用于 SwiftUI(用于半模态)。 I understand that I should inherit UIViewControllerRepresentable .我知道我应该继承UIViewControllerRepresentable Based upon an example I have for a custom ImagePicker, I've not been able to make this work.根据我对自定义 ImagePicker 的示例,我无法完成这项工作。

Can anyone help?任何人都可以帮忙吗? In particular I don't know to get a handle on the presentedViewController needed to init the UISheetPresentationController itself:特别是,我不知道要获得初始化UISheetPresentationController本身所需的presentedViewController的句柄:


    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 https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

If you want the Image Picker如果你想要图像选择器

import SwiftUI
///This is the ParentView
@available(iOS 15.0, *)
struct CustomSheet: View {
    //To control the picker
    @State var presentImagePicker: Bool = false
    //To house the selected image
    @State var uiImage: UIImage = UIImage.remove
    var body: some View {
        VStack{
            Button(presentImagePicker ? "dismiss image picker" : "present image picker", action: {
                presentImagePicker.toggle()
            })
            Image(uiImage: uiImage).resizable().frame(width: 100, height: 100, alignment: .center)
            CustomImageSheet_UI(presentImagePicker: $presentImagePicker, uiImage: $uiImage)
        }
    }
}
@available(iOS 15.0, *)
struct CustomImageSheet_UI: UIViewControllerRepresentable {
    
    @Binding var presentImagePicker: Bool
    @Binding var uiImage: UIImage
    init(presentImagePicker: Binding<Bool>,uiImage: Binding<UIImage>) {
        self._presentImagePicker = presentImagePicker
        self._uiImage = uiImage
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    func makeUIViewController(context: Context) -> CustomImageSheetViewController {
        let vc = CustomImageSheetViewController(coordinator: context.coordinator)
        return vc
    }
    
    func updateUIViewController(_ uiViewController: CustomImageSheetViewController, context: Context) {
        
        if presentImagePicker{
            uiViewController.presentImagePicker()
        }else{
            uiViewController.dismissModalView()
        }
    }
    class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: CustomImageSheet_UI
        
        init(_ parent: CustomImageSheet_UI) {
            self.parent = parent
        }
        //Adjust the variable when the user dismisses with a swipe
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            
            if parent.presentImagePicker{
                parent.presentImagePicker = false
            }
        }
        //Adjust the variable when the user cancels
        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            if parent.presentImagePicker{
                parent.presentImagePicker = 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.presentImagePicker = false
            }
        }
    }
}
//This custom view controller
@available(iOS 15.0, *)
class CustomImageSheetViewController: UIViewController {
    let coordinator: CustomImageSheet_UI.Coordinator
    init(coordinator: CustomImageSheet_UI.Coordinator) {
        self.coordinator = coordinator
        super.init(nibName: nil, bundle: .main)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func dismissModalView(){
        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 popover = imagePicker.popoverPresentationController {
            popover.sourceView = super.view
            let sheet = popover.adaptiveSheetPresentationController
            sheet.detents = [.medium(), .large()]
            sheet.largestUndimmedDetentIdentifier =
                .medium
            sheet.prefersScrollingExpandsWhenScrolledToEdge =
            false
            sheet.prefersEdgeAttachedInCompactHeight =
            true
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
        }
        
        present(imagePicker, animated: true, completion: nil)
    }
}

If you want one that takes any SwiftUI View it only needs a few changes.如果您想要一个采用任何 SwiftUI View它只需要一些更改。

@available(iOS 15.0, *)
struct CustomSheet: View {
    @State private var isPresented = false
    
    var body: some View {
        VStack{
        Button(isPresented ? "dismiss content" : "present content", action: {
            isPresented.toggle()
        })
            CustomSheet_UI(isPresented: $isPresented, detents: [.medium()], largestUndimmedDetentIdentifier: .large){
            Rectangle()
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                .foregroundColor(.clear)
                .border(Color.blue, width: 3)
                .overlay(Text("Hello, World!"))
        }
        }
    }
}
@available(iOS 15.0, *)
struct CustomSheet_UI<Content: View>: UIViewControllerRepresentable {
    
    let content: Content
    @Binding var isPresented: Bool
    let detents : [UISheetPresentationController.Detent]
    let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    
    init(isPresented: Binding<Bool>, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.detents = detents
        self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
        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, largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, 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 largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
    let prefersScrollingExpandsWhenScrolledToEdge: Bool
    let prefersEdgeAttachedInCompactHeight: Bool
    
    init(coordinator: CustomSheet_UI<Content>.Coordinator, detents : [UISheetPresentationController.Detent] = [.medium(), .large()], largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = .medium, prefersScrollingExpandsWhenScrolledToEdge: Bool = false, prefersEdgeAttachedInCompactHeight: Bool = true, @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.coordinator = coordinator
        self.detents = detents
        self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
        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
        if let hostPopover = hostingController.popoverPresentationController {
            hostPopover.sourceView = super.view
            let sheet = hostPopover.adaptiveSheetPresentationController
            sheet.detents = detents
            sheet.largestUndimmedDetentIdentifier =
            largestUndimmedDetentIdentifier
            sheet.prefersScrollingExpandsWhenScrolledToEdge =
            prefersScrollingExpandsWhenScrolledToEdge
            sheet.prefersEdgeAttachedInCompactHeight =
            prefersEdgeAttachedInCompactHeight
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
            
        }
        if presentedViewController == nil{
            present(hostingController, animated: true, completion: nil)
        }        
    }
}

The way this API seems to work is to use a regular UIViewController and in viewDidLoad you can grab the UISheetPresentationController and configure it.这个 API 的工作方式似乎是使用常规的UIViewController并在viewDidLoad中您可以获取UISheetPresentationController并对其进行配置。 By default all iOS 13+ modals are sheets automatically.默认情况下,所有 iOS 13+ 模态都是自动工作表。

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.我目前正在做的是使用充当工作表的 UIHostingController。

Create a custom hosting controller class.创建自定义托管 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 eg drag gesture.目前,委托并未让我检测工作表是否以非编程方式更改,例如拖动手势。 Not sure this is an early beta bug.不确定这是早期的 beta 错误。 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.也有点遗憾的是,他们没有添加一个小的定位设置,使工作表不可关闭,并且像地图中那样交互背后的视图。

Found the options given here a wee bit complex, so here is an alternative on 3 steps:发现这里给出的选项有点复杂,所以这里有 3 个步骤的替代方案:

1 1

Subclass UIHostingController and personalise子类UIHostingController和个性化

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 2

Create an UIViewControllerRepresentable using your UIHostingController , we are using a ViewBuilder here for maximum flexibility.使用您的UIHostingController创建一个UIViewControllerRepresentable ,我们在这里使用 ViewBuilder 以获得最大的灵活性。

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 3

Present as sheet on your SwiftUI View在您的 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!")
            }
        }
    }
}

From Lorem Ipsum Answer is work great on iPhone but not currently for iPad .来自 Lorem Ipsum 的答案在 iPhone 上效果很好,但目前不适用于 iPad

List列表

  • Background Color is clear背景颜色清晰
  • Content Size is wrong (not fit with the content)内容大小错误(与内容不符)

iPad Result iPad 结果

在此处输入图像描述

Workaround解决方法

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM