245 Followers
·
Follow

[Solution] SwiftUI ActionSheet crash on iPad

When I follow the #100DaysOfSwiftUI from https://www.hackingwithswift.com/100/swiftui. I experience with the crash when running on the iPad device that I don’t want to see. The crash log as below

2019-11-29 22:06:29.237883+1300 Instafilter[651:58326] *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIAlertController (<UIAlertController: 0x102008000>) of style UIAlertControllerStyleActionSheet from _TtGC7SwiftUI19UIHostingControllerV11Instafilter11ContentView_ (<_TtGC7SwiftUI19UIHostingControllerV11Instafilter11ContentView_: 0x101508c20>). 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.'*** First throw call stack:(0x1a51e8a48 0x1a4f0ffa4 0x1a8b5b588 0x1a8b657e0 0x1a8b63324 0x1a92644c0 0x1a9254010 0x1a9283e60 0x1a5165e68 0x1a5160d54 0x1a5161320 0x1a5160adc 0x1af0e9328 0x1a925ae78 0x100f726cc 0x1a4fea360)libc++abi.dylib: terminating with uncaught exception of type NSException(lldb)

Google helps me to navigate to a solution on the StackOverflow. However, it is hard to understand and how it works. I put them together and make it works on iPad with the result as below image

Image for post
Image for post

The idea is will show the Actionsheet on iPhone and Popover on iPad. So we define a structure that receives the inputs and outputs the proper view for the different device.

  1. Implement a PopSheet

The PopSheet struct almost the same in the StackOverflow. And I have the smalls update that removes keyword public of struct PopSheet to display the title and use the List to render the item so the source code doesn’t require the Divider between button.

struct PopSheet {
let title: Text
let message: Text?
let buttons: [PopSheet.Button]

public init(title: Text, message: Text? = nil, buttons: [PopSheet.Button] = [.cancel()]) {
self.title = title
self.message = message
self.buttons = buttons
}


func actionSheet() -> ActionSheet {
ActionSheet(title: title, message: message, buttons: buttons.map({ popButton in
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)
}
}))
}

func popover(isPresented: Binding<Bool>) -> some View {
VStack {
self.title.padding(.top)
Divider()
List {
ForEach(Array(self.buttons.enumerated()), id: \.offset) { (offset, button) in
VStack {
SwiftUI.Button(action: {
isPresented.wrappedValue = false
DispatchQueue.main.async {
button.action?()
}
}, label: {
button.label.font(.subheadline)
})
}
}
}
}
}

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)
}
}
}

2. Extensions View with a method that gets all information to render the ActionSheet or Popover according to the device is running. The idea is not different from the original solution, and I modified to make the popover base on the button change filter in the attachment.

extension View {
func popSheet(isPresented: Binding<Bool>, arrowEdge: Edge = .bottom, content: @escaping () -> PopSheet) -> some View {
Group {
if UIDevice.current.userInterfaceIdiom == .pad {
popover(isPresented: isPresented, attachmentAnchor: .point(.topTrailing), arrowEdge: arrowEdge, content: { content().popover(isPresented: isPresented) })
} else {
actionSheet(isPresented: isPresented, content: { content().actionSheet() })
}
}
}
}

3. Inject to the view that you want to display the PopSheet. Example:

Button(self.filterName) {
self.showingFilterSheet = true
}
.popSheet(isPresented: self.$showingFilterSheet, content: {
PopSheet(title: Text("Select a filter"), buttons: [
PopSheet.Button(kind: .default, label: Text(FilterType.Crystallize.rawValue), action: {
self.filterName = FilterType.Crystallize.rawValue
self.setFilter(CIFilter.crystallize())
}),
PopSheet.Button(kind: .default, label: Text(FilterType.Edges.rawValue), action: {
self.filterName = FilterType.Edges.rawValue
self.setFilter(CIFilter.edges())
}),
PopSheet.Button(kind: .default, label: Text(FilterType.GaussianBlur.rawValue), action: {
self.filterName = FilterType.GaussianBlur.rawValue
self.setFilter(CIFilter.gaussianBlur())
}),
PopSheet.Button(kind: .default, label: Text(FilterType.SepiaTone.rawValue), action: {
self.filterName = FilterType.SepiaTone.rawValue
self.setFilter(CIFilter.sepiaTone())
}),
PopSheet.Button(kind: .default, label: Text(FilterType.UnsharpMask.rawValue), action: {
self.filterName = FilterType.UnsharpMask.rawValue
self.setFilter(CIFilter.unsharpMask())
}),
PopSheet.Button(kind: .default, label: Text(FilterType.Vignette.rawValue), action: {
self.filterName = FilterType.Vignette.rawValue
self.setFilter(CIFilter.vibrance())
}),
PopSheet.Button(kind: .cancel, label: Text("Cancel"), action: {})
])
}

The full source code can see in the project Instafilter

#swiftui #iosdev #swift #actionsheet #popover

Have a great weekend

Written by

Senior mobile Developer (https://github.com/liemvo)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store