You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 03:57:45