macOS SwiftUI实现Finder风格Option键切换菜单内容
解决SwiftUI中复刻Finder分组按钮的Option键判定问题
你之前的核心问题是误用了NSEvent.modifierFlags静态属性——它返回的是事件循环中当前的修饰键状态,但在SwiftUI手势闭包执行时,点击事件可能已经被处理完毕,所以拿到的状态不准确。正确的做法是获取当前触发点击的事件的修饰键,也就是用NSApp.currentEvent?.modifierFlags。
以下是两种可行的实现方案:
方案1:纯SwiftUI实现
直接在Button的action闭包里检查当前事件的修饰键,不需要额外手势(Button本身的点击事件已经能正确关联到当前Event):
import SwiftUI struct FinderGroupButtonView: View { var body: some View { Button(action: handleButtonTap) { Text("分组") .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(.controlBackgroundColor)) .cornerRadius(6) } } private func handleButtonTap() { let isOptionPressed = NSApp.currentEvent?.modifierFlags.contains(.option) ?? false if isOptionPressed { showSortMenu() } else { showGroupMenu() } } private func showGroupMenu() { let groupMenu = NSMenu(title: "分组方式") groupMenu.addItem(NSMenuItem(title: "无", action: #selector(handleGroupAction), keyEquivalent: "")) groupMenu.addItem(NSMenuItem(title: "名称", action: #selector(handleGroupAction), keyEquivalent: "")) groupMenu.addItem(NSMenuItem(title: "日期", action: #selector(handleGroupAction), keyEquivalent: "")) // 获取按钮在屏幕上的位置,弹出菜单 if let buttonWindow = NSApp.keyWindow, let buttonFrame = buttonWindow.contentView?.convert(CGRect(x: 0, y: 0, width: 80, height: 30), to: nil) { let popUpPoint = NSPoint(x: buttonFrame.minX, y: buttonFrame.minY - groupMenu.menuHeight) groupMenu.popUp(positioning: nil, at: popUpPoint, in: buttonWindow.contentView) } } private func showSortMenu() { let sortMenu = NSMenu(title: "排序方式") sortMenu.addItem(NSMenuItem(title: "名称", action: #selector(handleSortAction), keyEquivalent: "")) sortMenu.addItem(NSMenuItem(title: "日期修改", action: #selector(handleSortAction), keyEquivalent: "")) sortMenu.addItem(NSMenuItem(title: "大小", action: #selector(handleSortAction), keyEquivalent: "")) if let buttonWindow = NSApp.keyWindow, let buttonFrame = buttonWindow.contentView?.convert(CGRect(x: 0, y: 0, width: 80, height: 30), to: nil) { let popUpPoint = NSPoint(x: buttonFrame.minX, y: buttonFrame.minY - sortMenu.menuHeight) sortMenu.popUp(positioning: nil, at: popUpPoint, in: buttonWindow.contentView) } } @objc private func handleGroupAction(_ sender: NSMenuItem) { print("选择分组:\(sender.title)") } @objc private func handleSortAction(_ sender: NSMenuItem) { print("选择排序:\(sender.title)") } }
方案2:原生NSButton包裹(更可靠)
如果纯SwiftUI的方式偶尔出现事件获取不稳定的情况,可以用NSViewRepresentable包裹原生NSButton,原生AppKit控件对修饰键的处理更精准:
import SwiftUI struct FinderStyleButton: NSViewRepresentable { let title: String let onTap: (Bool) -> Void // Bool表示是否按住Option键 func makeNSView(context: Context) -> NSButton { let button = NSButton(title: title, target: context.coordinator, action: #selector(Coordinator.onButtonTap(_:))) button.bezelStyle = .rounded button.setButtonType(.momentaryLight) return button } func updateNSView(_ nsView: NSButton, context: Context) { nsView.title = title } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject { let parent: FinderStyleButton init(parent: FinderStyleButton) { self.parent = parent } @objc func onButtonTap(_ sender: NSButton) { let isOptionPressed = NSApp.currentEvent?.modifierFlags.contains(.option) ?? false parent.onTap(isOptionPressed) } } } // 使用示例 struct ContentView: View { var body: some View { FinderStyleButton(title: "分组") { isOptionPressed in if isOptionPressed { self.showSortMenu() } else { self.showGroupMenu() } } } // 这里的showGroupMenu和showSortMenu实现同方案1 private func showGroupMenu() { /* ... */ } private func showSortMenu() { /* ... */ } }
关键注意点
- 永远用
NSApp.currentEvent?.modifierFlags替代NSEvent.modifierFlags——前者是当前触发点击的事件的修饰键状态,后者是全局的、可能过时的状态 - 不要依赖
onTapGesture(modifiers: .option),这种写法是要求必须按住Option才会触发手势,而你需要的是不管有没有按住都触发,然后根据状态分支处理 - 弹出菜单时,要正确计算按钮在屏幕上的位置,避免菜单出现在错误区域
内容的提问来源于stack exchange,提问作者user09274385
相关产品推荐
相关产品推荐

