我有一个
List
,我希望用户能够选择多个项目,右键单击以显示选项菜单,对于其中一个选项,显示一个从选定行之一指出的弹出窗口在列表中。到目前为止,我已经能够进行多重选择并显示带有选项的上下文菜单,但是所显示的弹出窗口是针对“列表”显示的,而不是任何选定的行。我该怎么做呢?
这是我的示例内容视图:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
@State private var multiSelectedContacts = Set<Item.ID>()
@State private var showPopover = false
var body: some View {
NavigationStack {
List (selection: $multiSelectedContacts) {
ForEach(items) { item in
ContentItemView(item: item)
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
.contextMenu(forSelectionType: Item.ID.self, menu: { _ in
Button("Label Items", action: {
showPopover = true
})
})
.popover(isPresented: $showPopover, content: {
Text("Test Label")
.frame(width: 100, height: 150)
})
Text("Select an item")
}
}
private func addItem() {
///...
}
}
struct ContentItemView: View {
@Environment(\.managedObjectContext) private var viewContext
let item: Item
@State var presentConfirmation = false
var body: some View {
HStack {
if let timestamp = item.timestamp, let itemNumber = item.itemNumber {
Text("\(itemNumber) - \(timestamp, formatter: itemFormatter)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .long
return formatter
}()
popover
修饰符需要修改ContentItemView
,而不是列表。
那么我们将什么传递给
isPresented
呢?我们不能只传递 $showPopover
,因为这会导致弹出窗口显示在所有选定的项目上。因此,我们需要一个自定义 Binding
,仅适用于 multiSelectedContacts
中的一项。假设 Item.ID
是 Comparable
,我们可以使用 min(by: <)
或 max(by: <)
找到这样的项目。
let binding = Binding {
showPopover && multiSelectedContacts.min(by: <) == item.id
} set: {
// when the popover is dismissed, we should reset showPopover
if !$0 { showPopover = false }
}
ContentItemView(item: item)
.popover(isPresented: binding) {
Text("Popover")
}
以这种方式精确选择一个项目可能会导致选择当前不可见的项目。例如,可以选择列表中的第一项并滚动到最底部,导致第一项不可见。
要处理这种情况,您还需要跟踪哪些项目是可见的:
@State private var visibleItems = Set<Item.ID>()
let binding = Binding {
showPopover && visibleItems.intersection(multiSelectedContacts).min(by: <) == item.id
} set: {
if !$0 { showPopover = false }
}
ContentItemView(item: item)
.onAppear { visibleItems.insert(item.id) }
.onDisappear { visibleItems.remove(item.id) }
.popover(isPresented: binding) {
Text("Popover")
}
这是一个完整的最小可重现示例:
struct Item: Identifiable, Hashable {
let id = UUID()
}
struct ContentView: View {
private let items: [Item] = (0..<100).map { _ in Item() }
@State private var multiSelectedContacts = Set<Item.ID>()
@State private var visibleItems = Set<Item.ID>()
@State private var showPopover = false
var body: some View {
NavigationStack {
List (selection: $multiSelectedContacts) {
ForEach(items) { item in
let binding = Binding {
showPopover && visibleItems.intersection(multiSelectedContacts).min(by: <) == item.id
} set: {
if !$0 {
showPopover = false
}
}
ContentItemView(item: item)
.onAppear {
visibleItems.insert(item.id)
}
.onDisappear {
visibleItems.remove(item.id)
}
.popover(isPresented: binding) {
Text("Popover")
}
}
}
.contextMenu(forSelectionType: Item.ID.self, menu: { _ in
Button("Label Items", action: {
showPopover = true
})
})
.onChange(of: multiSelectedContacts) {
showPopover = false
}
}
}
}
struct ContentItemView: View {
let item: Item
var body: some View {
Text(item.id.uuidString)
}
}