Touch on Mac 文档:迁移指南、游戏技术说明与 HIG
Touch on Mac 的演讲之后,Apple 又发布了什么? 到 10月28日 晚上,那篇中午还返回 404 的 AppKit 文章 “Adopting gesture recognizers in your app” 已经上线,其中一张四行的表格为每一类鼠标处理代码给出了下一步;第二篇文章 “Handling AppKit gestures” 记录了排他视图、针对卡住的识别器的两秒超时,以及两个调试用的默认设置;一篇新的技术说明 TN3216 讲的是游戏和模拟鼠标事件的顺序;三个「人机界面指南」页面带有日期为 10月28日 的更新日志条目,内容是 Mac 上的触控。1234 落地页的 “Documentation resources” 板块现在链接到这篇文章和文件承诺(file promises)示例,“Design resources” 板块链接到这三个 HIG 页面,而 Xcode 27.3 beta 板块仍写着 “Coming soon”(即将推出)。5
太长不看: 那篇面向开发者的文章于 10月28日 太平洋时间下午 1:47 发布,依据的是三场 Tech Talks 和一篇 6月的技术说明;它需要的那篇 AppKit 文章在中午返回 404。到晚上 9:51,那篇文章已经上线,旁边还多了两份文档。迁移文章把您的代码分为四行:标准控件和识别器无需改动;mouseDown(with:) 重写和跟踪循环获得单指模拟,应改成识别器;重写了某个鼠标方法的控件子类会退回到跟踪循环;永远不结束的自定义识别器会一直占住它的排他视图。1 第三行背后的规则是,AppKit 依据的是方法是否被重写,“not on what the override does”(而不是重写做了什么),所以调用 super 也无济于事。1 在保留的跟踪循环旁边加一个 buttonMask 为 0、仅限触控的 NSPanGestureRecognizer,这一做法现在有了文档,并写明了不改掩码的确切后果:鼠标拖移会启动识别器,而您的 mouseDown(with:) 永远不会运行。1 当 Info.plist 中没有那个键时,排他视图默认开启;Xcode 的 macOS App 模板只在项目的应用生命周期为 AppKit 应用委托时才把该键写为 NO,默认的 SwiftUI 生命周期则不写;这是我在这台 Mac 上读 Xcode 27.1 和 27.2 beta 内的模板得出的。27 卡住的识别器会在交互结束两秒后被取消;调试期间,一个用户默认设置会在缺少重写时崩溃,另一个则用崩溃取代无声的取消。2 游戏技术说明列出了一次轻点和一次拖移所产生的确切鼠标事件,告诉引擎不要累加增量,并解释在设置 NSIsTouchNative 之前,每个触控表面都会以一个额外的模拟 GCMouse 出现。3 HIG 把触控称为 “a secondary input mode”(一种辅助输入方式),要求 “tap targets bigger, not the controls themselves”(扩大点击区域,而不是控件本身),并为只在悬停时出现的控件新增了六条规则。4 仍未到来的:Xcode 27.3 beta,以及三个系统手势开关,它们在太平洋时间晚上 9:51 仍返回 404。56
上线了什么,在什么时候
下面每一项都附上我检查它的时间,因为服务器自己的时间戳与当天页面实际显示的内容对不上;实时追踪文章保存了当天的记录,并为这批文档加了一行迟到的条目。
- AppKit 文章。 “Adopting gesture recognizers in your app” 在太平洋时间中午从 Apple 的文档端点返回 404;到晚上 9:51,它已经上线,服务器给出的修改时间戳为太平洋时间下午 2:30,列在 AppKit 的 Essentials 一节中,排在 “Adopting Liquid Glass” 之后,并出现在该框架 Gestures 页面的 “Gesture handling” 之下;Wayback Machine 对它的首次存档时间是太平洋时间傍晚 6:08(凭据见参考来源)。1 “Bring Touch to your Mac app” 的 Tech Talk 页面仍然没有链接它。5
- 第二篇文章。 “Handling AppKit gestures” 在 Gestures 页面上与它并列。它没有修订历史,6月的技术说明没有链接它,Wayback Machine 没有它的存档,服务器时间戳为 10月27日 太平洋时间晚上 10:35;我中午检查的页面里没有它,所以我无法说出它是何时上线的。2
- 一篇新的技术说明。 “TN3216: Supporting touch input for games on macOS” 的修订历史只有一行,“2026-10-28 First published”,服务器时间戳为太平洋时间下午 2:09。3
- 三个 HIG 页面。 “Designing for macOS”、“Gestures” 和 “Pointing devices” 现在末尾都有日期为 2026年10月28日 的更新日志行。我中午抓取的副本里没有任何新文字,晚上 9:51 抓取的副本里有。新 “Gestures” 页面上服务器的修改响应头写的是 10月27日 5:11 UTC,比那份缺少新文字的中午副本还早;我把两个事实都报告出来,不给出任何发布时刻。4
- 落地页。 上午 11:49,“Tools and documentation” 下的三个板块都写着 “Coming soon”,链接放在 HTML 注释里。到晚上 9:51,“Documentation resources” 板块(“Learn how to build and optimize your app for touch input.”)链接到这篇文章和文件承诺示例,“Design resources” 板块(“Create experiences that take advantage of touch user interface patterns.”)链接到三个 HIG 页面和 Apple Design Resources,后者的产品边框列表中包括 “MacBook Pro PROLED XDR Display”。“Xcode 27.3 beta” 板块仍写着 “Coming soon”,页面里也仍有 HTML 注释
TK skill。5 - 示例代码。 “Supporting drag and drop through file promises” 的页面现在这样描述它的识别器:“The recognizers work the same way with a pointer or with direct touches on a Mac with a touch display.”(无论用指针,还是在配备触控显示屏的 Mac 上直接触控,这些识别器的工作方式都相同。)6
- 其他都没有变化。 开发者新闻订阅源最新的一条仍是 PDT 上午 10:00:55 的 “Say hello to Touch on Mac”;发布订阅源最新的条目是 10月26日 的小版本更新,没有 Xcode 27.3 或 macOS 27.3 beta;Tech Talks 列表还是那三场演讲;ActivityKit 页面的时间戳为 10月8日,仍未列出 macOS 平台。56
下午那篇文章说,成文的规则就是那几场演讲和 6月的技术说明。本文讲的是成文规则新增了什么;凡是晚间检查发现信息不同的地方,本文都对下午那篇文章做出更正,更正清单在文末。
为您的代码分类的表格
文章的 “Prepare your app for a Mac with a touch display” 一节是一张表格,共三列,分别是输入处理、触控下的行为和下一步,共四行。Apple 的行为一列原文引用;其余两列为压缩概括,只有第三行的下一步是 Apple 的原话:1
| 您的代码 | Apple 的 “Behavior with touch”(触控下的行为) | 下一步 |
|---|---|---|
| 标准 AppKit 控件和手势识别器 | “Works with touch, with no changes needed.”(无需改动即可配合触控工作。) | 测试。 |
mouseDown(with:) 重写和跟踪循环 |
“AppKit emulates a single-finger tap or drag, but supports multi-finger gestures and individual touches only through gesture recognizers.”(AppKit 模拟单指轻点或拖移,多指手势和单个触控只能通过手势识别器支持。) | 改用识别器。 |
重写了鼠标处理方法的 NSButton、NSSlider 及其他 AppKit 视图的子类 |
“AppKit falls back to a tracking loop.”(AppKit 退回到跟踪循环。) | “Replace the overrides with target-action pairs for tracking events.”(用跟踪事件的 target-action 对取代这些重写。) |
自定义 NSGestureRecognizer 子类 |
“A recognizer that never reaches a terminal state keeps its exclusive view, which can make your app appear unresponsive.”(永远到不了终止状态的识别器会一直占住它的排他视图,可能让应用看起来没有响应。) | 处理取消,并到达终止状态。 |
表格之后的测试只有四行,值得照原样执行:“Controls respond to a tap, and buttons highlight and activate as expected”、“Dragging a view, selecting text, and scrolling work with one finger”、“Pinch and rotate work where your app supports them”,以及 “Small targets, such as resize grips and grab handles, are easy to hit with a finger”(控件响应轻点,按钮按预期高亮并激活;一根手指即可拖移视图、选择文本和滚动;应用支持的地方,捏合和旋转都能用;缩放把手、抓取手柄等小目标用手指也容易点中)。1 搜索清单把演讲中对 mouseDown(with:) 重写的审查扩展到五个符号:“search your project for nextEvent(matching:until:inMode:dequeue:) and trackEvents(matching:timeout:mode:handler:), because tracking loops typically call these methods. Also search for mouseDown(with:) and mouseDragged(with:) overrides, and for uses of currentEvent in control action methods.”(在项目中搜索这两个方法,因为跟踪循环通常会调用它们;再搜索 mouseDown(with:) 和 mouseDragged(with:) 的重写,以及控件动作方法中对 currentEvent 的使用。)1 最后一项检查正是第四行所关心的:“confirm that your app stays responsive after a touch interaction ends, and that every gesture recognizer action method handles the NSGestureRecognizer.State.cancelled state.”(确认触控交互结束后应用仍保持响应,并且每个手势识别器动作方法都处理了 cancelled 状态。)1
Overview 中有两句话,解决了演讲留给读者自行推断的一个问题。模拟是单指的:“AppKit emulates mouse events from a single finger tapping or dragging”,以及 “Relying on this emulation, however, means you miss out on multi-finger gestures, like pinch and rotate, that gesture recognizers handle.”(AppKit 根据单指的轻点或拖移模拟鼠标事件;但依赖这种模拟,就会错过由手势识别器处理的捏合、旋转等多指手势。)1 测试手段也正是演讲所说的那一个,现在写进了一条提示:“Use Sidecar to test touch input without a Mac that has a touch display”,因为 “AppKit then routes touch on its screen the same way it does on a Mac with a touch display.”(AppKit 对其屏幕上的触控,会按与触控显示屏 Mac 相同的方式路由。)1
重写规则
表格第三行背后有一条规则,演讲和 6月的技术说明都没有讲得这么直白。如果您继承 NSButton 并重写 mouseDown(with:) 来添加一个高亮动画,“AppKit treats your override as a replacement for its own click handling, rather than as a side effect”(AppKit 会把您的重写当作对其自身点按处理的替代,而不是附带效果),按钮就会回到跟踪循环。1 接着是:“AppKit decides based on whether your subclass overrides the method, not on what the override does”(AppKit 依据的是子类是否重写了该方法,而不是重写做了什么),所以即使重写调用了 super.mouseDown(with:),退回依然发生,并且 “Overriding mouseDragged(with:) or mouseUp(with:) has the same effect.”(重写 mouseDragged(with:) 或 mouseUp(with:) 效果相同。)1
替代方案是演讲中的控件事件模式,而文章的代码清单与演讲的有一处细节不同,值得照抄。演讲 2:24 的清单为 [.trackingExited, .trackingEndedInside] 注册了一个动作,为 .trackingCancelled 另注册了一个;文章的隐藏集合是 [.trackingExited, .trackingEndedInside, .trackingEndedOutside, .trackingCancelled],多覆盖了在按钮外结束的按压。19 trackingEndedOutside 并不新:Apple 的文档把它列为 macOS 11.0,macOS 27.0 SDK 在 NSControl.h 中把它声明为 NSControlEventTrackingEndedOutside = 1 << 7(作者分析)。67 文章还指出,动作方法必须用 @objc 暴露给 Objective-C;如果只需要激活这一刻,primaryActionTriggered 会触发,“regardless of whether someone used a mouse, trackpad, or touch to produce it.”(无论用户是用鼠标、触控板还是触控产生的。)1
容器视图也有同样的陷阱:“If you start a drag from mouseDragged(with:) in an NSTableView, NSCollectionView, NSOutlineView, or NSBrowser, AppKit falls back to a tracking loop for the interaction”(如果在这些容器视图中从 mouseDragged(with:) 开始拖移,AppKit 会为这次交互退回到跟踪循环),出路是数据源和委托的拖移方法,表格视图用的是 tableView(_:pasteboardWriterForRow:)。1
对于两种常见的 mouseDown(with:) 重写,文章给出了改写前后的对照。“Most mouseDown(with:) overrides do one of two things: mark an item as selected when someone clicks it, or show a menu when someone Control-clicks.”(大多数 mouseDown(with:) 重写只做两件事之一:用户点按时把某个项目标为选中,或者用户按住 Control 点按时显示菜单。)1 对于选中,改为观察集合视图项或表格行视图的 isSelected,但要注意这个观察者 “also runs when your code changes the selection programmatically”(在代码以编程方式改变选择时也会运行);对于菜单,重写 menu(for:) 并让 AppKit 呈现它,如果菜单不依赖于这次点按,则设置 menu 或 defaultMenu。1
在保留的循环旁加一个仅限触控的识别器
演讲推荐在现有跟踪循环旁加一个仅限触控的平移识别器,作为小版本更新的路径。文章在 “Add touch support to your custom tracking loop” 一节记录了同样的做法,包括共用的开始、继续、结束和取消函数,然后以演讲没有篇幅做到的精确度,说明了每个属性的作用:1
let touchRecognizer = NSPanGestureRecognizer(target: self, action: #selector(handleTouchPan))
touchRecognizer.buttonMask = 0
touchRecognizer.allowedTouchTypes = .direct
addGestureRecognizer(touchRecognizer)
“setting buttonMask to 0 means the gesture recognizer only recognizes touches, not mouse or trackpad input, so your tracking loop keeps handling the mouse and trackpad exactly as before.”(把 buttonMask 设为 0,意味着手势识别器只识别触控,不识别鼠标或触控板输入,因此跟踪循环会和以前完全一样地继续处理鼠标和触控板。)1 如果把掩码留在默认值,识别器就会 “also responds to the primary mouse button. A mouse drag then begins the gesture recognizer, and AppKit doesn’t call your mouseDown(with:) override, so the tracking loop doesn’t run. A click that doesn’t move still reaches your tracking loop.”(也响应鼠标主按钮。于是鼠标拖移会启动手势识别器,AppKit 不再调用您重写的 mouseDown(with:),跟踪循环也就不会运行;没有移动的点按仍会到达跟踪循环。)1 所以这个掩码不是可有可无的细节;没有它,循环会悄无声息地收不到拖移。allowedTouchTypes 那一行属于双重保险:“Direct touches are already the default for allowedTouchTypes, and setting it to .direct states that intent explicitly.”(直接触控本来就是 allowedTouchTypes 的默认值,设为 .direct 只是把意图明确写出来。)1 Apple 自己对这种做法的评价是:“This is often the fastest, lowest-risk way to add touch support to code with a tracking loop you’d rather not rewrite all at once.”(对于不想一次性重写的跟踪循环代码,这往往是添加触控支持最快、风险最低的方式。)1
如果您选择替换循环,文章的平移示例会在 .cancelled 时把标题恢复到起始位置;而一个启动了拖移会话的循环,应把识别器而不是事件传给 beginDraggingSession(items:gesture:source:)。1
用两个委托方法做筛选
相对于演讲,最新的一节是 “Filter gesture recognizer candidates”。识别器是通过命中测试收集的,而不是通过响应者链:“Gesture recognizers don’t use the responder chain.”1 AppKit 收集识别器时,“not every recognizer it finds is actually applicable to the interaction”(找到的识别器并非都真正适用于这次交互),应用通过两个委托方法剔除不适用的那些:gestureRecognizer(_:shouldAttemptToRecognizeWith:) 用于 “to inspect the initiating NSEvent for pointer-based input”(检查指针输入的起始 NSEvent),gestureRecognizer(_:shouldReceive:) 用于 “to inspect each new NSTouch for touch input”(检查触控输入中每一个新的 NSTouch)。1 两者的意义在于:“Either method gives you the initial input, something the recognizer doesn’t otherwise expose, and lets you stop the recognizer from handling that event stream or touch.”(任一方法都会把初始输入交给您,这是识别器本身不会暴露的东西,并让您能阻止识别器处理该事件流或触控。)1 而且分工是严格的:“Direct touches don’t call this method. To filter them, implement gestureRecognizer(:shouldReceive:) instead, as shown above.”(直接触控不会调用这个方法;要筛选它们,请改为实现 gestureRecognizer(:shouldReceive:),如上所示。)1 只通过事件方法筛选,直接触控就不会经过这个委托的筛选。
这两个示例是我预计会被照搬的模式。一个是缩放把手:把缩放识别器挂在整个视图上,在两个委托方法中都返回位置是否落在把手的框架内,返回 true 的用意是 “only to give the gesture recognizer a chance to attempt recognition, not to guarantee it succeeds.”(只是给手势识别器一个尝试识别的机会,并不保证它成功。)1 另一个是由片段组成的时间线:时间线上只挂一个识别器,在事件方法中对片段做命中测试,把它存下来供动作方法使用,没有命中时返回 false。1 针对小目标的配套警告是:“If a custom control expects a touch to land inside its visible area, update it to handle touches that land outside that area.”(如果某个自定义控件预期触控落在其可见区域之内,请更新它,使其能处理落在区域之外的触控。)1
幸存者之间的优先级用失败依赖来表达,文章重复了第二篇文章中的警告,“Use this sparingly, because the waiting recognizer responds more slowly”(请谨慎使用,因为等待中的识别器响应更慢),并举例说 “making a single click wait for a double click to fail makes clicks feel sluggish.”(让单击等待双击失败,会让点按显得迟钝。)1
currentEvent、悬停与轻扫
“Handle touch input” 一节以文档的形式重申了演讲中那句统领全局的话:AppKit “doesn’t deliver direct touches (NSTouch) through the responder chain or through event monitors”(不会通过响应者链或事件监视器传递直接触控),以及 “Gesture recognizers are the only way to respond to multi-finger gestures or to receive individual touches from a Mac with a touch display or a Sidecar-connected iPad.”(手势识别器是响应多指手势、或从触控显示屏 Mac 及通过 Sidecar 连接的 iPad 接收单个触控的唯一途径。)1 AppKit 的输入概览页面从响应者的角度说了同样的话:“The responder chain receives touch events only from indirect touches on a trackpad, if a view asks for them. It doesn’t receive touch events from direct touches on a Mac with a touch display.”(响应者链只在视图请求时接收来自触控板间接触控的事件,不接收触控显示屏 Mac 上直接触控的事件。)6
关于能力检查,建议是尽量避免:“In most cases, instead of checking whether a device supports touch, write your event-handling code to work the same way regardless of input source, and decide on alternate presentations based on the interaction type.”(大多数情况下,不要检查设备是否支持触控,而应让事件处理代码对任何输入来源都以相同方式工作,再依据交互类型决定是否采用其他呈现方式。)1 文中为 touchCapabilities 给出的唯一例子是 “capturing a signature on a touch display.”(在触控显示屏上采集签名。)1
currentEvent 单独占了一节。由于控件交互要经过识别器,“currentEvent no longer reliably returns the event that initiated an action”(currentEvent 不再可靠地返回发起某个动作的事件),改写前后的对照用类级别的 NSEvent.modifierFlags 取代事件自身的修饰键标志,并提醒 “These report the state at the time you query them, not at the time of the original event. A touch doesn’t register in pressedMouseButtons.”(它们报告的是查询那一刻的状态,而不是原始事件发生时的状态;触控不会出现在 pressedMouseButtons 中。)1 不变的部分是:“Keyboard events and scroll-wheel events behave the same on every input source: AppKit delivers them through the responder chain. Hover tracking, which relies on mouseMoved(with:) and NSTrackingArea, applies to pointer input only, because a touch display has no hover.”(键盘事件和滚轮事件在任何输入来源上表现都相同,AppKit 通过响应者链传递它们;依赖 mouseMoved(with:) 和 NSTrackingArea 的悬停跟踪只适用于指针输入,因为触控显示屏没有悬停。)1
轻扫不会被转换:“AppKit doesn’t translate touch swipes for you.”1 如果您使用 trackSwipeEvent(options:dampenAmountThresholdMin:max:usingHandler:),就要自己实现;对于前进和后退导航,先检查 NSEvent.isTouchSwipeNavigationEnabled,“because the person can turn off swipe navigation in System Settings.”(因为用户可以在“系统设置”中关闭轻扫导航。)1 这个属性是新的:Apple 的文档把它列为 macOS 27.0,macOS 27.0 SDK 中的 NSEvent.h 在第 496 行以 API_AVAILABLE(macos(27)) 声明了它(作者分析)。67
自定义识别器:约定、超时与两个默认设置
文章为自定义 NSGestureRecognizer 子类给出的准则,是一份七条要点的约定:在 mouseCancelled(with:) 和 touchesCancelled(with:) 中处理取消;在事件方法中只更新私有的跟踪状态并设置 state;在动作方法中响应,而不是在事件回调中;为动作方法缓存起始的 NSEvent 或 NSTouch;“Override reset() to clear your cached state, so it doesn’t leak from one gesture into the next”(重写 reset() 清除缓存状态,以免从一个手势泄漏到下一个);“Always end an active gesture recognizer by transitioning to NSGestureRecognizer.State.ended, NSGestureRecognizer.State.failed, or NSGestureRecognizer.State.cancelled. Otherwise, other recognizers can’t activate”(始终通过转换到 ended、failed 或 cancelled 来结束活动中的手势识别器,否则其他识别器无法激活);以及重写 location(in:),因为 “If you skip this override and build your app with the macOS 27 SDK and later, AppKit returns NSZeroPoint and logs a console error.”(如果跳过这个重写,并用 macOS 27 及更高版本的 SDK 构建应用,AppKit 会返回 NSZeroPoint 并在控制台记录错误。)1 随后的 DragTrackingRecognizer 示例是一个骨架:它的鼠标按下、拖移和抬起重写的方法体都是注释,告诉您调用同一个私有的 track(_:event:state:) 方法,取消重写的注释则让您设置 .cancelled,动作方法再把缓存的位置和事件读回来。1
违反约定的后果,是在第二篇文章里量化的。“A gesture recognizer is stuck if it hasn’t reached a terminal state within 2 seconds after the person stops interacting with your app, such as when they release the mouse button or lift their fingers.”(如果在用户停止与应用交互后 2 秒内,例如松开鼠标按钮或抬起手指之后,手势识别器仍未到达终止状态,它就算卡住了。)2 在排他视图机制下,“A stuck gesture recognizer keeps holding its exclusive view”(卡住的手势识别器会一直占住它的排他视图),从而阻止其层级之外的收集;系统的补救是自动的:“To keep a single stuck gesture recognizer from blocking your app indefinitely, the system automatically transitions it to the NSGestureRecognizer.State.cancelled state, allowing gesture gathering to resume normally.”(为避免单个卡住的手势识别器无限期地阻塞应用,系统会自动把它转换到 cancelled 状态,让手势收集恢复正常。)2 这就是表格第四行背后的机制。它意味着,已上架应用中一个有问题的识别器,会阻塞其排他层级之外的手势收集,让应用其余部分看起来没有响应,直到用户停止交互两秒后的自动取消为止,而动作的目标仍需负责处理这次取消。
有两个用户默认设置,能让这些问题在开发期间带着堆栈跟踪崩溃,一个针对缺失的重写,一个取代无声的取消。“Set the NSGestureRecognizerCrashOnMissingOverrides key in UserDefaults to YES”(在 UserDefaults 中把该键设为 YES),或者以启动参数传入 -NSGestureRecognizerCrashOnMissingOverrides YES,就会在缺少本应重写的事件方法时崩溃;把 NSCrashOnStuckGestureTimeout 设为 “to a small number of seconds, such as 2,”(一个较小的秒数,例如 2),或者传入 -NSCrashOnStuckGestureTimeout 2,就会在识别器卡住时崩溃,而不是取消它。2 第一个并不能抓住所有问题:它 “doesn’t detect a gesture recognizer that correctly transitions to NSGestureRecognizer.State.cancelled and sends its action, if the action’s target doesn’t handle the canceled state.”(检测不到这样的情况:手势识别器正确转换到 cancelled 并发送了动作,但动作的目标没有处理取消状态。)2 接下来的警告值得写进检查清单:“Comment out or remove NSGestureRecognizerCrashOnMissingOverrides and NSCrashOnStuckGestureTimeout before shipping your app”(发布应用前,注释掉或删除这两个设置),以及 “Changes to these user defaults or launch arguments take effect only after you rebuild or relaunch your app.”(对这些用户默认设置或启动参数的更改,只有在重新构建或重新启动应用后才会生效。)2 这两个默认设置在演讲和 6月的技术说明中都没有出现。
同一篇文章还解释了两种并非成功的终止状态之间的区别:.failed 用于从未匹配的识别器,而 “If a gesture recognizer sets this after it already delivered an action, the system automatically converts it to NSGestureRecognizer.State.cancelled instead.”(如果手势识别器在已经传递过动作之后才设置这个状态,系统会自动把它转换为 cancelled。)2 它还点出了演讲中 Exposé 例子之外另一种系统取消的原因:“when the same touches begin a system-level gesture like smartMagnify(with:), the system takes over those touches for its own use.”(当同样的触控开始了 smartMagnify(with:) 这类系统级手势时,系统会接管这些触控自用。)2
同一页面上还有三个较小的事实。识别器可以延迟响应者事件:“If you set delaysPrimaryMouseButtonEvents to true, the system stores the relevant events until the gesture recognizer fails, then delivers them to the appropriate responders. If the gesture recognizer reaches the NSGestureRecognizer.State.ended state, the system never delivers the events.”(如果把 delaysPrimaryMouseButtonEvents 设为 true,系统会暂存相关事件,直到手势识别器失败,再把它们交给合适的响应者;如果手势识别器到达 ended 状态,系统就永远不会传递这些事件。)2 响应者自己的触控方法只适用于触控板:“These methods don’t have an equivalent delay mechanism, and they apply only to indirect touches from a trackpad.”(这些方法没有对应的延迟机制,并且只适用于来自触控板的间接触控。)2 视图控制器不能持有识别器:“You can’t attach a gesture recognizer to a view controller directly”(不能把手势识别器直接挂到视图控制器上),所以应把它加到控制器的视图上,并以控制器作为目标和委托。2
排他视图:默认开启,Xcode 的 AppKit 模板则关闭
下午那篇文章依据演讲和 6月的技术说明解释了排他性。这两篇文章补充了默认值、机制,以及一个关于 Xcode 的事实。
机制是 macOS 27 新增的:“In macOS 27 and later, the initial hit-tested view for a mouse click or touch event becomes an exclusive view for gathering gesture recognizers”(在 macOS 27 及更高版本中,鼠标点按或触控事件最初命中测试到的视图,会成为收集手势识别器的排他视图),以及 “This behavior simulates the presence of a single mouse pointer, even when a person touches a Mac with a touch display with several fingers at once.”(即使用户同时用几根手指触碰触控显示屏 Mac,这一行为也会模拟只有一个鼠标指针的情形。)2 默认值是:“Exclusive views are on by default”,以及 “If your app’s information property list doesn’t include the NSViewGestureRecognizerIsExclusive key, the system treats it as YES.”(如果应用的信息属性列表中没有这个键,系统会将其视为 YES。)2 该键自己的页面也这么说,“The default value is YES”,并把退出限定得很窄:“Set this key to NO to allow gesture recognizers in multiple views to activate simultaneously. Do this only if your app intentionally needs concurrent gesture recognition across different views.”(把该键设为 NO,可让多个视图中的手势识别器同时激活;只有当应用确实需要跨视图并发识别手势时才这样做。)6 系统按视图退出的例子是颜色面板:“AppKit uses this same technique in the standard Color Panel”,在那里几个滑块可以同时移动。2
关于 Xcode 的事实,两篇文章都写了:“Xcode adds the NSViewGestureRecognizerIsExclusive key with a value of NO to new macOS app projects, so those apps don’t use exclusive views unless you change the value to YES.”(Xcode 会在新的 macOS 应用项目中加入值为 NO 的该键,因此这些应用不使用排他视图,除非您把值改为 YES。)1 为了弄清这涵盖哪些项目,我读了这台 Mac 上的项目模板(作者分析)。在 Xcode 27.1(27A9275)和 Xcode 27.2 beta(27B5019j)中,macOS App 模板继承自一个 “Cocoa App Base” 单元,它的应用生命周期选项有两个值,SwiftUI 和 Cocoa,默认为 SwiftUI;值为 NO 的键 INFOPLIST_KEY_NSViewGestureRecognizerIsExclusive 只出现在 Cocoa 单元中。因此,当项目使用 AppKit 应用委托生命周期时,Xcode 会把它作为一个构建设置写入,并落进生成的 Info.plist;项目使用 SwiftUI 生命周期时则不会。7 生命周期选项在模板中没有显示名称;有显示名称的选项是 Interface:(SwiftUI、Storyboard 或 XIB,默认 SwiftUI),而 Storyboard 和 XIB 单元要求 AppKit 生命周期,所以 Storyboard 或 XIB 项目会得到这个键,SwiftUI 界面的项目只有在生命周期为 AppKit 应用委托时才会得到它。7 Document App 模板派生自同一个基础,所以在同样的条件下,它既继承那个构建设置,也把该键作为值为 false 的 Info.plist 条目写入;macOS Game 模板强制使用 Cocoa 生命周期,所以新建的 macOS 游戏一开始就是非排他的。7 这台 Mac 上没有比 27.1 更早的 Xcode,所以我无法说出模板是何时加入这个键的。实际的解读是:用这些模板创建的 AppKit 项目一开始是非排他的;SwiftUI 项目,以及任何 Info.plist 中没有该键的现有应用,则是排他的。第二篇文章的迁移说明正是由此而来:如果您的应用把该键设为了 NO,那么在替换跟踪循环时,“also set the view’s exclusiveGestureBehavior to NSView.ExclusiveGestureBehavior.exclusive”(同时把视图的 exclusiveGestureBehavior 设为 exclusive),因为 “This prevents simultaneous gesture recognition with unrelated views, which the loop previously prevented implicitly.”(这能防止与无关视图同时识别手势,而此前循环是隐式地阻止了这种情况。)2
第一篇文章理清了与排他性和滚动有关的三项设置,用的是这句话:“These settings don’t override each other.”(这些设置不会相互覆盖。)1 exclusiveGestureBehavior 和那个 plist 键决定 AppKit 收集哪些识别器,并且 “The view’s setting applies to that view and its subviews, and takes precedence over the app-wide key.”(视图的设置适用于该视图及其子视图,并优先于应用级的键。)1 isCancellableByScrollGesture 按识别器设置,与前者相互独立:“It lets a scroll gesture cancel that gesture recognizer even if it has already begun.”(即使该手势识别器已经开始,滚动手势也能取消它。)1 scrollGestureForRelationships 用于与滚动视图自身的识别器相竞争的识别器。1 文章提醒要留意的场景,是滚动视图里的按钮:在它的识别器上把 isCancellableByScrollGesture 设为 true,这样从按钮上开始的拖移会变成滚动,而不是点按。1
文本引擎还多了一个工具。NSTextSelectionManager “creates its gesture recognizers itself, so you don’t have a reference to them”(自己创建手势识别器,所以您拿不到它们的引用),因此,一个必须等待它们的自定义识别器,应在管理器的 gesturesForFailureRequirements 包含另一个识别器时,从 gestureRecognizer(_:shouldRequireFailureOf:) 返回 true。1 这个属性在文档中列为 macOS 27.0,并在 macOS 27.0 SDK 的 NSTextSelectionManager.h 第 59 行声明(作者分析)。67
模拟,五条要点
下午那篇文章引用了 6月技术说明中针对非 AppKit UI 框架的四条模拟规则。文章的清单有五条,新增的是拖移。18
- “Tapping emulates a mouse-down followed immediately by a mouse-up.”(轻点模拟一次鼠标按下,紧接着一次鼠标抬起。)
- “Touching and then immediately panning emulates a trackpad scroll, even if the UI doesn’t otherwise handle Multi-Touch input directly.”(触碰后立即平移,模拟触控板滚动,即使界面本身并不直接处理多点触控输入。)
- “Touching, briefly holding, and then moving emulates mouseDown(with:), followed by mouseDragged(with:) as it moves, and mouseUp(with:) on lift.”(触碰、短暂按住再移动,模拟 mouseDown(with:),移动时为 mouseDragged(with:),抬起时为 mouseUp(with:)。)
- “Touching and holding without movement emulates rightMouseDown(with:), and rightMouseUp(with:) on lift.”(按住不动,模拟 rightMouseDown(with:),抬起时为 rightMouseUp(with:)。)
- “Pinching or rotating with two fingers emulates the trackpad magnify and rotate gestures.”(双指捏合或旋转,模拟触控板的放大和旋转手势。)
第三条是演讲中 “drag with a small hold delay”(带一点按住延迟的拖移)(11:31) 的成文形式:立即移动的触控是滚动,先按住的触控是拖移。9 6月的清单有轻点、立即平移、长按以及双指捏合或旋转,没有拖移这一条。8 上限是两根手指:“This emulation covers interactions with one or two fingers. If another finger touches down during a pinch or rotation, the emulation ignores it and keeps tracking the original two fingers.”(这种模拟涵盖一指或两指的交互;如果在捏合或旋转期间又有一根手指按下,模拟会忽略它,继续跟踪原来的两根手指。)1 三根及以上的手指需要识别器;一旦框架自己处理触控,把 NSIsTouchNative 设为 YES 就会关闭额外的模拟。1
游戏:TN3216
这篇技术说明面向游戏:“This guidance focuses on configurations common in games, such as an app with a single view backed by a CAMetalLayer.”(本指导聚焦于游戏中常见的配置,例如只有一个由 CAMetalLayer 支撑的视图的应用。)3 它的设计定位比 HIG 更严格:“Touch extends the Mac experience rather than replacing other input”(触控是对 Mac 体验的延伸,而不是取代其他输入),以及 “Touch works best for brief interactions that complement these devices”(触控最适合补充这些设备的简短交互),并指示 “Design touch interactions for brief moments that the player can complete with one hand, while the other hand stays on the keyboard, mouse, or controller”(把触控交互设计成玩家用一只手就能完成的短暂时刻,另一只手留在键盘、鼠标或手柄上),适用的交互 “such as inventory management, map zoom and scroll, or the settings menu.”(例如物品栏管理、地图缩放与滚动,或设置菜单。)3
兼容性方面,仍是同样的模拟,只是针对游戏做了调整。“AppKit applies game-specific emulation to apps in a games category, and can apply emulation to an individual view, such as one that hosts a third-party UI framework.”(AppKit 会对游戏类别的应用施加游戏专用的模拟,也可以对单个视图施加模拟,例如承载第三方 UI 框架的视图。)3 类别从 Info.plist 读取:如果 LSApplicationCategoryType 是 public.app-category.games 或某个游戏子类别,“the system applies game-specific mouse emulation.”(系统会施加游戏专用的鼠标模拟。)3 它要解决的是鼠标永远不会遇到的问题:“A mouse moves continuously from one position to the next, but a player can tap or start a drag at two distant locations in succession”(鼠标从一个位置连续移动到下一个位置,而玩家可以先后在两个相距很远的位置轻点或开始拖移),所以累加增量的游戏会漂移,而 “For games, AppKit sends a mouseMoved event to the new location before each mouseDown(with:), so your game can refresh its cached mouse position.”(对于游戏,AppKit 会在每次 mouseDown(with:) 之前向新位置发送一个 mouseMoved 事件,让游戏能刷新缓存的鼠标位置。)3
技术说明接着按顺序列出模拟的事件。一次轻点:mouseMoved(with:),“from the last mouse position to the tap location”(从上一个鼠标位置到轻点位置),然后是 mouseDown(with:),再然后是 mouseUp(with:)。一次拖移:mouseMoved(with:) 到拖移开始的位置,mouseDown(with:),mouseDragged(with:) “repeatedly while the touch continues”(在触控持续期间反复发送),以及 mouseUp(with:)。3 然后是一个 “Important” 框里的规则:“Don’t intercept events by overriding sendEvent(:) or pulling them with nextEvent(matching:until:inMode:dequeue:). Doing so can interfere with gesture recognizers that respond to a physical mouse.”(不要通过重写 sendEvent(:) 或用 nextEvent 拉取的方式拦截事件,这样做可能干扰响应实体鼠标的手势识别器。)3
在演讲中找不到对应内容的部分是 GCMouse。“By default, the system presents each touch surface, whether a Mac with a touch display or a Sidecar-connected iPad, as an additional GCMouse that emulates mouse input from touches”(默认情况下,系统会把每个触控表面,无论是触控显示屏 Mac 还是通过 Sidecar 连接的 iPad,呈现为一个额外的、根据触控模拟鼠标输入的 GCMouse):手指按下即按下左键,移动时以一个增量调用鼠标移动处理程序,抬起即松开。3 它的局限是:“The emulated GCMouse ignores additional fingers, and it doesn’t emulate other buttons, scrolling, or pinch input”(模拟的 GCMouse 会忽略多出的手指,也不模拟其他按钮、滚动或捏合输入),并且由于触控会落在互不相连的位置,“Don’t use them to track an absolute pointer position.”(不要用它们来跟踪绝对的指针位置。)3
NSIsTouchNative 对游戏的作用比对 AppKit 应用更大。设为 YES 后,“AppKit’s emulation will stick to a generic emulation - ignoring the app’s category or whether it uses a third-party UI framework - and disable GCMouse emulation”(AppKit 的模拟会只保留通用模拟,不考虑应用类别或是否使用第三方 UI 框架,并停用 GCMouse 模拟),于是触控表面不再以 GCMouse 出现,并且 “Your game receives touch through gesture recognizers instead of emulated mouse events.”(游戏会通过手势识别器接收触控,而不是通过模拟的鼠标事件。)3 仍有一点残留:“AppKit still emulates mouse events for any NSView that handles them, for example by overriding mouseDown:.”(对于任何处理鼠标事件的 NSView,例如重写了 mouseDown: 的视图,AppKit 仍会模拟鼠标事件。)3 该键自己的页面说,大多数 AppKit 应用不需要它,它是 “primarily intended for apps that embed a non-AppKit rendering or input framework and implement their own touch-handling pipeline”(主要面向嵌入了非 AppKit 渲染或输入框架、并自行实现触控处理流程的应用);游戏引擎正是这种情况。6
实现一节是一个用 Swift 和 Objective-C 写的委托模式:一个平移识别器和一个轻点识别器,buttonMask 都为 0,以忽略实体鼠标;一个放大识别器;shouldRecognizeSimultaneouslyWith 只对平移与捏合这一对返回 true;shouldRequireFailureOf 让轻点等待两者。3 一条注释确认了第一篇文章所说的默认值:“The gesture recognizers must include direct in allowedTouchTypes. On macOS 27 and later, this is the default.”(手势识别器的 allowedTouchTypes 必须包含 direct;在 macOS 27 及更高版本中,这是默认值。)3 路由建议是按引擎的思路来的:“Install the gesture recognizers once in your app and leave them enabled”(在应用中安装一次手势识别器并保持启用),把回调转换为引擎的输入事件,并且 “Let the active input handler decide if and how to react to different gestures”(让当前的输入处理程序决定是否以及如何响应不同的手势);示例中的平移处理程序会用后备缩放因子把点转换为像素,并为原点在左上角的引擎翻转 y 轴。3 切换之后,标准控件照常工作,“as long as you don’t attach your own direct-touch gesture recognizers to them.”(只要您不在它们上面挂自己的直接触控手势识别器。)3
结尾的检查清单分两半。对于依赖模拟的游戏:从每个事件中读取位置,不要重写 sendEvent:,也不要从 nextEventMatchingMask: 拉取。对于自己处理触控的游戏:设置 NSIsTouchNative,使用 buttonMask 为 0 的直接触控识别器,在玩家预期的地方加入触控并保持单手可完成,“Continue to read GCMouse for the physical mouse. With NSIsTouchNative set, touch no longer appears as an emulated GCMouse”(继续为实体鼠标读取 GCMouse;设置 NSIsTouchNative 后,触控不再以模拟的 GCMouse 出现),并尽可能使用标准控件。3
HIG,日期为 10月28日
下午那篇文章说,落地页计划链接的四个 HIG 页面都没有关于 Mac 显示屏触控的指导。现在其中三个有了,每个都带一行日期为 2026年10月28日 的更新日志;Live Activities 页面没有变化。4
Designing for macOS(“Added guidance for touch input.”,新增了触控输入指导)改写了平台描述中的三行,并新增一条最佳实践。人体工学:“On a Mac with a touch display, people reach forward to touch the screen, so they tend to use touch for brief, intentional interactions rather than sustained work.”(在触控显示屏 Mac 上,人们要向前伸手才能触碰屏幕,所以往往把触控用于简短、有意的交互,而不是持续的工作。)输入方式现在以键盘和指针设备打头,并在其他方式中列出 “Gestures on a trackpad or a Mac with a touch display”(触控板或触控显示屏 Mac 上的手势)。新增的最佳实践用一句话概括了整场设计演讲:“Support standard gestures on trackpads and Mac devices with a touch display. Use larger tap targets and controls that expand on touch to make touch interaction easier, while keeping control size and density the same to stay optimized for keyboard and pointing device input.”(在触控板和触控显示屏 Mac 上支持标准手势;使用更大的点击区域和在触控时展开的控件,让触控交互更容易,同时保持控件尺寸和密度不变,以继续针对键盘和指针设备输入进行优化。)4
Gestures(“Added guidance for gestures and touch input in macOS.”,新增了 macOS 中手势与触控输入的指导)扩充了它的 macOS 一节,该节现在链接到两篇 AppKit 文章,作为面向开发者的指导。第一句就定下了地位:“On a Mac with a touch display, or on an iPad connected with Sidecar, people can also interact directly using touch gestures as a secondary input mode.”(在触控显示屏 Mac 上,或在通过 Sidecar 连接的 iPad 上,人们也可以把触控手势作为辅助输入方式直接交互。)4 然后是六条规则,每条都以粗体开头:
- “Keep the layout and size of interface elements the same when you support touch.”(支持触控时,保持界面元素的布局和尺寸不变。)理由是:“In macOS, touch is secondary to keyboard and pointing device input”(在 macOS 中,触控次于键盘和指针设备输入),而且人们经常在不同输入方式之间切换。与 iPhone Duo 的对比正是要点所在,在那台设备上,内屏会改变尺寸类别:Mac 变的是输入方式,布局不变。
- “Make tap targets bigger, not the controls themselves”(扩大点击区域,而不是控件本身),做法是 “enlarging their tap targets to anticipate that interaction may start slightly outside of their visual bounds”(扩大点击区域,以预见交互可能从视觉边界稍外处开始),以及 “Where possible, use standard framework controls, which the system configures with an optimal tap target size automatically.”(尽可能使用标准框架控件,系统会自动为它们配置最佳的点击区域尺寸。)
- “Consider providing expandable controls”(考虑提供可展开的控件),即一种在轻点时出现、按住时保持、并且 “for a short time after they lift their finger to keep the control accessible to touch”(在手指抬起后再保留片刻,让控件仍可被触及)的临时触控尺寸变体;“When using standard system controls, steppers, sliders, and dials gain this behavior automatically.”(使用标准系统控件时,步进器、滑块和旋钮会自动获得这一行为。)
- “Don’t use touch as the only way to access controls or features”(不要让触控成为访问控件或功能的唯一途径),反过来也一样:“aim to provide touch equivalents for actions otherwise performed exclusively with a pointing device, like using a keyboard modifier to shift-drag or displaying a menu when the pointer moves over an element.”(对于原本只能用指针设备完成的操作,例如用键盘修饰键做 Shift 拖移,或指针移到元素上时显示菜单,尽量提供触控的等效操作。)
- “Where possible, align with gestures people already know”(尽可能与人们已经熟悉的手势保持一致),来源可以是您的 iPhone 或 iPad 应用,也可以是触控板,并且 “Test to ensure they don’t conflict with macOS system gestures.”(进行测试,确保它们不与 macOS 系统手势冲突。)
- “Make sure custom gestures work for both touch and trackpad”(确保自定义手势在触控和触控板上都能工作),做法是 “modifier keys where needed to help the gesture work for all touch inputs.”(在需要时借助修饰键,让手势适用于所有触控输入。)4
该页面没有给出 36 点这个数字;这个数字出自两场演讲(“Touch on macOS uses a touch target of 36 points”,设计演讲 9:17;以及 AppKit 演讲中把点击矩形扩大到 “a minimum of 36 points on each axis”,5:53)。9 它新增了一张表格 “macOS trackpad and touch display gestures”,把每个操作与触控板和触控的形式一一对应:选择是主按钮点按或轻点;打开是双击或双击轻点;滚动在触控板上用两根手指,在显示屏上用一根;缩放和旋转在两者上都是同样的捏合和双指转动;上下文菜单是辅助点按或按住;拖移是点按并拖移,或者 “Touch and hold, then drag.”(按住后拖移。)4 在跨平台表格中,“Touch (or pinch) and hold” 现在列出了 macOS,双击轻点一行也加上了 “open an item on a Mac with a touch display.”(在触控显示屏 Mac 上打开一个项目。)4
Pointing devices(“Added guidance for revealing hidden controls with touch on Mac, and updated guidance and artwork for iPadOS and macOS.”,新增了在 Mac 上用触控显示隐藏控件的指导,并更新了 iPadOS 和 macOS 的指导与插图)是规则最具体的一页,放在新标题 “Touch support for hidden controls” 之下。前提是:“Some Mac apps reveal certain controls only when the pointer moves over them. These controls need another way to appear when someone uses touch on a Mac that supports it.”(有些 Mac 应用只在指针移到某些控件上时才显示它们;当有人在支持触控的 Mac 上使用触控时,这些控件需要另一种方式出现。)4 规则如下:
- “Decide which hidden controls need touch support. Reserve touch support for essential controls.”(决定哪些隐藏控件需要触控支持,只为关键控件保留触控支持。)工具提示不在此列,触控已能以别的方式完成的操作也不在此列:“someone can swipe to scroll a carousel, so there’s no need to reveal the navigation buttons that exist for mice without a scroll wheel.”(用户可以轻扫来滚动轮播,所以无需显示那些为没有滚轮的鼠标而设的导航按钮。)还有:“Avoid keeping hidden controls visible all the time just to accommodate touch.”(不要仅为照顾触控而让隐藏控件始终可见。)
- “Reveal a hidden control when someone taps its general location on a touch display.”(当有人在触控显示屏上轻点隐藏控件的大致位置时,显示该控件。)只显示相关的那些,并且 “Hide a control again when the person finishes with it, or after a few seconds if they don’t use it.”(用户用完后再次隐藏控件,或者用户不使用时,几秒后隐藏。)
- “Retain the current selection when revealing a hidden control”(显示隐藏控件时保留当前选择),因为该控件可能作用于这一选择。
- “Make essential hidden controls available in a context menu”(让关键的隐藏控件可以从上下文菜单中访问),触控通过按住打开这个菜单。
- “In a sidebar, reveal hidden controls when someone taps the corresponding section header or row.”(在边栏中,当有人轻点相应的分区标题或行时,显示隐藏控件。)例子是访达:“tapping the Favorites header in the Finder sidebar collapses the section and reveals a disclosure button.”(轻点访达边栏中的“个人收藏”标题,会折叠该分区并显示一个展开按钮。)显示出来的控件会一直保留,直到触控离开边栏或用户使用指针设备;轻点隐藏控件所在区域只会显示它,不会激活它:“The control acts only when they tap it again.”(只有再次轻点时,控件才会起作用。)
- “In a tabbed window, reveal the selected tab’s close button when someone taps anywhere in the window”(在带标签页的窗口中,当有人轻点窗口任意位置时,显示所选标签页的关闭按钮),并一直保留,直到用户回到指针设备。4
这六条规则是两场演讲中悬停指导的成文形式,并补充了演讲没有给出的时长和例外:显示出来的控件在几秒后隐藏,需要第二次轻点才会激活,而边栏和标签页关闭按钮是会保持可见的情形。同一次修订还为 Liquid Glass 改写了该页面的 iPadOS 指针内容,把文中默认的指针形状从圆形改为箭头,把光标表格中仅限 AppKit 的一列换成了 AppKit 和 SwiftUI 的符号,并删除了指针吸附一节;这些都与 Mac 显示屏无关。4
仍未到来的
于 10月28日 太平洋时间晚上 9:51 核对,范围包括 Apple 的文档端点、落地页、开发者新闻与发布订阅源,以及 Tech Talks 列表。56
- Xcode 27.3 beta。落地页的板块仍写着 “Coming soon”,发布订阅源中没有比 10月26日 小版本更新(其中包括 macOS 27.2)更新的条目,Xcode 27.3 和 macOS 27.3 的发行说明页面返回 404。最新的 Xcode 仍是 27.2 RC,它的 SDK 我还没有检查;下午那篇文章关于部署目标的提醒仍然适用,部署目标的背景见 Xcode 27 发布那篇文章。
- AppKit 演讲演示过的三个系统手势开关:
NSWindow.disablesDirectTouchSystemGestures、UIViewController.prefersDirectTouchSystemGesturesDisabled和 SwiftUI 的directTouchSystemGesturesDisabled()。三个都返回 404,两篇新文章也都没有提到它们。文章描述了系统手势如何取消您的识别器,却没有描述如何关闭系统手势。 - 可在 macOS 上使用的 ActivityKit。框架页面的时间戳为 10月8日,没有列出 macOS 平台,Live Activities 演讲中的
AlertConfiguration.announcement(_:)返回 404。下午那篇文章关于 Live Activities 的一节维持原样,状态机那篇文章仍然只是一个 iOS 上的实现。 - AppKit 演讲在 12:16 提到的 “gesture modernization skill”(手势现代化技能)。没有任何 Apple 页面点出它的名字;落地页上的
TK skill注释仍在。59 - 演讲页面指向这篇文章的链接,以及 Apple 关于经由模拟器或 Device Hub 使用 Mac 触控的任何说明。
- SwiftUI 文档更新页面上带日期的条目。它的服务器时间戳为 10月28日 太平洋时间下午 2:58,但最新的一节仍是 2026年9月,与手势相关的条目仍是 2026年6月的
GestureInputKinds那一行;无论改动了什么,都不是新增一个带日期的小节。6
对开发者文章的更正
下午那篇文章在中午的核对时是准确的;到 10月28日 太平洋时间晚上 9:51 的核对时,其中有五处说法已不再成立。那篇文章中有一个与本文同时发布的更新块,链接到这里,相关说法也已就地更正;以下是清单。
- 落地页上三个 “Coming soon” 板块现在只剩一个。文档资源和设计资源已经上线,Xcode 27.3 beta 还没有。5
- “Adopting gesture recognizers in your app” 不再返回 404。它已经上线,迁移指导应以这份文档为准。1
- HIG 页面不再对 Mac 显示屏上的触控保持沉默。四个页面中有三个带有 10月28日 的更新日志行,Live Activities 页面没有。4
- 模拟清单有五条规则,而不是 6月技术说明中的四条;新增的是先按住再拖移这一条。18
- 只有 Info.plist 中没有该键的应用,排他视图才默认开启。在 Xcode 27.1 和 27.2 beta 中,Xcode 的 macOS App 和 Document App 模板会为应用生命周期为 AppKit 应用委托的项目把该键写为
NO。127
下午那篇文章关于 SDK、三个未入文档的开关和 ActivityKit 的说法,没有因为此后发布的任何内容而改变。
关键要点
对 AppKit 开发者: 在 Sidecar 上运行文章的四行测试,然后搜索它点名的五个符号;控件子类中对 mouseDown(with:)、mouseDragged(with:) 或 mouseUp(with:) 的重写,无论是否调用 super,都会让该控件退回到跟踪循环,修复方法是在 NSControl.Events 上使用 target-action 对,并在隐藏集合中加入 .trackingEndedOutside。1
对保留下来的循环: 仅限触控的识别器需要把 buttonMask 设为 0,否则鼠标拖移会启动识别器,您的 mouseDown(with:) 永远不会运行;allowedTouchTypes = .direct 本来就是默认值。1
对自定义识别器: 到达终止状态,重写 reset() 和 location(in:),调试时使用 -NSGestureRecognizerCrashOnMissingOverrides YES 和 -NSCrashOnStuckGestureTimeout 2,并在发布前移除;在正式环境中,卡住的识别器会在用户停止交互两秒后被取消。12
对排他性: 没有该键的应用是排他的;用 Xcode 的 macOS App 模板、以 AppKit 应用委托生命周期创建的项目(所有 Storyboard 或 XIB 项目)不是排他的,使用默认 SwiftUI 生命周期的项目则是。迁移跟踪循环之前,先读一读您的 Info.plist。27
对游戏: 从事件中读取位置,而不是累加增量;不要重写 sendEvent(_:);并要知道,在 NSIsTouchNative 设为 YES 之前,每个触控表面都是一个额外的模拟 GCMouse。3
对设计师: 触控是 “a secondary input mode”(一种辅助输入方式);保持布局和控件尺寸不变;扩大点击区域,而不是控件;在用户轻点附近位置时显示隐藏控件,在用户用完后或闲置几秒后再隐藏,边栏和标签页的关闭按钮是例外;并为关键控件提供上下文菜单。4
常见问题
AppKit 迁移文章上线了吗?
上线了。“Adopting gesture recognizers in your app” 在 10月28日 太平洋时间中午返回 404,到晚上 9:51 已经上线,列在 AppKit 的 Essentials 和 Gestures 两节中,并由落地页的文档板块链接。15
我的应用开启了排他视图吗?
如果您的 Info.plist 中没有 NSViewGestureRecognizerIsExclusive 键,那就是开启的:“If your app’s information property list doesn’t include the NSViewGestureRecognizerIsExclusive key, the system treats it as YES.”(如果应用的信息属性列表中没有这个键,系统会将其视为 YES。)2 在 Xcode 27.1 或 27.2 beta 中,用 macOS App 模板、以 AppKit 应用委托生命周期创建的项目(所有 Storyboard 或 XIB 项目),会以构建设置的形式得到值为 NO 的该键;同一模板在默认的 SwiftUI 生命周期下不写这个键(作者对这台 Mac 上模板的分析)。7
永远不结束的手势识别器会怎样?
它会一直占住自己的排他视图,直到系统将其取消:识别器 “is stuck if it hasn’t reached a terminal state within 2 seconds after the person stops interacting with your app”(如果在用户停止与应用交互后 2 秒内仍未到达终止状态,就算卡住),系统会 “automatically transitions it to the NSGestureRecognizer.State.cancelled state.”(自动把它转换到 cancelled 状态。)2 调试时设置 NSCrashOnStuckGestureTimeout 默认设置,即可改为得到一份堆栈跟踪。2
为什么加了平移识别器之后,我的跟踪循环收不到拖移了?
因为识别器的 buttonMask 处于默认值,占用了鼠标主按钮:“A mouse drag then begins the gesture recognizer, and AppKit doesn’t call your mouseDown(with:) override, so the tracking loop doesn’t run.”(于是鼠标拖移会启动手势识别器,AppKit 不再调用您重写的 mouseDown(with:),跟踪循环也就不会运行。)对于仅限触控的识别器,请把 buttonMask 设为 0。1
游戏在 Mac 上如何接收触控?
以模拟鼠标事件的形式,按文档规定的顺序,在每次 mouseDown(with:) 之前先有一个移到新位置的 mouseMoved(with:);同时以一个额外的 GCMouse 的形式,它的增量是不连续的。把 NSIsTouchNative 设为 YES,游戏就会改用手势识别器,模拟的 GCMouse 也随之移除。3
HIG 给出 Mac 上的触控目标尺寸了吗?
没有。Gestures 页面说 “Make tap targets bigger, not the controls themselves”(扩大点击区域,而不是控件本身),尺寸则交给框架控件决定;36 点这个数字出自两场演讲,而不是 HIG。49
本文的事实出自哪里?
Apple 的两篇 AppKit 文章、TN3216、三个 HIG 页面和落地页,均于 2026年10月28日 太平洋时间晚上 9:51 取回,其中文章的可用性、HIG 页面和落地页,与同一天中午和上午 11:49 的核对结果做了比较;文中点名的每个符号的 Apple 文档 JSON;用于比较的 6月技术说明;演讲引文所依据的 Tech Talks 字幕轨;以及我这台 Mac 上 Xcode 27.1 和 Xcode 27.2 beta 内的头文件与项目模板,这部分属于作者分析。媒体报道不作为来源。
参考来源
-
Apple Developer Documentation,Adopting gesture recognizers in your app,AppKit。取回于 2026年10月28日 太平洋时间晚上 9:51(HTTP 200,
Last-Modified: Wed, 28 Oct 2026 21:30:45 GMT);同一天太平洋时间中午,同一 URL 及其 JSON 端点返回 404。列在 AppKit 框架页面的 Essentials 一节中,排在 “Adopting Liquid Glass” 之后的第二位,并出现在 Gestures 页面的 “Gesture handling” 之下。Wayback Machine 的首次存档时间为 2026-10-29 01:08:12 UTC(CDX 行20261029010812,状态 200;原始响应头和 CDX 凭据与本文的证据一同保存)。引用章节:Overview、“Prepare your app for a Mac with a touch display”(表格、四行测试、搜索清单)、“Filter gesture recognizer candidates”、“Handle touch input”、“Update your event-handling code”、“Replace common mouse-down overrides”、“Handle tracking-loop compatibility”、“Replace your custom tracking loops with gesture recognizers”、“Add touch support to your custom tracking loop”、“Write a custom gesture recognizer”、“Account for gesture recognizer behavior changes” 和 “Support non-AppKit UI frameworks”。本文中的 Swift 代码清单,是把文章 “Add touch support to your custom tracking loop” 中的清单精简为四行。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation,Handling AppKit gestures,AppKit。取回于 2026年10月28日 太平洋时间晚上 9:51(
Last-Modified: Wed, 28 Oct 2026 05:35:41 GMT);页面没有修订历史;TN3212 没有链接它;截至该时间,Wayback Machine 的索引中没有它的存档。引用章节:Overview、“Track gesture recognizer states”、“Understand gesture recognizer gathering”、“Implement a custom gesture recognizer”、“Prioritize gesture recognizers”、“Delay responder events”、“Configure exclusive views”、“Replace event-tracking loops” 和 “Diagnose unresponsive gesture recognizers”。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation,TN3216: Supporting touch input for games on macOS,修订历史 “2026-10-28 First published”,取回于 2026年10月28日 太平洋时间晚上 9:51(
Last-Modified: Wed, 28 Oct 2026 21:09:50 GMT)。引用章节:Overview、“Extend the player experience meaningfully”、“Understand automatic touch support in existing games”、“Understand GCMouse emulation”、“Opt out of mouse emulation”、“Implement touch events”、“Route gestures in your game engine”、“Use standard AppKit controls for automatic touch support” 和 “Review your touch integration”。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer,Human Interface Guidelines,取回于 2026年10月28日 太平洋时间晚上 9:51,并与同一天太平洋时间中午取回的副本比较:Designing for macOS,更新日志 “October 28, 2026 | Added guidance for touch input.”;Gestures,更新日志 “October 28, 2026 | Added guidance for gestures and touch input in macOS.”,含 macOS 平台一节和表格 “macOS trackpad and touch display gestures”;Pointing devices,更新日志 “October 28, 2026 | Added guidance for revealing hidden controls with touch on Mac, and updated guidance and artwork for iPadOS and macOS.”,含 “Touch support for hidden controls” 一节;Live Activities,与中午的副本逐字节相同,更新日志止于 2026年10月13日。前三个页面的中午副本都不包含文中引用的文字。新 Gestures JSON 上服务器的
Last-Modified响应头为Tue, 27 Oct 2026 05:11:02 GMT,晚上 9:59 的第一次请求带有X-Cache: hit-stale。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer,Get ready for Touch on Mac,取回于 2026年10月28日 太平洋时间上午 11:49(三个板块都写着 “Coming soon”,链接在 HTML 注释中)和晚上 9:51:“Xcode 27.3 beta” / “Download the latest SDKs to build, run, and test your app.” / “Coming soon”;“Documentation resources” / “Learn how to build and optimize your app for touch input.”,链接到文章和示例;“Design resources” / “Create experiences that take advantage of touch user interface patterns.”,链接到 Gestures、Designing for macOS、Pointing devices 和 Apple Design Resources,后者的产品边框列表中包括 “MacBook Pro PROLED XDR Display”;两次抓取中都有注释
TK skill。Latest News RSS,最新条目 “Say hello to Touch on Mac”,Wed, 28 Oct 2026 10:00:55 PDT;Releases RSS,最新条目日期为 Mon, 26 Oct 2026 10:00:00 PDT(iOS、iPadOS、macOS 27.2 (26B123)、tvOS、visionOS 和 watchOS 27.2);Tech Talks 列表中有 111456、111467 和 111468,没有其他关于触控或 Live Activities 的演讲;Bring Touch to your Mac app 的页面没有链接任何一篇文章;以上均取回于太平洋时间晚上 9:51。 ↩↩↩↩↩↩↩↩↩ -
Apple 文档 JSON(
developer.apple.com/tutorials/data/documentation/<path>.json),取回于 2026年10月28日 太平洋时间晚上 9:51。找到的有:trackingEndedOutside(macOS 11.0)、NSEvent.isTouchSwipeNavigationEnabled(macOS 27.0)、NSTextSelectionManager.gesturesForFailureRequirements(macOS 27.0)、NSGestureRecognizerDelegate 上的gestureRecognizer(_:shouldRequireFailureOf:)(macOS 10.10)、NSViewGestureRecognizerIsExclusive(“The default value isYES.”)、NSIsTouchNative、Mouse, Keyboard, and Trackpad(关于响应者链与触控的两句话)、Supporting drag and drop through file promises(关于直接触控的那句话),以及 SwiftUI updates(Last-Modified: Wed, 28 Oct 2026 21:58:06 GMT,最新一节为 “September 2026”)。未找到(HTTP 404):appkit/nswindow/disablesdirecttouchsystemgestures、uikit/uiviewcontroller/prefersdirecttouchsystemgesturesdisabled、swiftui/view/directtouchsystemgesturesdisabled()、activitykit/alertconfiguration/announcement(_:)、xcode-release-notes/xcode-27_3-release-notes、macos-release-notes/macos-27_3-release-notes。ActivityKit:Last-Modified: Thu, 08 Oct 2026 15:31:35 GMT,平台为 iOS、iPadOS 和 Mac Catalyst。 ↩↩↩↩↩↩↩↩↩↩↩ -
2026年10月28日 的 SDK 与模板检查(作者分析),对象为 Xcode 27.1(27A9275)所带的
MacOSX27.0.sdk:NSControl.h第 180 行为NSControlEventTrackingEndedOutside = 1 << 7,Xcode 27.2 beta(27B5019j)所带的MacOSX27.2.sdk中同一行相同;NSEvent.h第 496 行为@property (class, readonly, getter=isTouchSwipeNavigationEnabled) BOOL touchSwipeNavigationEnabled API_AVAILABLE(macos(27));;NSTextSelectionManager.h第 59 行声明了gesturesForFailureRequirements;NSGestureRecognizer.h第 116 行和第 149 行声明了shouldRequireFailureOfGestureRecognizer:。项目模板用 plistlib 读取,对象为 Xcode 27.1(27A9275)和 Xcode 27.2 beta(27B5019j),位于Platforms/MacOSX.platform/Developer/Library/Xcode/Templates/Project Templates/macOS/Application/之下:Cocoa App Base.xctemplate/TemplateInfo.plist在Options[0] (appLifecycle) / Units / Cocoa / Targets[0] / SharedSettings处保存着键INFOPLIST_KEY_NSViewGestureRecognizerIsExclusive,字符串值为NO,别处都没有;Project Templates/macOS/Base/下的macOS App Lifecycle.xctemplate定义了appLifecycle选项,没有Name,Type为popup,Default为SwiftUI,Values为SwiftUI、Cocoa;macOS Application UI.xctemplate定义了userInterface,Name为Interface:,Default为SwiftUI,Values为SwiftUI、Storyboard、XIB,它的Storyboard和XIB单元,与它的一个SwiftUI单元一样,带有appLifecycle为Cocoa的RequiredOptions;App.xctemplate经由Core Data App、macOS SwiftUI App、macOS Application UI和Cocoa App Native Base派生自Cocoa App Base;Cocoa Document App.xctemplate在其Cocoa生命周期单元中写入一个Info.plist:NSViewGestureRecognizerIsExclusive节点,定义为<key>NSViewGestureRecognizerIsExclusive</key><false/>;Game.xctemplate把appLifecycle设为OverrideCocoa;Document App.xctemplate同时派生自Cocoa Document App和Cocoa App Base(经由Core Data Document App、macOS SwiftUI Document-Based App、macOS Application UI和Cocoa App Native Base)。在两个 Xcode 中,只有这两个模板文件包含该键。Xcode 的INFOPLIST_KEY_构建设置会写入生成的 Info.plist。 ↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation,TN3212: Adopting gesture recognizers for Sidecar touch support,修订历史 “2026-06-08 First published”,“Support third-party UI frameworks” 一节,四条要点:“A tap emulates a mouse-down followed immediately by a mouse-up”、“A touch and immediate pan emulates trackpad scroll events, even if the UI doesn’t respond to them”、“A long press without movement emulates a rightMouseDown(with:) (optionally followed by rightMouseDragged(with:)) with a rightMouseUp(with:) on touch lift” 和 “A two-finger pinch or rotation simultaneously emulates both the trackpad magnify and rotate gestures”。取回于 2026年10月28日。 ↩↩↩
-
Apple Developer,Tech Talks Bring Touch to your Mac app(13:19,Raleigh Ledet)和 Design for touch on Mac(16:52,Cas Lemmens),2026年10月28日。引文出自英文字幕轨;括号中的时间是所引片段的第一个字幕时间点:“drag with a small hold delay”(111468,11:31),“there’s a gesture modernization skill that has an understanding of the features and changes I’ve talked about”(111468,12:16),“Touch on macOS uses a touch target of 36 points”(111456,9:17),“a minimum of 36 points on each axis”(111468,5:53)。演讲页面的代码清单 “Use NSControl events to detect tracking”(2:24)为
[.trackingBegan, .trackingEntered]、[.trackingExited, .trackingEndedInside]和.trackingCancelled注册了addTarget(_:action:for:)。 ↩↩↩↩↩