How can I control the preferred presentation size of a modal sheet on iPad with SwiftUI? I'm surprised how hard it is to find an answer on Google for this.
Also, what is the best way to know if the modal is dismissed by dragging it down (cancelled) or actually performing a custom positive action?
The action of the button toggles the state variable. To tell SwiftUI you want to display a modal, you call sheet(isPresented:onDismiss:content:) .
To use a sheet, give it something to show (some text, an image, a custom view, etc), add a Boolean that defines whether the detail view should be showing, then attach it to your main view as a modal sheet. Important: If you're targeting iOS 14 or below, you should use @Environment(\.
Here is my solution for showing a form sheet on an iPad in SwiftUI:
struct MyView: View {
@State var show = false
var body: some View {
Button("Open Sheet") { self.show = true }
.formSheet(isPresented: $show) {
Text("Form Sheet Content")
}
}
}
Enabled by this 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))
}
}
You should be able to modify the code in func show()
according to UIKit specs in order to get the sizing the way you like (and you can even go so far as to inject parameters from the SwiftUI side if needed). This is just how I get a form sheet to work on iPad as .sheet
was just too big for my use case
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With