SwiftUI — contextMenu

contextMenu 是什么? #

SwiftUIcontextMenu 修饰符用于在用户长按视图时,弹出一个 上下文菜单Context Menu),提供一组相关的操作选项。这种效果类似于 UIKit 中的上下文菜单功能,特别适用于弹性快捷菜单,比如文件操作、链接操作等场景。

  • 它是一种 轻量级交互方式,通常结合长按手势触发,用来展示一组快捷选项。
  • 使用设备: 支持 iPhone、iPad 和 macOS 的交互体验,但需要注意的是:
    • 在 iOS 上通常通过长按激活。
    • 在 macOS 上支持右键或双指点击激活。

ContextMenu 的方法签名 #

创建上下文菜单的 .contextMenu 修饰符通常具有以下方法签名:

@ViewBuilder func contextMenu<MenuItems>(@ViewBuilder menuItems: () -> MenuItems) -> some View 
where MenuItems : View
  • menuItems: 提供一组菜单项(为视图)。
  • 每个菜单项可以是按钮、图标、文本等。

基本用法:在视图上添加 contextMenu #

可以将 contextMenu 修饰符加到任意可以使用修饰符的视图上,长按后即可显示上下文菜单。

代码示例 #

struct ContextMenuExample: View {
    var body: some View {
        Text("Long press me!")
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            .contextMenu {
                Button(action: {
                    print("Edit tapped")
                }) {
                    Label("Edit", systemImage: "pencil")
                }
                
                Button(action: {
                    print("Delete tapped")
                }) {
                    Label("Delete", systemImage: "trash")
                }
                
                Button(action: {
                    print("Info tapped")
                }) {
                    Label("Info", systemImage: "info.circle")
                }
            }
    }
}
  • 解释:
    • contextMenu 提供了 3 个按钮。
    • 每个按钮可以附加一个图标(Label 用于设置文本和图标)。
    • 使用长按触发上下文菜单。

运行效果:

  • 用户长按 Long press me! 的按钮时,会弹出一个有 “Edit”, “Delete” 和 “Info” 的快捷菜单。
  • 每个按钮点击触发其绑定的 action

进阶用法:条件是否启用 contextMenu #

可以通过条件逻辑控制 上下文菜单是否启用,例如:

代码示例 #

struct ConditionalContextMenuExample: View {
    @State private var isMenuEnabled = true

    var body: some View {
        VStack {
            Text("Long press me!")
                .padding()
                .background(Color.green)
                .foregroundColor(.white)
                .cornerRadius(8)
                .contextMenu(isEnabled: isMenuEnabled) { // 通过 `isEnabled` 控制菜单展示
                    Button("Option 1") {
                        print("Option 1 selected")
                    }
                }
            
            Toggle("Enable Context Menu", isOn: $isMenuEnabled)
                .padding()
        }
    }
}
  • 解释:
    • isEnabled 参数控制 contextMenu 是否激活。
    • 当切换开关 Enable Context Menu 为关闭时,长按不再触发上下文菜单。

动态生成菜单内容 #

有时菜单的内容需要动态生成(如基于状态显示不同选项)。

代码示例 #

struct DynamicContextMenuExample: View {
    @State private var selectedItem: String = "Item 1"

    var body: some View {
        Text("Selected: \(selectedItem)")
            .padding()
            .background(Color.orange)
            .foregroundColor(.white)
            .cornerRadius(8)
            .contextMenu {
                Button("Item 1") {
                    selectedItem = "Item 1"
                }
                Button("Item 2") {
                    selectedItem = "Item 2"
                }
                Button("Item 3") {
                    selectedItem = "Item 3"
                }
            }
    }
}
  • 解释:
    • 上下文菜单中的选项(Item 1Item 2Item 3)会动态修改 selectedItem
    • 菜单选择的结果会立即显示在界面上。

上下文菜单菜单项的自定义图标 #

可以使用系统图标或自定义的内容来搭配菜单项。

代码示例 #

struct CustomIconsContextMenuExample: View {
    var body: some View {
        Image(systemName: "folder.fill")
            .resizable()
            .frame(width: 100, height: 100)
            .foregroundColor(.blue)
            .contextMenu {
                Button(action: {
                    print("Open tapped")
                }) {
                    Label("Open", systemImage: "folder.open")
                }

                Button(action: {
                    print("Copy tapped")
                }) {
                    Label("Copy", systemImage: "doc.on.doc")
                }

                Button(action: {
                    print("Delete tapped")
                }) {
                    Label("Delete", systemImage: "trash")
                }
            }
    }
}
  • Label 定义一个菜单项,同时结合 systemImage 添加系统图标。
  • 图形和文字的排列自动由 SwiftUI 处理,无需额外调整。

结合状态和逻辑 #

你可以结合 @Binding 状态和其他 UI 组件的交互逻辑来控制 contextMenu 的触发。

代码示例 #

struct StateContextMenuExample: View {
    @State private var isFavorite = false

    var body: some View {
        VStack {
            Text(isFavorite ? "❤️ Favorite" : "💔 Not Favorite")
                .padding()
                .background(isFavorite ? Color.red : Color.gray)
                .cornerRadius(8)
                .contextMenu {
                    Button(action: {
                        isFavorite = true
                    }) {
                        Label("Mark as Favorite", systemImage: "heart.fill")
                    }
                    
                    Button(action: {
                        isFavorite = false
                    }) {
                        Label("Remove Favorite", systemImage: "heart.slash")
                    }
                }
        }
    }
}
  • 解释:
    • 用户可以通过上下文菜单来切换状态(如 FavoriteNot Favorite)。
    • 同时文本和背景颜色会基于状态动态更新。

适用场景 #

iOS(长按触发) #

  • 文件操作: 在文件或图片上提供打开、复制、分享、删除等操作。
  • 列表项目: 在行项目上长按触发快捷操作(如 编辑删除标记 等)。

macOS(右键点击触发) #

  • 右键菜单: 替代标准的 macOS 右键菜单,适用于更复杂的操作逻辑。

嵌套功能菜单 #

  • 结合 @State 和条件逻辑,根据状态生成或隐藏不同内容。既能支持简单静态菜单,也能实现动态交互。

注意事项 #

  1. 触发方式不同的设备行为:

    • iOS 上:用户长按触发 contextMenu
    • macOS 上:用户可以通过右键或双指点击触发。
  2. 多层级菜单不可用:

    • SwiftUI 的 contextMenu 不支持嵌套子菜单(即多层级结构)。如果需要复杂菜单,请考虑其他替代方案(如 Popover)。
  3. 适配 macOS:

    • 如果在 macOS 开发中需要更复杂的上下文菜单,可能需要结合 Commands 来实现。
  4. 动画效果:

    • contextMenu 的触发和 dismiss 都有内置的系统动画,无法自定义。

与其他控件的对比 #

功能contextMenuPopoverActionSheet
触发方式长按触发(iOS)或右键菜单(macOS)。点击触发(按钮常见配合)。iPhone 弹出底部菜单。
菜单内容仅支持简单静态或动态菜单。完全自定义的弹窗内容。通常用于一组选项操作。
演示方式与上下文相关的快捷菜单。自定义 UI,可以放置任何视图。适用于操作选项列表。
交互复杂度单级菜单,支持动态生成。可放置复杂交互逻辑组件,如 Form、Slider 等。只展示选项,不适合复杂交互。

总结 #

  • contextMenu 是 SwiftUI 提供的快捷上下文菜单功能,适合用于文件操作、列表交互、可选操作等场景。
  • 它可以动态生成内容,结合 @Binding 状态实现动态交互,但不支持多级子菜单。
  • 支持 iOS 和 macOS,能够很好地适应触控和桌面设备的交互体验。
本文共 1740 字,上次修改于 Jan 11, 2025