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

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

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

【问题讨论】:

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

标签: swift ipad swiftui


【解决方案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 我该如何解决这个问题?有什么想法吗?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-12-31
        • 1970-01-01
        • 1970-01-01
        • 2020-04-25
        • 1970-01-01
        • 2013-06-10
        • 1970-01-01
        相关资源
        最近更新 更多