Menu 菜单

Menu 视图在 iOS 和 macOS 上都有良好的支持,并且可以根据平台自动适配。在 macOS 上, Menu 自动呈现为下拉按钮。

Menu 视图主要用于显示按钮的弹出菜单,以便在用户点击按钮时显示一组选项。这些选项可以是用于执行特定操作的按钮,也可以是用于选择不同选项的选择器。Menu 视图的主要用途包括:

声明方法

// 1.当点击 B 的时候,就会展示 A
Menu {
  //这里是 Menu 菜单点开以后的内容 A
} label: {
	//这里是 Menu 未展开时的界面 B
}

// 2.未展开时显示个字符串,展开后显示3个按钮
Menu("Options") {
		Button("Order Now", action: placeOrder)
    Button("Adjust Order", action: adjustOrder)
    Button("Cancel", action: cancelOrder)
}
// 还可以将菜单放置在菜单中,所谓嵌套。这将导致二级菜单在最初会折叠起来,点击后再展开:
struct ContentView: View {
    var body: some View {
        Menu("Options") {
            Button("Order Now", action: placeOrder)
            Button("Adjust Order", action: adjustOrder)
            Menu("Advanced") {
                Button("Rename", action: rename)
                Button("Delay", action: delay)
            }
            Button("Cancel", action: cancelOrder)
        }
    }
    func placeOrder() { }
    func adjustOrder() { }
    func rename() { }
    func delay() { }
    func cancelOrder() { }
}

1. 按钮触发

您可以将Menu视图与按钮一起使用,以便在用户点击按钮时显示菜单。这可以通过在按钮上使用Menu修饰符来实现。当用户点击按钮时,Menu视图将显示相关的选项供用户选择。

Button(action: {
    // Your action
}) {
    Text("Show Menu")
        .menu {
            // Menu content
        }
}

2. 手势触发

除了按钮触发,您还可以使用手势来触发Menu视图。例如,您可以使用长按手势来触发Menu视图的显示。

Text("Long Press Me")
    .contextMenu {
        // Menu content
    }
    .onTapGesture {
        // Your action
    }

3. 条件触发

您可以根据特定条件来触发Menu视图的显示。这可以通过使用 @State@Binding 来控制Menu视图的显示与隐藏。

@State private var isMenuVisible = false

Button(action: {
    isMenuVisible.toggle()
}) {
    Text("Show Menu")
}
.menu(isPresented: $isMenuVisible) {
    // Menu content
}

4. 同时支持点击和长按展开

在 iOS 15 及更高版本中,菜单还可以设置一个 primary action 。设置完成后,当点击菜单按钮就会执行 primary action;如果不是点击,是长按住就可以展现完整的菜单选项。

struct ContentView: View {
    var body: some View {
        Menu("Options") {
            Button("Order Now", action: placeOrder)
            Button("Adjust Order", action: adjustOrder)
            Button("Cancel", action: cancelOrder)
        } primaryAction: {
            justDoIt()
        }
    }

    func justDoIt() {
        print("Button was tapped")
    }

    func placeOrder() { }
    func adjustOrder() { }
    func cancelOrder() { }
}

contextMenu 上下文菜单

SwiftUI 为我们提供了 ContextMenu 修饰符,用于在应用程序中创建弹出菜单。在 iOS 中,这通常是通过长按触发的;在较旧的 iPhone 上,用户可以通过用力按压某物来触发 3D Touch;但它的工作原理与 macOS 上的右键单击相同 - 这是一个灵活的 API。

注意事项

以下提示,能在你使用上下文菜单时,帮助确保为用户提供最佳体验:

  1. 如果要使用【上下文菜单】,请在大部份地方都支持它们。否则当按住某些元素,却发现什么也没有发生,会令人沮丧。
  2. 让你的选项列表尽可能简短,目标是三个或更少。
  3. 不要重复用户已经在用户界面其他地方看到的选项。
  4. 请记住,上下文菜单本质上是隐藏的,因此在隐藏上下文菜单中的重要操作之前请三思。

调用方法

上下文菜单是由一组按钮构建的,每个按钮都有自己的操作、文本和图标。文本和图标可以直接在按钮内部提供,因为 SwiftUI 将提供隐式 HStack 以确保它们符合系统标准外观和感觉。我们可以构建一个简单的上下文菜单来控制视图的背景颜色,如下所示:

Text("Change Color")
		.padding()
		.contextMenu {
				Button("Red") { backgroundColor = .red }
				Button("Green") { backgroundColor = .green }
				Button("Blue") { backgroundColor = .blue }
		}

//像 TabView 一样,上下文菜单中的每个项目都可以使用 Label 视图附加文本和图像
Text("Options")
    .contextMenu {
        Button {
            print("Change country setting")
        } label: {
            Label("Choose Country", systemImage: "globe")
        }

        Button {
            print("Enable geolocation")
        } label: {
            Label("Detect Location", systemImage: "location.circle")
        }
    }

样式设置

contextMenu 有个问题:为了保持应用程序之间的界面统一,iOS 会将每个图像渲染为纯色,并保留不透明度。这使得许多图片毫无用处:

// 例如您有三张照片用在了 contextMenu 中,则所有三张照片都会呈现为纯黑色正方形,因为所有颜色都被删除了
// 因此,你应该使用线条图标作为替代,例如 Apple 的 SF Symbols
Button("Red", systemImage: "checkmark.circle.fill") {
    backgroundColor = .red
}

//另外,如果使用 foregroundStyle() 修饰符,会发现无效(iOS 希望菜单看起来统一,所以尝试随机给它们着色是行不通的)
.foregroundStyle(.red)

//如果您确实希望该项目显示为红色(您应该知道这意味着破坏性),应该使用按钮角色去声明
Button("Red", systemImage: "checkmark.circle.fill", role: .destructive) {
    backgroundColor = .red
}

应用:长按删除列表项

contextMenu 指的是通过长按元素弹出的菜单。

//例如:

//首先定义删除方法,加到body外面
func delete(item restaurant: Restaurant){
    //闭包中的 $0 代表 restaurants 数组中的一个个元素
    if let index = self.restaurants.firstIndex(where: { $0.id == restaurant.id }){
        self.restaurants.remove(at: index)
    }
}

//设置
var body: some View {

	ForEach(restaurants) { restaurant in
	                
			BasicImageRow(restaurant: restaurant)
			//跟 onDelete 處理器不同的是,這個 contextMenu 並不会提供所选餐厅的索引。要設置它還需要多進行一點工作。
	    .contextMenu {
			    Button(action: {
	            delete(item: restaurant)
	        }){
	            HStack{
		            Text("Delete")
	              Image(systemName: "trash")
	            }
	        }
	     }
	}
}

应用:长按显示 Menu

例子:除了按钮触发,您还可以使用手势来触发Menu视图。例如,您可以使用长按手势来触发Menu视图的显示。