iPad 在 SwiftUI 中呈现 ActionSheet
Present ActionSheet in SwiftUI on iPad
我已经得到一个 ActionSheet,可以在 iPhone 设备上正常显示。但它崩溃了 iPad。说它需要弹出窗口的位置。有没有人有幸使用这段代码?我正在使用 iOS 13 beta 3 和 Xcode 11 beta 3。(这使用了在 beta 2 中不可用的呈现 ActionSheet 的版本)
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
这是一个已知的测试版错误。等待修复。
遗憾的是,iOS13 的最终版本尚未修复此错误。developer forums 中提到了它,我已经提交了反馈(FB7397761
), 但目前需要通过在 UIDevice.current.userInterfaceIdiom == .pad
.
时使用其他 UI 来解决它
根据记录,(无用的)异常消息是:
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.'
作为解决方法,此 popSheet
函数将在 iPad 上显示一个弹出窗口,在其他任何地方显示一个 ActionSheet
:
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)
}
}
}
这是我针对该错误的解决方法 - 它保留了 iPhone 设备的 "actionsheet" 功能,但只是为 iPad
创建了一个 "alert" 样式的控制器
对于我的情况来说很简单,可能会对其他人有所帮助
var preferredStyle: UIAlertController.Style
if UIDevice.current.userInterfaceIdiom == .pad {
preferredStyle = .alert
}
else{
preferredStyle = .actionSheet
}
let cellMenu = UIAlertController(title: nil, message: "Bought Item?", preferredStyle: preferredStyle)
//Create actions
//Add Actions to menu
self.present(cellMenu, animated: true, completion: nil)
最后,正如在 iOS 13.4 中测试的那样,这个问题已经解决,至少在测试版中是这样。冲突约束警告仍然存在,但崩溃消失了。现在这是呈现动作的合适方式 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()
}
}
试试这个组件,它会在 iPhone 上显示一个 actionSheet,在 iPad 和 Mac 上显示一个 Popover。
https://github.com/AndreaMiotto/ActionOver
我已经得到一个 ActionSheet,可以在 iPhone 设备上正常显示。但它崩溃了 iPad。说它需要弹出窗口的位置。有没有人有幸使用这段代码?我正在使用 iOS 13 beta 3 和 Xcode 11 beta 3。(这使用了在 beta 2 中不可用的呈现 ActionSheet 的版本)
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
这是一个已知的测试版错误。等待修复。
遗憾的是,iOS13 的最终版本尚未修复此错误。developer forums 中提到了它,我已经提交了反馈(FB7397761
), 但目前需要通过在 UIDevice.current.userInterfaceIdiom == .pad
.
根据记录,(无用的)异常消息是:
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.'
作为解决方法,此 popSheet
函数将在 iPad 上显示一个弹出窗口,在其他任何地方显示一个 ActionSheet
:
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)
}
}
}
这是我针对该错误的解决方法 - 它保留了 iPhone 设备的 "actionsheet" 功能,但只是为 iPad
创建了一个 "alert" 样式的控制器对于我的情况来说很简单,可能会对其他人有所帮助
var preferredStyle: UIAlertController.Style
if UIDevice.current.userInterfaceIdiom == .pad {
preferredStyle = .alert
}
else{
preferredStyle = .actionSheet
}
let cellMenu = UIAlertController(title: nil, message: "Bought Item?", preferredStyle: preferredStyle)
//Create actions
//Add Actions to menu
self.present(cellMenu, animated: true, completion: nil)
最后,正如在 iOS 13.4 中测试的那样,这个问题已经解决,至少在测试版中是这样。冲突约束警告仍然存在,但崩溃消失了。现在这是呈现动作的合适方式 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()
}
}
试试这个组件,它会在 iPhone 上显示一个 actionSheet,在 iPad 和 Mac 上显示一个 Popover。 https://github.com/AndreaMiotto/ActionOver