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

Swift模拟媒体键代码脚本运行正常但编译后失效问题排查

编译后的Swift程序无法模拟媒体键(脚本运行正常)

问题描述

我参考Stack Overflow的回答改写了模拟媒体键的Swift代码,精简版本如下:

import Quartz

func doKey(down: Bool) {
  let volumeDown = 1
  let flags = NSEvent.ModifierFlags(rawValue: (down ? 0xa00 : 0xb00))
  let data1 = Int((volumeDown << 16) | (down ? 0xa00 : 0xb00))

  let ev = NSEvent.otherEvent(
    with: NSEvent.EventType.systemDefined,
    location: NSPoint(x: 0, y: 0),
    modifierFlags: flags,
    timestamp: 0,
    windowNumber: 0,
    context: nil,
    subtype: 8,
    data1: data1,
    data2: -1
  )

  let cev = ev?.cgEvent
  cev?.post(tap: CGEventTapLocation.cgSessionEventTap)
}

doKey(down: true)
doKey(down: false)

这段代码以脚本形式通过swift ./script-name.swift运行时完全正常,但编译为独立程序后(无论是否公证)均无法工作,终端无错误输出,退出码为0。我已尝试将编译后的程序添加到系统偏好设置的无障碍权限列表中,但问题依旧。

原因分析

  1. 权限上下文差异:Swift脚本运行在终端进程的上下文里,继承了终端的无障碍权限;而编译后的独立程序是单独的进程,需要自己被授予无障碍权限,且系统首次请求时可能误将终端列为权限主体,而非程序本身。
  2. 硬编码值的环境兼容性:代码中直接使用硬编码的十六进制值(如0xa00、0xb00)作为修饰符标志,这些值依赖特定的运行环境,在编译后的独立程序中可能无法正确映射到系统事件的修饰符。
  3. 事件循环缺失:脚本运行时依赖Swift的即时执行环境自动处理事件循环,但编译后的命令行程序执行完毕后立即退出,导致CGEvent.post投递的事件还未被系统处理就终止了进程。

解决方案

1. 确保程序获得正确的无障碍权限

  • 打开系统设置 > 隐私与安全性 > 无障碍
  • 点击列表下方的+按钮,找到编译后的程序(如./your-compiled-program)并添加
  • 勾选程序对应的权限,然后重启程序(必要时重启系统确保权限生效)

2. 修正事件构造逻辑(替换硬编码值)

使用系统提供的枚举值代替硬编码的十六进制,确保事件修饰符在不同环境下的一致性,同时修正data1的构造逻辑以正确区分按键按下/松开状态:

import Quartz
import Foundation

func simulateVolumeDown() {
    let volumeDownKeyCode = 1 // 音量减小对应的键码
    let baseModifierFlags = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.option.rawValue
    
    // 构造按键按下事件
    let downModifierFlags = NSEvent.ModifierFlags(rawValue: baseModifierFlags)
    let downData1 = (volumeDownKeyCode << 16) | (baseModifierFlags & 0xFFFF) | (0x1 << 24) // 0x1<<24标记按键按下
    
    if let downEvent = NSEvent.otherEvent(with: .systemDefined,
                                         location: .zero,
                                         modifierFlags: downModifierFlags,
                                         timestamp: 0,
                                         windowNumber: 0,
                                         context: nil,
                                         subtype: 8, // 媒体事件固定subtype
                                         data1: downData1,
                                         data2: -1) {
        downEvent.cgEvent?.post(tap: .cgSessionEventTap)
    }
    
    // 构造按键松开事件
    let upModifierFlags = NSEvent.ModifierFlags(rawValue: baseModifierFlags | 0x800) // 0x800标记按键松开
    let upData1 = (volumeDownKeyCode << 16) | ((baseModifierFlags | 0x800) & 0xFFFF)
    
    if let upEvent = NSEvent.otherEvent(with: .systemDefined,
                                       location: .zero,
                                       modifierFlags: upModifierFlags,
                                       timestamp: 0,
                                       windowNumber: 0,
                                       context: nil,
                                       subtype: 8,
                                       data1: upData1,
                                       data2: -1) {
        upEvent.cgEvent?.post(tap: .cgSessionEventTap)
    }
}

// 执行模拟
simulateVolumeDown()

// 启动RunLoop,给系统足够时间处理事件
RunLoop.main.run(until: Date().addingTimeInterval(0.1))

3. 添加事件循环等待

编译后的命令行程序需要手动启动RunLoop并短暂运行,确保事件有足够时间被系统接收和处理,避免进程提前退出。代码中RunLoop.main.run(until: Date().addingTimeInterval(0.1))会让程序等待0.1秒后退出,足够完成事件投递。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 07:33:55