在 iPad 上具有自定义大小的 SwiftUI sheet() 模态

Posted

技术标签:

【中文标题】在 iPad 上具有自定义大小的 SwiftUI sheet() 模态【英文标题】:SwiftUI sheet() modals with custom size on iPad 【发布时间】:2020-04-01 09:39:20 【问题描述】:

如何使用 SwiftUI 在 iPad 上控制模态表的首选呈现大小?我很惊讶在谷歌上找到答案是多么困难。

此外,了解模态框是否通过向下拖动(取消)或实际执行自定义积极操作而被解除的最佳方法是什么?

【问题讨论】:

您找到解决方案了吗?我正在尝试复制 UIModalPresentationStyle.formSheet 的大小和行为,并且不想自己滚动它。我们希望我们在 iPad 上的模态能够坚持这种风格。 developer.apple.com/documentation/uikit/… 不,我没有。我最终只是在 ZStack 中使用了常规视图。非常烦人,因为它还有各种其他可访问性问题。 【参考方案1】:

这是我在 SwiftUI 中在 iPad 上显示表单的解决方案:

struct MyView: View 
    @State var show = false

    var body: some View 
        Button("Open Sheet")  self.show = true 
            .formSheet(isPresented: $show) 
                Text("Form Sheet Content")
            
    

由此 UIViewControllerRepresentable 启用

class FormSheetWrapper<Content: View>: UIViewController, UIPopoverPresentationControllerDelegate 

    var content: () -> Content
    var onDismiss: (() -> Void)?

    private var hostVC: UIHostingController<Content>?

    required init?(coder: NSCoder)  fatalError("") 

    init(content: @escaping () -> Content) 
        self.content = content
        super.init(nibName: nil, bundle: nil)
    

    func show() 
        guard hostVC == nil else  return 
        let vc = UIHostingController(rootView: content())

        vc.view.sizeToFit()
        vc.preferredContentSize = vc.view.bounds.size

        vc.modalPresentationStyle = .formSheet
        vc.presentationController?.delegate = self
        hostVC = vc
        self.present(vc, animated: true, completion: nil)
    

    func hide() 
        guard let vc = self.hostVC, !vc.isBeingDismissed else  return 
        dismiss(animated: true, completion: nil)
        hostVC = nil
    

    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) 
        hostVC = nil
        self.onDismiss?()
    


struct FormSheet<Content: View> : UIViewControllerRepresentable 

    @Binding var show: Bool

    let content: () -> Content

    func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> FormSheetWrapper<Content> 

        let vc = FormSheetWrapper(content: content)
        vc.onDismiss =  self.show = false 
        return vc
    

    func updateUIViewController(_ uiViewController: FormSheetWrapper<Content>,
                                context: UIViewControllerRepresentableContext<FormSheet<Content>>) 
        if show 
            uiViewController.show()
        
        else 
            uiViewController.hide()
        
    


extension View 
    public func formSheet<Content: View>(isPresented: Binding<Bool>,
                                          @ViewBuilder content: @escaping () -> Content) -> some View 
        self.background(FormSheet(show: isPresented,
                                  content: content))
    


您应该能够根据 UIKit 规范修改 func show() 中的代码,以便按照您喜欢的方式调整大小(如果需要,您甚至可以从 SwiftUI 端注入参数)。这就是我如何让表单在 iPad 上工作的方式,因为 .sheet 对于我的用例来说太大了

【讨论】:

谢谢,这真的很有帮助!我有一个问题,我的表单缩小到非常小。我从您的示例代码中删除了这两行并为我修复了它:vc.view.sizeToFit() vc.preferredContentSize = vc.view.bounds.size 这在 ios 14 上对我来说非常有效,但在 iOS 15 上似乎内容视图的布局不正确。它的所有子视图都相互叠加。有没有人调整它以在 iOS 15 上工作? @MattL 我刚刚在 iPadOS 15 上使用过它,它确实有效。我的内容不是很复杂——一个带有一些文本、HStacks、按钮的 VStack。 我尝试像这样在show() 中设置自定义宽度,但没有任何改变:vc.preferredContentSize = CGSize(width: 400, height: vc.view.bounds.size.height)。有谁知道如何为纸张尺寸使用一定的宽度?【参考方案2】:

我刚刚在这个 SO How can I make a background color with opacity on a Sheet view? 中发布了相同的内容 但它似乎完全符合我的需要。它使工作表的背景透明,同时允许根据需要调整内容的大小,使其看起来好像它是工作表的唯一部分。在 iPad 上运行良好。

使用我整天试图找到的来自@Asperi 的 AWESOME 答案,我构建了一个简单的视图修改器,现在可以在 .sheet 或 .fullScreenCover 模式视图中应用它并提供透明背景。然后,您可以根据需要为内容设置框架修饰符以适应屏幕,而无需用户知道模态框不是自定义大小的。

import SwiftUI

struct ClearBackgroundView: UIViewRepresentable 
    func makeUIView(context: Context) -> some UIView 
        let view = UIView()
        DispatchQueue.main.async 
            view.superview?.superview?.backgroundColor = .clear
        
        return view
    
    func updateUIView(_ uiView: UIViewType, context: Context) 
    


struct ClearBackgroundViewModifier: ViewModifier 
    
    func body(content: Content) -> some View 
        content
            .background(ClearBackgroundView())
    


extension View 
    func clearModalBackground()->some View 
        self.modifier(ClearBackgroundViewModifier())
    

用法:

.sheet(isPresented: $isPresented) 
            ContentToDisplay()
            .frame(width: 300, height: 400)
            .clearModalBackground()
    

【讨论】:

【参考方案3】:

如果它对其他人有帮助,我可以通过依靠这段代码来保存视图控制器来完成这项工作: https://gist.github.com/timothycosta/a43dfe25f1d8a37c71341a1ebaf82213

struct ViewControllerHolder 
    weak var value: UIViewController?
    init(_ value: UIViewController?) 
        self.value = value
    


struct ViewControllerKey: EnvironmentKey 
    static var defaultValue: ViewControllerHolder?  ViewControllerHolder(UIApplication.shared.windows.first?.rootViewController) 


extension EnvironmentValues 
    var viewController: ViewControllerHolder? 
        get  self[ViewControllerKey.self] 
        set  self[ViewControllerKey.self] = newValue 
    


extension UIViewController 
    func present<Content: View>(
        presentationStyle: UIModalPresentationStyle = .automatic,
        transitionStyle _: UIModalTransitionStyle = .coverVertical,
        animated: Bool = true,
        completion: @escaping () -> Void =  /* nothing by default*/ ,
        @ViewBuilder builder: () -> Content
    ) 
        let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
        toPresent.modalPresentationStyle = presentationStyle
        toPresent.rootView = AnyView(
            builder()
                .environment(\.viewController, ViewControllerHolder(toPresent))
        )
        if presentationStyle == .overCurrentContext 
            toPresent.view.backgroundColor = .clear
        
        present(toPresent, animated: animated, completion: completion)
    


加上一个专门的视图来处理模态中的常见元素:

struct ModalContentView<Content>: View where Content: View 
    // Use this function to provide the content to display and to bring up the modal.
    // Currently only the 'formSheet' style has been tested but it should work with any
    // modal presentation style from UIKit.
    public static func present(_ content: Content, style: UIModalPresentationStyle = .formSheet) 
        let modal = ModalContentView(content: content)

        // Present ourselves
        modal.viewController?.present(presentationStyle: style) 
            modal.body
        
    

    // Grab the view controller out of the environment.
    @Environment(\.viewController) private var viewControllerHolder: ViewControllerHolder?
    private var viewController: UIViewController? 
        viewControllerHolder?.value
    

    // The content to be displayed in the view.
    private var content: Content

    public var body: some View 
        VStack 
            /// Some specialized controls, like X button to close omitted...

            self.content
        
    

最后,只需调用: ModalContentView.present( MyAwesomeView() )

在 .formSheet 模式中显示 MyAwesomeView

【讨论】:

【参考方案4】:

@ccwasden 的回答存在一些问题。关闭弹出框不会一直更改 $isPresented,因为未设置委托且从未分配过 hostVC。

这里需要一些修改。 在 FlexSheetWrapper 中:

func show() 
    guard hostVC == nil else  return 
    let vc = UIHostingController(rootView: content())

    vc.view.sizeToFit()
    vc.preferredContentSize = vc.view.bounds.size

    vc.modalPresentationStyle = .formSheet
    vc.presentationController?.delegate = self
    hostVC = vc
    self.present(vc, animated: true, completion: nil)

在表单中:

func updateUIViewController(_ uiViewController: FlexSheetWrapper<Content>,
                            context: UIViewControllerRepresentableContext<FlexSheet<Content>>) 
    if show 
        uiViewController.show()
    
    else 
        uiViewController.hide()
    

【讨论】:

【参考方案5】:

来自@ccwasden 的回答,我解决了当你在代码开头$isPresented = true 时出现的问题,加载视图时模态不会出现,这里是代码View+FormSheet.swift

结果

// You can now set `test = true` at first
.formSheet(isPresented: $test) 
    Text("Hi")

查看+FormSheet.swift

import SwiftUI

class ModalUIHostingController<Content>: UIHostingController<Content>, UIPopoverPresentationControllerDelegate where Content : View 
    
    var onDismiss: (() -> Void)
    
    required init?(coder: NSCoder)  fatalError("") 
    
    init(onDismiss: @escaping () -> Void, rootView: Content) 
        self.onDismiss = onDismiss
        super.init(rootView: rootView)
        view.sizeToFit()
        preferredContentSize = view.bounds.size
        modalPresentationStyle = .formSheet
        presentationController?.delegate = self
    
    
    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) 
        print("modal dismiss")
        onDismiss()
    


class ModalUIViewController<Content: View>: UIViewController 
    var isPresented: Bool
    var content: () -> Content
    var onDismiss: (() -> Void)
    private var hostVC: ModalUIHostingController<Content>
    
    private var isViewDidAppear = false
    
    required init?(coder: NSCoder)  fatalError("") 
    
    init(isPresented: Bool = false, onDismiss: @escaping () -> Void, content: @escaping () -> Content) 
        self.isPresented = isPresented
        self.onDismiss = onDismiss
        self.content = content
        self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
        super.init(nibName: nil, bundle: nil)
    
    
    func show() 
        guard isViewDidAppear else  return 
        self.hostVC = ModalUIHostingController(onDismiss: onDismiss, rootView: content())
        present(hostVC, animated: true)
    
    
    func hide() 
        guard !hostVC.isBeingDismissed else  return 
        dismiss(animated: true)
    
    
    override func viewDidAppear(_ animated: Bool) 
        super.viewDidAppear(true)
        isViewDidAppear = true
        if isPresented 
            show()
        
    
    
    override func viewDidDisappear(_ animated: Bool) 
        super.viewDidDisappear(animated)
        isViewDidAppear = false
    
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
        super.viewWillTransition(to: size, with: coordinator)
        show()
    


struct FormSheet<Content: View> : UIViewControllerRepresentable 
    
    @Binding var show: Bool
    
    let content: () -> Content
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<FormSheet<Content>>) -> ModalUIViewController<Content> 
    
        let onDismiss = 
            self.show = false
        
        
        let vc = ModalUIViewController(isPresented: show, onDismiss: onDismiss, content: content)
        return vc
    
    
    func updateUIViewController(_ uiViewController: ModalUIViewController<Content>,
                                context: UIViewControllerRepresentableContext<FormSheet<Content>>) 
        if show 
            uiViewController.show()
        
        else 
            uiViewController.hide()
        
    


extension View 
    public func formSheet<Content: View>(isPresented: Binding<Bool>,
                                         @ViewBuilder content: @escaping () -> Content) -> some View 
        self.background(FormSheet(show: isPresented,
                                  content: content))
    



【讨论】:

感谢您的回答。我尝试在.formSheet 中使用TextField(),但是当我开始输入时,我收到此消息:Binding&lt;String&gt; action tried to update multiple times per frame 我该如何解决这个问题?有什么想法吗?

以上是关于在 iPad 上具有自定义大小的 SwiftUI sheet() 模态的主要内容,如果未能解决你的问题,请参考以下文章

自定义按钮样式 (SwiftUI) tvOS

自定义尺寸 iPad 拆分视图

SwiftUI:禁用具有自定义 ButtonStyle 的 NavigationLink 的最佳方法

SwiftUI:自定义按钮无法识别具有清晰背景和 buttonStyle 的触摸

SwiftUI 如何获取自定义幻灯片转换的视图大小?

具有自定义数据结构的 SwiftUI 列表,每个循环都嵌套 [关闭]