NSEvent 是 macOS AppKit 框架中用于表示用户输入事件(例如鼠标、键盘、触控板等)的核心类。几乎所有用户交互行为都会被封装为一个 NSEvent 对象,并由系统传递给 app 的响应链。
NSEvent 代表系统级事件对象,可以:
1、监听键盘按键(按下、松开)。
2、监听鼠标移动、点击、滚动。
3、监听触控板手势(两指滚动、捏合缩放等)。
4、响应系统事件(如 app 激活、窗口焦点变化)。
常用属性
1、通用属性(所有事件都有)
1、type:NSEvent.EventType类型,事件类型,如 .keyDown。
2、timestamp:TimeInterval类型,事件发生的时间戳。
3、locationInWindow:NSPoint类型,鼠标/触摸事件相对窗口的位置。
4、window / windowNumber:NSWindow?类型,触发事件的窗口。
5、modifierFlags:NSEvent.ModifierFlags类型,当前按下的修饰键(如 ⌘ ⌥ ⇧)。
可组合使用:
event.modifierFlags.contains(.command)
event.modifierFlags.contains(.shift)
event.modifierFlags.intersection([.command, .option])
全部修饰符包括:.command、.option、.shift、.control、.capsLock和.function(Fn)。
6、context:NSGraphicsContext?类型,图形上下文(通常用不到)。
7、eventRef:Unmanaged<CGEvent>?类型,原始低层事件(用于精细处理)。
8、cgEvent:CGEvent?类型,Core Graphics 层级事件对象(更底层,macOS 10.6+)。
9、appKitDefined / .systemDefined / .applicationDefined / .periodic:系统内部定义或应用自定义事件。
2、键盘事件(keyDown / keyUp)
1、keyCode:UInt16类型,原始硬件键码(设备级别)。
2、characters:String?类型,按下的键所产生的字符,受修饰键影响(如 shift)。
3、charactersIgnoringModifiers:String?类型,按下的键对应的字符,忽略修饰键(除 Shift)。
if event.modifierFlags.contains(.command),event.charactersIgnoringModifiers == "v" { }
这表示监听Command + v快捷键。
4、isARepeat / isKeyRepeat:Bool类型,是否是按住键后的重复触发。
5、unicodeScalarValues:[UnicodeScalar]?类型,更底层的字符编码值(macOS 13+)。
6、hasMarkedText:Bool类型,是否为输入法中未完成的输入(如拼音)。
7、markedRange:NSRange类型,当前输入法标记文字的范围。
8、selectedRange:NSRange类型,当前选中的文字范围。
3、鼠标事件(mouseDown / mouseDragged / mouseUp)
1、buttonNumber:Int类型,鼠标按下的是哪个按钮(0 左键,1 右键,2 中键)。
2、clickCount:Int类型,连续点击次数(1 单击,2 双击)。
3、pressure:Float类型,压力值(如 Force Touch,0~1)。
4、trackingNumber:Int类型,用于追踪区域鼠标。
5、eventNumber:Int类型,系统内部的事件序号。
6、mouseLocation:NSPoint类型,当前鼠标在屏幕坐标下的位置。
7、trackingArea:NSTrackingArea?类型,所属的跟踪区域(通常用于 mouseEntered / mouseExited)。
8、associatedEventsMask:NSEvent.EventTypeMask类型,鼠标拖拽中可以附带哪些事件类型。
9、buttonMask:Int类型,当前按下的所有鼠标按钮的组合(macOS 10.10+)。
10、cursorUpdate:鼠标光标区域变更(例如进入链接)。
4、滚轮事件(scrollWheel)
1、deltaX / deltaY / deltaZ:CGFloat类型,滚动的位移(横向/纵向/深度)。
2、hasPreciseScrollingDeltas:Bool类型,是否是精确滚动(如触控板)。
3、scrollingDeltaX/Y:CGFloat类型,实际滚动增量。
4、isDirectionInvertedFromDevice:Bool类型,滚动方向是否反转(与系统设置有关)。
5、触控板、触摸事件
这些只有在处理 gesture 或 touch 事件时才有意义:
1、touches(matching:in:):Set<NSTouch>类型,获取与事件关联的触摸信息。
2、allTouches():Set<NSTouch>类型,获取所有触摸对象。
3、magnification:CGFloat类型,捏合手势缩放值(用于 magnify 事件)。
4、rotation:CGFloat类型,旋转手势的角度(用于 rotate 事件)。
5、phase:NSEvent.Phase类型,当前触控阶段(began、moved、ended)。
6、momentumPhase:NSEvent.Phase类型,动量阶段,如滚动结束的惯性部分。
7、.beginGesture / .endGesture:手势开始/结束。
8、smartMagnify:智能缩放手势(双指双击)。
9、.touchesBegan / .touchesMoved / .touchesEnded / .touchesCancelled:多点触控事件(Trackpad)。
6、Apple Pencil / 手写板支持(较少用)
1、tilt:NSEvent.Tilt类型,输入设备的倾斜角度(iPad 手写板相关)。
2、vendorDefined:Data类型,自定义厂商事件数据。
3、.tabletPoint / .tabletProximity:手写板输入,不过多介绍。
7、Game Controller / 特殊设备输入(很少用)
1、subtype:Int类型,事件子类型(例如遥控器输入)。
2、data1 / data2:Int类型,原始设备数据字段。
类级别属性
NSEvent 作为一个类,提供了若干类级别(Type-Level)的属性与方法,用于访问系统当前输入状态、注册全局事件监听器、获取鼠标状态等。
1、鼠标位置与状态
1、mouseLocation:NSPoint类型,当前鼠标在全局屏幕坐标系中的位置(左下为原点)。
print("\(NSEvent.mouseLocation)") // (917.01171875, 875.40625)
2、pressedMouseButtons:Int类型,当前按下的鼠标按钮组合(按位编码)。
2、当前按键状态
1、modifierFlags:NSEvent.ModifierFlags类型,当前所有修饰键(⌘ ⌥ ⇧ ⌃等)的状态。
let flags = NSEvent.modifierFlags
if flags.contains(.command) && flags.contains(.option) {
print("⌘ 和 ⌥ 被按下了")
}
这里是读取实时的物理修饰符,⌘ 和 ⌥ 被同时按下后,执行该代码才会触发if语句。
2、keyRepeatDelay:TimeInterval类型,键盘按键初次长按的延迟时间。
3、keyRepeatInterval:TimeInterval类型,按键持续按下时重复的时间间隔。
3、全局事件监控(全局监听鼠标、键盘、滚轮等)
1、addGlobalMonitorForEvents(matching:handler:):Any?类型,监听所有应用中的事件(无权阻止)要在前台 App 中使用。
NSEvent.addGlobalMonitorForEvents(matching: .scrollWheel) { event in
print("滚轮 deltaY: \(event.scrollingDeltaY)")
}
2、addLocalMonitorForEvents(matching:handler:):Any?类型,监听当前应用中的事件(可修改、阻止)。
3、removeMonitor(_:):Void类型,移除上面两个 monitor 中返回的对象。
4、鼠标按钮工具方法
1、doubleClickInterval:TimeInterval类型,系统双击的最大时间间隔。
2、mouseCoalescingEnabled:Bool类型,是否启用鼠标事件合并(减少事件数量)。
3、setMouseCoalescingEnabled(_:):Void类型,设置上面这个开关。
5、系统输入管理器相关(较少使用)
1、isMouseCoalescingEnabled:Bool类型,是否合并鼠标事件(macOS 13+ 改名为此)。
2、startPeriodicEvents(afterDelay:withPeriod:):Void类型,启动周期性事件(发送 .periodic)。
3、stopPeriodicEvents():Void类型,停止周期性事件。
使用场景
1、响应事件(事件处理)
继承 NSView 或 NSWindow 时处理输入事件,比如在自定义视图、窗口或控制器中响应鼠标、键盘等:
override func mouseDown(with event: NSEvent) {
if event.clickCount == 2 {
print("双击了鼠标")
}
print("点击位置:\(event.locationInWindow)")
}
override func keyDown(with event: NSEvent) {
print("按下键盘:\(event.characters ?? "")")
}
常用的事件处理方法:
1、mouseDown(with:):鼠标按下。
2、mouseDragged(with:):鼠标拖动。
3、mouseUp(with:):鼠标释放。
4、scrollWheel(with:):鼠标滚动。
5、keyDown(with:):键盘按下。
6、keyUp(with:):键盘松开。
7、scrollWheel(with:):鼠标滚轮。
必须放在 NSResponder 子类(如 NSView、NSWindow)中,属于响应链一部分(用户点击后自动传递),可以覆盖并拦截(不调用 super 就不会继续传递)。
2、全局/本地监听事件(事件监控)
适合用于状态栏应用、后台监控工具、快捷键等。
示例,监听当前应用的键盘按键:
let monitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { event in
print("本地监听到按键:\(event.characters ?? "")")
return event // 或 return nil 来阻止事件
}
可以拦截事件(返回 nil 就不会继续传递),运行在当前 app 内部。
示例,监听所有应用的键盘按键:
let monitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { event in
print("全局监听键盘:\(event)")
}
无法拦截(返回值无效),可监听所有 app 的事件(前提是开启辅助功能权限)。适合快捷键监听器、截图工具、辅助工具等。
事件方法和NSEvent属性
mouseDown(with:)等方法可以响应鼠标点击行为,这些方法都接收一个NSEvent实例。
override func mouseDown(with event: NSEvent) { // mouseDown()是事件方法
if event.clickCount == 2 { // clickCount是NSEvent属性
print("双击了鼠标")
}
print("点击位置:\(event.locationInWindow)")
}
NSEvent的属性指的是事件对象本身所包含的数据,比如鼠标点击的位置等信息。
事件传递机制
NSEvent 被系统发送到视图树上的响应链中,由最先响应的 NSResponder(比如 NSView, NSWindow, NSViewController)依次处理。
比如:
1、用户点击窗口 → 系统创建 NSEvent。
2、发送到最前面的窗口(NSWindow)。
3、传递到窗口的 contentView。
4、传到具体控件(如 NSButton)。
作用域与行为范围
NSEvent.addLocalMonitorForEvents(…) 是应用级的事件监听器,
无论在 App 的哪里注册(子视图中、控制器中、单例中),它都能监听整个 App 内部的键盘事件,前提是这个 App 正在“激活并响应键盘”。
例如,在子视图中注册:
struct MyView: View {
init() {
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
print("收到事件")
return event
}
}
var body: some View {
Text("Hello")
}
}
这段代码也能监听整个 App 内的键盘事件,不是只监听 MyView。
但是,addLocalMonitorForEvents只有在当前App为前台、主窗口时,才会响应。如果App在后台,则不会监听。
如果想要在后台也实现监听功能,就需要addGlobalMonitorForEvents,但它不允许拦截,只能监听(而且需要辅助权限)。
addLocalMonitorForEvents,作用于整个App,可以拦截事件,不需要权限。
addGlobalMonitorForEvents作用于所有App,不可以拦截事件,需要“辅助功能”权限。
因此在使用addLocalMonitorForEvents时,通常建议封装到单例中统一管理,避免多处监听,如果在多个视图中重复注册,就可能导致监听器混乱或重复触发。
若要监听全局(系统级)按键行为,请使用 addGlobalMonitorForEvents + 请求用户启用“辅助功能”。
总结
在macOS(AppKit)开发中,监听键盘和鼠标、处理触控板手势、实现快捷键等功能,需要使用NSEvent。
当我们在日常使用的过程中,以截图工具为例,通常会要求我们授权“辅助功能”,这里就是利用NSEvent的addGlobalMonitorForEvents方法,监听键盘、鼠标和滚轮等信息(即使应用处于后台)。

但因为它监听了我们的键盘等信息,才能在其他应用打开时,实现截图效果。因此,在授权“辅助功能”时应该考虑到这一点,这涉及隐私问题。
扩展知识
NSEvent.EventType种类
NSEvent.EventType 是枚举类型,代表事件种类:
1、.keyDown / .keyUp:按键按下/抬起
2、.mouseDown / .mouseUp / .mouseMoved:鼠标点击、松开、移动
3、.rightMouseDown / . rightMouseUp:右键按下/抬起。
4、.flagsChanged:修饰键(Shift/Command)变化。
5、.scrollWheel:鼠标滚轮或触控板滚动。
6、gesture / .magnify / .swipe:手势、缩放、滑动。
7、.otherMouseDown:其他鼠标键(如中键)。
在键盘监听这种场景下,Combine 是否不可替代?
在“SwiftUI使用NSEvent监听剪贴板”示例中,涉及到复杂的Combine,也许会想要通过变量的修改来提到Combine:
@Published var didPressPaste: Bool = false
在SwiftUI中监听这一变量:
.onChange(of: KeyboardMonitor.shared.didPressPaste) { ... }
这里涉及到使用的场景:
1、当我们的状态变化是持久的,可以使用SwiftUI监听@Published变量。
2、如果是事件流(可多次触发),应该使用Combine的PassthroughSubject,因为它专门用于“瞬时”事件(不是状态)。
键盘 ⌘V 是一种”事件”而不是”状态”,所以 Combine 是最合适的方式。
这里的区别在于,@Published会保留数据,并且当数据修改时,如果数值是重复的,那么就不会触发通知,所以不适合“粘贴事件”。
Combine的PassthroughSubject属于离散的瞬时事件,它不会保留数据,每次触发(即使是重复值)都会通过send()发送通知,适合“粘贴事件”。