HStack 行为中带有弹出窗口的 SwiftUI 多个按钮
SwiftUI Multiple Buttons with Popovers in HStack Behavior
当 HStack 中有多个带有弹出窗口的按钮时,我会遇到奇怪的行为。每当您点击一个按钮时,弹出窗口都会正确显示。但是,当您单击第二个项目时,第一个弹出窗口会快速关闭然后重新打开。预期的行为是它关闭第一个弹出窗口并打开第二个。 Xcode12.5.1,iOS14.5
这是我的代码:
struct ContentView: View {
var items = ["item1", "item2", "item3"]
var body: some View {
HStack {
MyGreatItemView(item: items[0])
MyGreatItemView(item: items[1])
MyGreatItemView(item: items[2])
}
.padding(300)
}
struct MyGreatItemView: View {
@State var isPresented = false
var item: String
var body: some View {
Button(action: { isPresented.toggle() }) {
Text(item)
}
.popover(isPresented: $isPresented) {
PopoverView(item: item)
}
}
}
struct PopoverView: View {
@State var item: String
var body: some View {
print("new PopoverView")
return Text("View for \(item)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
感谢您的帮助!
通常你会使用 popover(item:content:)
, but you'll get an error... even the example in the documentation 崩溃。
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'UIPopoverPresentationController (<UIPopoverPresentationController: 0x14a109890>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.'
我想到的是在 ContentView
中使用单数 @State presentingItem: Item?
。这可确保所有弹出窗口都绑定到相同的 State
,因此您可以完全控制显示哪些弹出窗口。
但是,.popover(isPresented:content:)
的 isPresented
参数需要 Bool
。如果这是真的,它会提出,如果不是,它将被驳回。要将 presentingItem
转换为 Bool
,只需使用自定义 Binding
.
Binding(
get: { presentingItem == item }, /// present popover when `presentingItem` is equal to this view's `item`
set: { _ in presentingItem = nil } /// remove the current `presentingItem` which will dismiss the popover
)
然后,在每个按钮的操作中设置 presentingItem
。这是事情变得有点棘手的部分 - 我添加了 0.5
秒延迟以确保首先关闭当前显示的弹出窗口。否则不会出现。
if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
presentingItem = item /// present this popover after a delay
}
}
完整代码:
/// make equatable, for the `popover` presentation logic
struct Item: Equatable {
let id = UUID()
var name: String
}
struct ContentView: View {
@State var presentingItem: Item? /// the current presenting popover
let items = [
Item(name: "item1"),
Item(name: "item2"),
Item(name: "item3")
]
var body: some View {
HStack {
MyGreatItemView(presentingItem: $presentingItem, item: items[0])
MyGreatItemView(presentingItem: $presentingItem, item: items[1])
MyGreatItemView(presentingItem: $presentingItem, item: items[2])
}
.padding(300)
}
}
struct MyGreatItemView: View {
@Binding var presentingItem: Item?
let item: Item /// this view's item
var body: some View {
Button(action: {
if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if presentingItem == nil { /// extra check to ensure no popover currently presented
presentingItem = item /// present this popover after a delay
}
}
}
}) {
Text(item.name)
}
/// `get`: present popover when `presentingItem` is equal to this view's `item`
/// `set`: remove the current `presentingItem` which will dismiss the popover
.popover(isPresented: Binding(get: { presentingItem == item }, set: { _ in presentingItem = nil }) ) {
PopoverView(item: item)
}
}
}
struct PopoverView: View {
let item: Item /// no need for @State here
var body: some View {
print("new PopoverView")
return Text("View for \(item.name)")
}
}
结果:
当 HStack 中有多个带有弹出窗口的按钮时,我会遇到奇怪的行为。每当您点击一个按钮时,弹出窗口都会正确显示。但是,当您单击第二个项目时,第一个弹出窗口会快速关闭然后重新打开。预期的行为是它关闭第一个弹出窗口并打开第二个。 Xcode12.5.1,iOS14.5
这是我的代码:
struct ContentView: View {
var items = ["item1", "item2", "item3"]
var body: some View {
HStack {
MyGreatItemView(item: items[0])
MyGreatItemView(item: items[1])
MyGreatItemView(item: items[2])
}
.padding(300)
}
struct MyGreatItemView: View {
@State var isPresented = false
var item: String
var body: some View {
Button(action: { isPresented.toggle() }) {
Text(item)
}
.popover(isPresented: $isPresented) {
PopoverView(item: item)
}
}
}
struct PopoverView: View {
@State var item: String
var body: some View {
print("new PopoverView")
return Text("View for \(item)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
感谢您的帮助!
通常你会使用 popover(item:content:)
, but you'll get an error... even the example in the documentation 崩溃。
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'UIPopoverPresentationController (<UIPopoverPresentationController: 0x14a109890>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.'
我想到的是在 ContentView
中使用单数 @State presentingItem: Item?
。这可确保所有弹出窗口都绑定到相同的 State
,因此您可以完全控制显示哪些弹出窗口。
但是,.popover(isPresented:content:)
的 isPresented
参数需要 Bool
。如果这是真的,它会提出,如果不是,它将被驳回。要将 presentingItem
转换为 Bool
,只需使用自定义 Binding
.
Binding(
get: { presentingItem == item }, /// present popover when `presentingItem` is equal to this view's `item`
set: { _ in presentingItem = nil } /// remove the current `presentingItem` which will dismiss the popover
)
然后,在每个按钮的操作中设置 presentingItem
。这是事情变得有点棘手的部分 - 我添加了 0.5
秒延迟以确保首先关闭当前显示的弹出窗口。否则不会出现。
if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
presentingItem = item /// present this popover after a delay
}
}
完整代码:
/// make equatable, for the `popover` presentation logic
struct Item: Equatable {
let id = UUID()
var name: String
}
struct ContentView: View {
@State var presentingItem: Item? /// the current presenting popover
let items = [
Item(name: "item1"),
Item(name: "item2"),
Item(name: "item3")
]
var body: some View {
HStack {
MyGreatItemView(presentingItem: $presentingItem, item: items[0])
MyGreatItemView(presentingItem: $presentingItem, item: items[1])
MyGreatItemView(presentingItem: $presentingItem, item: items[2])
}
.padding(300)
}
}
struct MyGreatItemView: View {
@Binding var presentingItem: Item?
let item: Item /// this view's item
var body: some View {
Button(action: {
if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if presentingItem == nil { /// extra check to ensure no popover currently presented
presentingItem = item /// present this popover after a delay
}
}
}
}) {
Text(item.name)
}
/// `get`: present popover when `presentingItem` is equal to this view's `item`
/// `set`: remove the current `presentingItem` which will dismiss the popover
.popover(isPresented: Binding(get: { presentingItem == item }, set: { _ in presentingItem = nil }) ) {
PopoverView(item: item)
}
}
}
struct PopoverView: View {
let item: Item /// no need for @State here
var body: some View {
print("new PopoverView")
return Text("View for \(item.name)")
}
}
结果: