在 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<String> action tried to update multiple times per frame
我该如何解决这个问题?有什么想法吗?以上是关于在 iPad 上具有自定义大小的 SwiftUI sheet() 模态的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI:禁用具有自定义 ButtonStyle 的 NavigationLink 的最佳方法