在选择嵌套的Picker时出现的问题

移动开发 2026-07-09

我从以下代码开始,它展示了一个Picker

import SwiftUI

struct ContentView: View {
    enum MenuOption: String, CaseIterable {
        case sort = "Sort"
        case search = "Search"
        case somethingElse = "Something Else"
    }

    @State var selectedOption: MenuOption = .sort

    var body: some View {
        Picker("", selection: $selectedOption) {
            ForEach(MenuOption.allCases, id: \.self) {
                Text($0.rawValue)
            }
        }
        .pickerStyle(.menu)
    }
}

这运行得很顺利。

现在,我想为case sort添加一个二级子菜单,显示两个选项,于是我尝试了如下代码:

struct ContentView: View {
    enum MenuOption: String, CaseIterable {
        case sort = "Sort"
        case search = "Search"
        case somethingElse = "Something Else"
    }

    enum SortOption: String, CaseIterable {
        case date = "Date"
        case title = "Title"
    }

    @State var selectedOption: MenuOption = .sort
    @State var selectedSort: SortOption = .date

    var body: some View {
        Picker("", selection: $selectedOption) {
            ForEach(MenuOption.allCases, id: \.self) {
                if $0 == .sort {
                    Picker("Sort", selection: $selectedSort) {
                        ForEach(SortOption.allCases, id: \.self) {
                            Text($0.rawValue)
                        }
                    }
                    .pickerStyle(.menu)

                }
                else {
                    Text($0.rawValue)
                }
            }
        }
        .pickerStyle(.menu)
        .controlSize(.small)
    }
}

乍一看还可以,但如果选择了“Search”,随后把鼠标悬停到Sort > Date时,我本来希望选择器显示“Sort”,但它却仍然显示为“Search”。

我该如何修复?

解决方案

你可以考虑用Picker替换为Menu,然后为外部菜单使用自定义标签。还有其他可能性:

  • 如果枚举符合Identifiable,那么这将简化ForEach
  • 如果排序选项是可选的,那么这将简化标签逻辑。

以下是应用了所有改动的更新示例:

struct ContentView: View {
    enum MenuOption: String, CaseIterable, Identifiable {
        case sort = "Sort"
        case search = "Search"
        case somethingElse = "Something Else"
        var id: MenuOption { self }
    }

    enum SortOption: String, CaseIterable, Identifiable {
        case date = "Date"
        case title = "Title"
        var id: SortOption { self }
    }

    @State var selectedOption: MenuOption = .sort
    @State var selectedSort: SortOption? = .date

    var body: some View {
        Menu {
            ForEach(MenuOption.allCases) { option in
                if option == .sort {
                    Menu("Sort") {
                        ForEach(SortOption.allCases) { sort in
                            Button(sort.rawValue) {
                                selectedOption = .sort
                                selectedSort = sort
                            }
                        }
                    }
                }
                else {
                    Button(option.rawValue) {
                        selectedOption = option
                        selectedSort = nil
                    }
                }
            }
        } label: {
            Text(selectedSort?.rawValue ?? selectedOption.rawValue)
        }
        .controlSize(.small)
    }
}

Animation


编辑 关于你的评论:

唯一的问题是菜单的宽度现在会随着字符串长度而变化。

确实如此。修复的一种方法是应用.buttonSizing(.flexible)。这会让菜单按钮使用所有可用的宽度。然后它可以被限制到实际需要的宽度。

限制宽度的一种方法是使用.frame修饰符和一个固定的最大宽度:

Menu {
    // ...
}
.controlSize(.small)
.buttonSizing(.flexible)
.frame(maxWidth: 130)

另外,你也可以根据最宽选项标签的宽度来确定菜单标签的占位宽度。

  • 可以使用ZStack把所有选项标签合并在一起。最宽标签的宽度将决定ZStack的宽度。
  • 还需要一些水平填充,以容纳菜单指示符以及原生按钮样式自带的填充。
  • 如果使用默认字体(正文),那么当应用.controlSize(.small)时,这个宽度可能大于菜单按钮所用的字体大小。因此基准宽度可能不需要太多水平填充。
private var footprint: some View {
    ZStack(alignment: .leading) {
        ForEach(MenuOption.allCases) { option in
            Text(option.rawValue)
        }
        ForEach(SortOption.allCases) { sort in
            Text(sort.rawValue)
        }
    }
    .padding(.horizontal)
}

然后把Menu作为覆盖层显示在(隐藏的)基准宽度之上:

var body: some View {
    footprint
        .hidden()
        .overlay {
            Menu {
                // ...
            }
            .controlSize(.small)
            .buttonSizing(.flexible)
        }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章