swift - 在 SwiftUI 中递归构建菜单
问题描述
问题
我最近在 iOS 14 中发现了 SwiftUI OutlineGroup
(我使用的是 Xcode 12 beta 6)。这非常有效,无论是单独使用还是在使用 时List
,都可以根据需要从树状结构、已识别数据的基础集合中计算“视图和披露组”。
即,如果您有一个struct
递归定义,那么这对于构建DisclosureGroup
元素非常有效。但是我正在寻找一些不同的东西,可以让我建立一个“下拉”(或汉堡包)菜单。
在 iOS 14 中还有一个名为 的控件Menu
,它完全按照我的意愿呈现“下拉”(或汉堡)菜单:
但是我似乎无法将两者一起使用来构建Menu
基于递归表示的数据的动态,例如:
struct Tree<Value: Hashable>: Hashable {
let value: Value
var children: [Tree]? = nil
}
并以以下方式构建菜单:
struct SideMenu: View {
var body: some View {
Menu {
Button(action: {}) {
Image(systemName: "person")
.foregroundColor(.gray)
.imageScale(.large)
Text("Profile")
.foregroundColor(.gray)
.font(.headline)
}
Button(action: {}) {
Image(systemName: "person.3")
.foregroundColor(.gray)
.imageScale(.large)
Text("Family Members")
.foregroundColor(.gray)
.font(.headline)
}
Button(action: {}) {
Image(systemName: "calendar")
.foregroundColor(.gray)
.imageScale(.large)
Text("Events")
.foregroundColor(.gray)
.font(.headline)
}
} label: {
Image(systemName: "line.horizontal.3")
}
}
}
问题
有没有一种方法可以从递归数据中构建Menu
,类似于使用的方法OutlineGroup
?
解决方案
我喜欢用枚举来表示树,以避免不可能或不一致的状态。此外,您需要一个递归 UI 函数调用,但使用方法会使编译器对我来说失败(Xcode 12 beta 6),所以我将菜单部分分隔在不同的视图中,这似乎可行。现在你有了一个可以从你的 ViewModel 构建的完全动态的菜单。
import SwiftUI
enum ViewEvent {
case profileTapped
case familyMembersTapped
case eventsTapped
case foldersTapped
case deletedItemsTapped
}
struct MenuItem: Identifiable {
var id: String { return text }
let text: String
let systemImage: String?
let action: ViewEvent?
}
enum MenuContent: Identifiable {
var id: String {
switch self {
case let .item(item): return item.id
case let .submenu(text, _): return text
}
}
case item(MenuItem)
indirect case submenu(text: String, content: [MenuContent])
}
struct ViewState {
let menu: [MenuContent]
let content: String
static var `default`: ViewState {
.init(
menu: [
.item(MenuItem(text: "Profile", systemImage: "person", action: .profileTapped)),
.item(MenuItem(text: "Family Members", systemImage: "person.3", action: .familyMembersTapped)),
.item(MenuItem(text: "Events", systemImage: "calendar", action: .familyMembersTapped)),
.submenu(text: "More", content: [
.item(MenuItem(text: "Folders", systemImage: "folder.fill", action: .foldersTapped)),
.item(MenuItem(text: "Deleted", systemImage: "trash.fill", action: .deletedItemsTapped))
])
],
content: "Content")
}
}
struct ContentView: View {
@State var viewState: ViewState
var body: some View {
HStack(alignment: .top, spacing: 16) {
AppMenu(contents: viewState.menu) {
Image(systemName: "line.horizontal.3")
}
Text("Content")
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
}
struct AppMenuItem: View {
let item: MenuItem
func dispatch(_ action: ViewEvent) {
// todo: call viewModel.dispatch
print("Sending action \(action)")
}
init(item: MenuItem) {
self.item = item
}
var body: some View {
Button(action: {
item.action.map { action in dispatch(action) }
}) {
item.systemImage.map { systemImage in
Image(systemName: systemImage)
.foregroundColor(.gray)
.imageScale(.large)
}
Text(item.text)
.foregroundColor(.gray)
.font(.headline)
}
}
}
struct AppSubmenu: View {
let text: String
let contents: [MenuContent]
var body: some View {
AppMenu(contents: contents) {
HStack {
Text(text)
Image(systemName: "chevron.right")
}
}
}
}
struct AppMenu<Label: View>: View {
let label: () -> Label
let contents: [MenuContent]
init(contents: [MenuContent], @ViewBuilder label: @escaping () -> Label) {
self.contents = contents
self.label = label
}
var body: some View {
Menu {
ForEach(contents) { content in
// In case this is an item
if case let .item(item) = content {
AppMenuItem(item: item)
}
// In case this is a submenu
if case let .submenu(text, contents) = content {
AppSubmenu(text: text, contents: contents)
}
}
} label: { label() }
}
}
推荐阅读
- python - 无法安装 PyAudio,portaudiomodule 错误
- vb.net - 组合框功能
- java - Wildfly/JBOSS 持久性错误 MSC000001:无法启动服务 jboss.persistenceunit
- javascript - 为什么警报有效,但音乐不会停止?
- pyhf - `normfactor` 中的高斯约束
- powershell - 如何在 foreach 循环中使用 Powershell 写入 CSV 中的一个条目
- sql - 如何检查 BigQuery 的列表中是否有多个值
- terraform - 如何使用 terraform 导入/下载现有的 cloudflare 资源?
- reactjs - 在 CRA 中正确实现 robots.txt
- javascript - 从 JS 对象返回最大值