Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Present ActionSheet in SwiftUI on iPad

I've gotten an ActionSheet to present just fine on an iPhone device. But it crashes for iPad. Says it needs the location for the popover. Has anyone had luck with this code? I'm using iOS 13 beta 3 and Xcode 11 beta 3. (This uses a version of presenting the ActionSheet not available in beta 2)

import SwiftUI  struct ContentView : View {     @State var showSheet = false      var body: some View {         VStack {             Button(action: {                 self.showSheet.toggle()             }) {                 Text("Show")             }             .presentation($showSheet) { () -> ActionSheet in                 ActionSheet(title: Text("Hello"))              }         }     } }  #if DEBUG struct ContentView_Previews : PreviewProvider {     static var previews: some View {         ContentView()     } } #endif 
like image 448
MScottWaller Avatar asked Jul 06 '19 03:07

MScottWaller


People also ask

How to show actionSheet in SwiftUI?

You show an action sheet by using the actionSheet(isPresented:content:) view modifier to create an action sheet, which then appears whenever the bound isPresented value is true . The content closure you provide to this modifier produces a customized instance of the ActionSheet type.

Is iPad a SwiftUI?

The Swift Playgrounds iPad app has recently added support for SwiftUI, as well as the Combine framework which provides new ways to handle asynchronous events.

What is action sheet?

An action sheet is a modal view that presents choices related to an action people initiate. DEVELOPER NOTE When you use SwiftUI, you can enable action sheet functionality in all platforms by specifying a presentation modifier for a confirmation dialog.


2 Answers

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)         }     } }  
like image 60
marcprux Avatar answered Sep 19 '22 17:09

marcprux


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()     } } 
like image 23
MScottWaller Avatar answered Sep 22 '22 17:09

MScottWaller