Present ActionSheet in SwiftUI on iPad
Sadly, this bug has not been fixed for the final release of iOS 13. It was mentioned on the developer forums, and I've filed a feedback for it (FB7397761
), but for the time being one needs to work around it by using some other UI when UIDevice.current.userInterfaceIdiom == .pad
.
For the record, the (unhelpful) exception message is:
2019-10-21 11:26:58.205533-0400 LOOksTape[34365:1769883] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIAlertController (<UIAlertController: 0x7f826e094a00>) of style UIAlertControllerStyleActionSheet from _TtGC7SwiftUI19UIHostingController…
The modalPresentationStyle of a UIAlertController with this style is UIModalPresentationPopover.
You must provide location information for this popover through the alert controller's popoverPresentationController.
You must provide either a sourceView and sourceRect or a barButtonItem.
If this information is not known when you present the alert controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.'
As a workaround, this popSheet
function will display a popover on the iPad and an ActionSheet
everywhere else:
public extension View {
/// Creates an `ActionSheet` on an iPhone or the equivalent `popover` on an iPad, in order to work around `.actionSheet` crashing on iPad (`FB7397761`).
///
/// - Parameters:
/// - isPresented: A `Binding` to whether the action sheet should be shown.
/// - content: A closure returning the `PopSheet` to present.
func popSheet(isPresented: Binding<Bool>, arrowEdge: Edge = .bottom, content: @escaping () -> PopSheet) -> some View {
Group {
if UIDevice.current.userInterfaceIdiom == .pad {
popover(isPresented: isPresented, attachmentAnchor: .rect(.bounds), arrowEdge: arrowEdge, content: { content().popover(isPresented: isPresented) })
} else {
actionSheet(isPresented: isPresented, content: { content().actionSheet() })
}
}
}
}
/// A `Popover` on iPad and an `ActionSheet` on iPhone.
public struct PopSheet {
let title: Text
let message: Text?
let buttons: [PopSheet.Button]
/// Creates an action sheet with the provided buttons.
public init(title: Text, message: Text? = nil, buttons: [PopSheet.Button] = [.cancel()]) {
self.title = title
self.message = message
self.buttons = buttons
}
/// Creates an `ActionSheet` for use on an iPhone device
func actionSheet() -> ActionSheet {
ActionSheet(title: title, message: message, buttons: buttons.map({ popButton in
// convert from PopSheet.Button to ActionSheet.Button (i.e., Alert.Button)
switch popButton.kind {
case .default: return .default(popButton.label, action: popButton.action)
case .cancel: return .cancel(popButton.label, action: popButton.action)
case .destructive: return .destructive(popButton.label, action: popButton.action)
}
}))
}
/// Creates a `.popover` for use on an iPad device
func popover(isPresented: Binding<Bool>) -> some View {
VStack {
ForEach(Array(buttons.enumerated()), id: \.offset) { (offset, button) in
Group {
SwiftUI.Button(action: {
// hide the popover whenever an action is performed
isPresented.wrappedValue = false
// another bug: if the action shows a sheet or popover, it will fail unless this one has already been dismissed
DispatchQueue.main.async {
button.action?()
}
}, label: {
button.label.font(.title)
})
Divider()
}
}
}
}
/// A button representing an operation of an action sheet or popover presentation.
///
/// Basically duplicates `ActionSheet.Button` (i.e., `Alert.Button`).
public struct Button {
let kind: Kind
let label: Text
let action: (() -> Void)?
enum Kind { case `default`, cancel, destructive }
/// Creates a `Button` with the default style.
public static func `default`(_ label: Text, action: (() -> Void)? = {}) -> Self {
Self(kind: .default, label: label, action: action)
}
/// Creates a `Button` that indicates cancellation of some operation.
public static func cancel(_ label: Text, action: (() -> Void)? = {}) -> Self {
Self(kind: .cancel, label: label, action: action)
}
/// Creates an `Alert.Button` that indicates cancellation of some operation.
public static func cancel(_ action: (() -> Void)? = {}) -> Self {
Self(kind: .cancel, label: Text("Cancel"), action: action)
}
/// Creates an `Alert.Button` with a style indicating destruction of some data.
public static func destructive(_ label: Text, action: (() -> Void)? = {}) -> Self {
Self(kind: .destructive, label: label, action: action)
}
}
}
Finally, as tested in iOS 13.4 this has been resolved, at least in the beta. The conflicting constraints warning persists, but the crash is gone. This is now the appropriate way to present an action sheet.
import SwiftUI
struct ContentView : View {
@State var showSheet = false
var body: some View {
VStack {
Button(action: {
self.showSheet.toggle()
}) {
Text("Show")
}
.actionSheet(isPresented: $showSheet, content: { ActionSheet(title: Text("Hello"))
})
}
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}