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

NSStatusBar弹出框嵌套子弹出框时TextField异常及崩溃问题

问题描述

开发了一款在NSStatusBar弹出框中显示时区的Mac应用,原本运行正常,但在原弹出框内通过按钮添加第二个弹出框并加入TextField后出现问题:

  • TextField显示已获得焦点但无法输入
  • 反复切换第二个弹出框会导致崩溃

报错信息如下:

...as the first responder for window <_NSPopoverWindow: 0x7fb9c1807bb0>, but it is in a different window ((null))! This would eventually crash when the view is freed. The first responder will be set to nil.

已提取NSStatusBar配置和两个弹出框视图制作了可复现测试实例,其中使用EventMonitor捕获点击外部区域来关闭弹出框。开发环境为macOS 12.6.2,目标版本设置为12.0。

可复现测试代码

AppDelegate.swift

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    
    var popover: NSPopover!
    var statusBarItem: NSStatusItem!
    var eventMonitor: EventMonitor?
    var contentView = ContentView()
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the popover
        let popover = NSPopover()
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
        
        // Create the status item
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        
        if let button = self.statusBarItem.button {
            button.image = NSImage(named: "Icon")
            button.action = #selector(togglePopover(_:))
        }
        
        eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [unowned self] event in
            if self.popover.isShown {
                closePopover(event)
            }
        }
        eventMonitor?.start()
        
        NSApp.activate(ignoringOtherApps: true)
    }
    
    @objc func togglePopover(_ sender: AnyObject?) {
        if popover.isShown {
            closePopover(sender)
        } else {
            showPopover(sender)
        }
    }
    
    func showPopover(_ sender: AnyObject?) {
        if let button = statusBarItem.button {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            self.popover.contentViewController?.view.window?.becomeKey()
        }
        eventMonitor?.start()
    }
    
    func closePopover(_ sender: AnyObject?) {
        popover.performClose(sender)
        eventMonitor?.stop()
    }
}

open class EventMonitor {
    
    fileprivate var monitor: AnyObject?
    fileprivate let mask: NSEvent.EventTypeMask
    fileprivate let handler: (NSEvent?) -> ()
    
    public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> ()) {
        self.mask = mask
        self.handler = handler
    }
    
    deinit {
        stop()
    }
    
    open func start() {
        monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as AnyObject?
    }
    
    open func stop() {
        if monitor != nil {
            NSEvent.removeMonitor(monitor!)
            monitor = nil
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    
    @State private var showingPopover = false
    @State var value: String = "Initial Value"
    
    var body: some View {
        VStack {
            Button {
                showingPopover = true
            } label: {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
            }
            .popover(isPresented: $showingPopover) {
                EditView(value: $value)
            }
            Text("Hello, world!")
        }
        .padding()
    }
}

struct EditView: View {
    @Binding var value:  String
    
    var body: some View {
        VStack {
            TextField("Location ", text: $value)    // the location as a string
                .multilineTextAlignment(.center)
                .lineLimit(1)
                .onSubmit {
                    print("value submit \(value)")
                }
        }
        .padding()
        .frame(width:200, height: 50)
    }
}
解决方案

问题根源在于全局事件监视器的逻辑缺陷和弹出框焦点管理冲突:全局监视器仅监听主弹出框状态,会错误干预子弹出框生命周期;手动设置主弹出框becomeKey()会破坏SwiftUI弹出框的响应者链,导致TextField无法正常输入。

1. 修复全局事件监视器逻辑

修改事件监视器的判断逻辑,同时检测点击是否在主/子弹出框内部,避免误关闭或残留响应者:

// 在AppDelegate的applicationDidFinishLaunching中替换eventMonitor初始化代码
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [unowned self] event in
    // 检查点击是否在主弹出框内
    let isInMainPopover = self.popover.isShown && self.popover.contentViewController?.view.window?.contains(event?.locationInWindow ?? .zero) ?? false
    // 检查点击是否在子弹出框内
    let isInSubPopover = NSApplication.shared.windows.contains { window in
        guard let popoverWindow = window as? NSPopoverWindow else { return false }
        return popoverWindow.isVisible && popoverWindow.contains(event?.locationInWindow ?? .zero)
    }
    
    // 仅当点击在所有弹出框外部时,关闭弹出框
    if !isInMainPopover && !isInSubPopover {
        if self.popover.isShown {
            closePopover(event)
        }
        self.contentView.showingPopover = false
    }
}

同时调整弹出框关闭时的监视器生命周期管理:

// 修改closePopover方法
func closePopover(_ sender: AnyObject?) {
    popover.performClose(sender)
    // 移除原有的eventMonitor?.stop()
}

// 修改togglePopover方法
@objc func togglePopover(_ sender: AnyObject?) {
    if popover.isShown {
        closePopover(sender)
        eventMonitor?.stop()
    } else {
        showPopover(sender)
        eventMonitor?.start()
    }
}

2. 移除强制焦点设置

删除showPopover方法中手动设置焦点的代码,交由SwiftUI自动管理弹出框焦点:

func showPopover(_ sender: AnyObject?) {
    if let button = statusBarItem.button {
        popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        // 移除该行:self.popover.contentViewController?.view.window?.becomeKey()
    }
    eventMonitor?.start()
}
验证效果
  • TextField可正常获取焦点并输入内容
  • 反复打开/关闭子弹出框不会触发崩溃
  • 点击外部区域可正确关闭所有弹出框

内容的提问来源于stack exchange,提问作者paulrbarnard

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 07:15:27