如何在SwiftUI Mac应用中实现类似AppKit的窗口绑定菜单项?
在SwiftUI macOS应用中实现基于响应者链的动态菜单项
核心思路
SwiftUI macOS应用的窗口内容由NSHostingController托管,我们可以通过自定义该控制器子类,实现目标Selector并控制菜单项的启用状态,再结合AppKit的NSMenuItem(将target设为nil),让系统通过响应者链自动匹配当前激活窗口的响应者,实现和AppKit一致的动态菜单项效果。
具体实现步骤
1. 自定义NSHostingController子类
创建托管SwiftUI视图的自定义控制器,实现目标方法并控制菜单项的启用逻辑:
import SwiftUI class PairedTextFieldHostingController<Content: View>: NSHostingController<Content> { // 用于控制成对TextField显示的状态 @objc var togglePairedState: Bool = false // 菜单项对应的Selector方法,必须标记@objc @objc func togglePairedTextField(_ sender: Any?) { togglePairedState.toggle() objectWillChange.send() // 通知SwiftUI视图更新状态 } // 重写该方法,控制菜单项的启用/禁用 override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(togglePairedTextField(_:)) { // 根据当前视图是否支持该功能返回结果 if let contentView = rootView as? PairedTextFieldSupporting { return contentView.supportsPairedTextField } return false } return super.canPerformAction(action, withSender: sender) } } // 协议:标记SwiftUI视图是否支持成对TextField功能 protocol PairedTextFieldSupporting { var supportsPairedTextField: Bool { get } }
2. 实现支持功能的SwiftUI视图
让目标视图遵循协议,并绑定控制器的状态:
struct ContentView: View, PairedTextFieldSupporting { // 绑定自定义HostingController的状态 @Binding var isPaired: Bool // 声明当前视图是否支持该功能 var supportsPairedTextField: Bool { // 可根据业务逻辑调整,比如存在输入控件时返回true true } var body: some View { VStack(spacing: 16) { if isPaired { TextField("输入框1", text: .constant("")) TextField("输入框2", text: .constant("")) } else { TextField("单个输入框", text: .constant("")) } } .padding() .frame(width: 300, height: 200) } }
3. 在AppDelegate中配置菜单和窗口
通过NSApplicationDelegate添加自定义菜单项,并让窗口使用自定义HostingController:
@main struct PairedTextFieldApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { Text("Loading...") // 占位,实际内容由AppDelegate设置 } } } class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // 给主菜单添加自定义菜单项(示例:添加到"编辑"菜单下) guard let mainMenu = NSApplication.shared.mainMenu, let editMenu = mainMenu.item(withTitle: "编辑")?.submenu else { return } let toggleItem = NSMenuItem( title: "切换成对TextField", action: #selector(PairedTextFieldHostingController.togglePairedTextField(_:)), keyEquivalent: "" ) toggleItem.target = nil // 关键:让系统遍历响应者链寻找实现方法的对象 editMenu.addItem(toggleItem) } // 创建窗口时使用自定义HostingController func application(_ application: NSApplication, didCreateWindowScene scene: NSWindowScene) { guard let window = scene.windows.first else { return } // 创建绑定控制器状态的SwiftUI视图 let contentView = ContentView(isPaired: .init(get: { (window.contentViewController as? PairedTextFieldHostingController<ContentView>)?.togglePairedState ?? false }, set: { newValue in (window.contentViewController as? PairedTextFieldHostingController<ContentView>)?.togglePairedState = newValue })) // 设置自定义HostingController为窗口内容控制器 let hostingController = PairedTextFieldHostingController(rootView: contentView) window.contentViewController = hostingController } }
关键说明
- 响应者链生效逻辑:将
NSMenuItem的target设为nil后,系统会从当前第一响应者开始遍历响应者链,找到第一个实现目标Selector的对象,此时菜单项启用并绑定到该对象;未找到则菜单项自动禁用。 - canPerformAction的作用:通过重写该方法,可精细控制菜单项启用状态,比如仅当视图确实支持成对TextField功能时返回
true。 - 多窗口适配:每个窗口拥有独立的
PairedTextFieldHostingController实例,系统会自动匹配当前激活窗口的响应者,菜单项状态随激活窗口的支持情况动态变化。
内容的提问来源于stack exchange,提问作者Duncan C
相关产品推荐
相关产品推荐

