ActionSheet crash on iPad

I would like to present an action sheet on iPad using SwiftUI, but it crashes with the following error:


Terminating app due to uncaught exception 'NSGenericException',
reason: 'Your application has presented a UIAlertController ()
of style UIAlertControllerStyleActionSheet from _TtGC11AppAlpha17HostingControllerVS_11ContentView_
(<_TtGC11AppAlpha17HostingControllerVS_11ContentView_: 0x105702dc0>).
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.'


My code:


.actionSheet(isPresented: $isShowingActionSheet) {
    ActionSheet(
        title: Text("Add a photo"),
        message: Text("Please select a source"),
        buttons: [
            .cancel { },
            .default(Text("Photo from library")) {
                self.imageSource = .photoLibrary
                self.isShowingImagePicker.toggle()
            },
            .default(Text("Take a picture")) {
                self.imageSource = .camera
                self.isShowingImagePicker.toggle()
            }
        ]
    )
}


I understand that I have to pass a source view for the action sheet to layout properly, but I don't have access to the underlying UIKit element. How should it be done?


Target: iPad

Xcode 11.2 beta 2 (11B44)

Replies

On the iPad, it seems you need to use .popover rather than .actionSheet. Sadly, ActionSheet() isn't a View type, so you can't just return your ActionSheet from the .popover content block. I've been playing around with it a bit, but I don't see an easy way to replicate the action sheet look-and-feel exactly. For example, I don't see any way to customize the popover's dimensions or background color at all; the view returned from the .popover builder block is simply placed inside an existing container of a static size (I'm guessing about 200x400 or something like that).


Here's the code I've used to play with this so far:


import SwiftUI
import UIKit

struct ContentView: View {
    @State var showingActionSheet = false
    @State var imageSource: UIImagePickerController.SourceType = .photoLibrary
    @State var showingPicker = false

    var body: some View {
        VStack {
            Button(action: {
                self.showingActionSheet.toggle()
            }) {
                Text("Show action sheet")
            }
        }
        .popover(isPresented: $showingActionSheet) {
            VStack {
                Text("Add a photo").bold().font(.largeTitle).foregroundColor(.secondary)
                Text("Select a source").font(.title).foregroundColor(.secondary)

                Divider()

                Button(action: {
                    self.showingActionSheet = false
                }) {
                    Text("Cancel").bold().font(.subheadline)//(.system(size: 18.0))
                }

                Divider()

                Button(action: {
                    self.imageSource = .photoLibrary
                    self.showingPicker.toggle()
                    self.showingActionSheet = false
                }) {
                    Text("Photo from library").font(.system(size: 18.0))
                }

                Divider()

                Button(action: {
                    self.imageSource = .camera
                    self.showingPicker.toggle()
                    self.showingActionSheet = false
                }) {
                    Text("Take a picture").font(.title)
                }
            }
            .padding(.vertical)
            .background(Color.green.opacity(0.6))
            Spacer()
        }
        .padding(40)
        .background(Color.yellow)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


I'm planning to file a bug report on this; I strongly suggest you do the same.

Good call. Filed bug FB7389876 on Feedback Assistant

We've run into this one as well (FB #FB7397761, FWIW). You can see our workaround at https://stackoverflow.com/questions/56910941/present-actionsheet-in-swiftui-on-ipad/58490096#58490096

With the caveat that I've not actually pulled down your code to try it out yet: did you have any trouble persuading the popover to change its size? I couldn't get it to be a different size for love nor money.

You could ask your own UIKit components to display the action sheet.


Use view controller containment to enclose your SwiftUI view. Have your own UIViewController subclass to enclose SwiftUI's view controller. In turn, have a property in the SwiftUI struct that is a closure that gets called to invoke the action sheet. Finally, initialize this closure from the enclosing UIViewController subclass to display that action sheet – in the closure you should have easy access to a UIView instance.