SwiftUI 如何在按钮下方显示确认弹窗?
结论
iOS 16 没有公开的 SwiftUI 修饰符可以让 .alert() 或 .confirmationDialog() 在 iPhone 上显示成 Reminders 那种贴近按钮、带小箭头的确认气泡。要让内容出现在按钮附近,可以将 .popover 附加到按钮,并在弹层中自定义确认内容。弹层会锚定到按钮,但外观由应用控制,无法保证与 Reminders 完全一致。
为什么现有修饰符不会贴在按钮旁
.alert() 使用系统警告框样式。.confirmationDialog() 则根据设备和呈现环境显示系统提供的确认界面,在 iPhone 上通常是屏幕底部的操作表。两者都没有指定按钮锚点的参数。
UIKit 的 UIAlertController 也有类似限制:在适用的 iPad 场景中,.actionSheet 可以通过 popoverPresentationController 指定锚点;在 iPhone 上通常仍会显示为底部操作表。仅设置 UIKit 锚点,无法让 iPhone 上的界面变成 Reminders 式气泡。
使用 SwiftUI popover 锚定按钮
下面的示例适用于 iOS 16.4 及以上版本。.presentationCompactAdaptation(.popover) 请求系统在紧凑宽度环境中保留 popover 呈现。实际布局仍可能因系统版本和呈现环境而异。
struct MyView: View {
@Environment(\.dismiss) private var dismiss
@State private var showDiscardConfirmation = false
var body: some View {
NavigationStack {
Form {
// Content here
}
.navigationTitle("New Reminder")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
showDiscardConfirmation = true
} label: {
Image(systemName: "xmark")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(.secondary)
.frame(width: 30, height: 30)
.background(Color.gray.opacity(0.1))
.clipShape(Circle())
}
.popover(
isPresented: $showDiscardConfirmation,
attachmentAnchor: .rect(.bounds),
arrowEdge: .top
) {
VStack(alignment: .leading, spacing: 12) {
Text("Discard this new reminder?")
.font(.headline)
HStack {
Button("Keep Editing") {
showDiscardConfirmation = false
}
Button("Discard Changes", role: .destructive) {
showDiscardConfirmation = false
dismiss()
}
}
}
.padding()
.frame(width: 280)
.presentationCompactAdaptation(.popover)
}
}
}
}
}
}
.popover 附加在按钮上,因此按钮会作为弹层的锚点。arrowEdge: .top 请求箭头位于弹层顶部,通常会让弹层出现在按钮下方。具体位置由系统布局决定。
UIKit 方案的边界
使用 UIKit 标准操作表时,可以创建 .actionSheet 类型的 UIAlertController,并为 iPad 设置 popoverPresentationController 的 sourceView 和 sourceRect。这种做法适合需要系统操作表样式的场景,但不能保证 iPhone 上显示为按钮下方的小气泡。
如果需要严格复刻某个系统界面的外观,就要自行构建和维护 SwiftUI 弹层内容。系统版本变化也可能影响呈现细节。对于 iOS 16.0 至 16.3,还需检查目标 SDK 支持的紧凑宽度适配方式,不能假定上述适配修饰符可用。
备注:内容仅供参考。
