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

React Native开发macOS应用:无法监听应用切换事件该如何解决?

问题描述

我正在使用React Native开发一款macOS应用,希望检测用户切换应用的时机并捕获截图。编写了如下测试代码:

import Foundation
import AppKit

@objc(ScreenShotModule)
public class ScreenShotModule: NSObject, RCTBridgeModule {
  
  public static func moduleName() -> String! {
    return "ScreenShotModule"
  }
  
  var count:Int = 0
  
  override init() {
    super.init()
    print("called init")
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(appDidBecomeActive),
      name: NSWorkspace.didActivateApplicationNotification,
      object: nil)
  }
  
  @objc func appDidBecomeActive(notification: NSNotification) {
    print("got notified")
    let displayId = CGMainDisplayID()
    guard let image = CGDisplayCreateImage(displayId) else { return }
        
    let bitmapRep = NSBitmapImageRep(cgImage: image)
    guard let imageData = bitmapRep.representation(using: .png, properties: [:]) else { return }
        
    let fileManager = FileManager.default
    let directoryURLs = fileManager.urls(for: .picturesDirectory, in: .userDomainMask)
        
    if let documentDirectory = directoryURLs.first {
      let filePath = documentDirectory.appendingPathComponent("screenshot_\(count).png")
      do {
        try imageData.write(to: filePath)
        count += 1
        print("Screenshot saved to: \(filePath)")
      } catch {
        print("Error saving screenshot: \(error)")
      }
    }
  }
  
  @objc func triggerAppDidBecomeActive() {
      let dummyNotification = NSNotification(name: NSNotification.Name(""), object: nil)
      self.appDidBecomeActive(notification: dummyNotification)
  }
  
  @objc func postNotification() {
    print("posted!")
    NotificationCenter.default.post(name: NSWorkspace.didActivateApplicationNotification, object: "foo",
                                    userInfo: ["key": "Value"])
  }
  
  @objc public static func requiresMainQueueSetup() -> Bool {
    return true
  }
}

手动调用triggerAppDidBecomeActive方法或通过postNotification发送模拟通知时,都能正常完成截图操作,但实际运行应用过程中切换其他应用时,无法触发NSWorkspace.didActivateApplicationNotification通知。已尝试开启或关闭应用沙箱,也使用过DistributedNotificationCenter,均无效果。请问该如何检测用户切换应用的事件?


解决方案

1. 切换到NSWorkspace专属通知中心监听

NSWorkspace.didActivateApplicationNotification不属于系统默认的NotificationCenter.default,而是归属NSWorkspace自己的通知中心。修改观察者注册代码:

override init() {
    super.init()
    print("called init")
    // 使用NSWorkspace的通知中心注册观察者
    NSWorkspace.shared.notificationCenter.addObserver(
        self,
        selector: #selector(appDidBecomeActive),
        name: NSWorkspace.didActivateApplicationNotification,
        object: nil)
}

2. 沙箱环境下的权限配置

如果开启了应用沙箱:

  • 必须在Info.plist中添加屏幕录制权限说明:
    <key>NSScreenRecordingUsageDescription</key>
    <string>需要屏幕录制权限以捕获切换应用时的截图</string>
    
  • 访问Pictures目录需要开启User Selected File或Downloads Folder权限,或者直接将截图存储到应用容器内的目录(比如Application Support),避免权限问题。

3. 备选方案:用CGEventTap监听切换事件

如果通知监听仍不生效,可以通过监听键盘事件(比如Cmd+Tab)来捕获应用切换动作,这种方式需要应用获得辅助功能权限(系统偏好设置 → 安全性与隐私 → 隐私 → 辅助功能,添加你的应用):

override init() {
    super.init()
    print("called init")
    setupEventTap()
}

private func setupEventTap() {
    guard let eventTap = CGEvent.tapCreate(
        tap: .cgSessionEventTap,
        place: .headInsertEventTap,
        options: .listenOnly,
        eventsOfInterest: CGEventMask(1 << CGEventType.keyDown.rawValue),
        callback: { _, type, event, refcon in
            guard let refcon = refcon else { return nil }
            let module = Unmanaged<ScreenShotModule>.fromOpaque(refcon).takeUnretainedValue()
            
            // 监听Cmd+Tab组合键
            let tabKeyCode = 48
            if event.getIntegerValueField(.keyboardEventKeycode) == tabKeyCode,
               event.flags.contains(.maskCommand) {
                DispatchQueue.main.async {
                    module.captureScreenshot()
                }
            }
            return event
        },
        userInfo: Unmanaged.passUnretained(self).toOpaque()
    ) else {
        print("无法创建事件监听")
        return
    }
    
    let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
    CGEvent.tapEnable(tap: eventTap, enable: true)
}

// 抽离截图逻辑为独立方法
@objc private func captureScreenshot() {
    print("开始截图")
    let displayId = CGMainDisplayID()
    guard let image = CGDisplayCreateImage(displayId) else { return }
        
    let bitmapRep = NSBitmapImageRep(cgImage: image)
    guard let imageData = bitmapRep.representation(using: .png, properties: [:]) else { return }
        
    let fileManager = FileManager.default
    let directoryURLs = fileManager.urls(for: .picturesDirectory, in: .userDomainMask)
        
    if let documentDirectory = directoryURLs.first {
      let filePath = documentDirectory.appendingPathComponent("screenshot_\(count).png")
      do {
        try imageData.write(to: filePath)
        count += 1
        print("Screenshot saved to: \(filePath)")
      } catch {
        print("Error saving screenshot: \(error)")
      }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 22:27:03