我正在尝试将用 Java 编写的程序移植到 SwiftUI。 UI 的基本结构是主视图左侧的导航树。用户可以选择一项,多项详细信息将显示在右侧的表格中。 导航树中的节点允许不同的操作 - 取决于它们的类型。 这些操作应该作为上下文菜单来实现。
我已经实现了一个小示例,但我无法使上下文菜单工作。
我想我使用了错误的方法。这是我的示例代码:
import SwiftUI
struct ContentView: View {
@State var selection = Set<Tree<String>>()
var body: some View {
NavigationView {
List(treeNodes, id: \.value, children: \.children, selection: $selection) { tree in
NavigationLink {
TabView {
Tab("Details 1", image: "Block") {
Text(tree.value)
}
Tab("Details 2", image: "Haus") {
Text(tree.value)
}
}
} label: {
Label(tree.value, image: tree.icon!)
}
}
.contextMenu(forSelectionType: Tree<String>.self) { nodes in
if nodes.count == 1 {
Button("One node selected") {}
} else {
Button("No node or more than 1 selected") {}
}
}
.listStyle(SidebarListStyle())
}
}
}
#Preview {
ContentView()
}
用于完成代码
import SwiftUI
// MARK: - Tree
/// Class for arbitrary trees. Values must be hashable
class Tree<Value: Hashable>: Hashable {
var value: Value
var icon: String?
var children: [Tree]?
init(value: any Hashable, icon: String? = nil, children: [Tree]? = nil) {
// swiftlint: disable force_cast
self.value = value as! Value
// swiftlint: enable force_cast
self.icon = icon
self.children = children
}
static func == (lhs: Tree<Value>, rhs: Tree<Value>) -> Bool {
return lhs.value == rhs.value
}
func hash(into hasher: inout Hasher) {
hasher.combine(value)
}
}
// MARK: - Value
/// Class for the stored value
class Value: Hashable, Equatable {
var id = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Value, rhs: Value) -> Bool {
return lhs.id == rhs.id
}
}
// MARK: - init demo data
/// example data using string values
let treeNodes: [Tree<String>] = [
.init(
value: "Root", icon: "Block",
children: [
.init(value: "House 1", icon: "Haus"),
.init(value: "House 2", icon: "Haus"),
.init(
value: "House 3", icon: "Haus",
children: [
.init(value: "Apartment 1", icon: "Wohnung"),
.init(value: "Apartment 2", icon: "Wohnung")
]
)
]
)
]
您不应将新 API 与旧 API 混合使用。使用
NavigationSplitView
而不是 NavigationView
。之后,代码将按预期工作。
NavigationSplitView {
List(treeNodes, id: \.value, children: \.children, selection: $selection) { tree in
NavigationLink(value: tree) {
// why make icon optional when you are going to force unwrap it?
Label(tree.value, image: tree.icon!)
}
}
.contextMenu(forSelectionType: Tree<String>.self) { nodes in
if nodes.count == 1 {
Button("One node selected") {}
} else {
Button("No node or more than 1 selected") {}
}
}
.listStyle(.sidebar)
} detail: {
// I'm not sure if TabViews in a detail view is officially supported.
// consider using a segmented Picker to act as tabs instead.
TabView {
// it is unclear what detail view you want to show when multiple items are selected,
// so here I have just used a random one from the set
if let tab = selection.first {
Tab("Details 1", image: "Block") {
Text(tab.value)
}
Tab("Details 2", image: "Haus") {
Text(tab.value)
}
} else {
Tab {
Text("No selection")
}
}
}
}
还可以考虑将
Tree
更改为 struct
,因为它们在 SwiftUI 中更容易使用。你只需要:
struct Tree<Value: Hashable>: Hashable {
var value: Value
var icon: String?
var children: [Tree]?
}
如果你真的希望
Tree
成为一个类,你应该将其设为 @Observable
,否则 SwiftUI 将无法看到 value
、icon
、children
的变化来更新视图。
我不确定
Value
类想要实现什么目标。