10 月 28 日,苹果发布了搭载 PROLED XDR Display 的 MacBook Pro,这是第一款带触摸屏的 Mac,运行 macOS 27 Golden Gate。关于 MacBook Pro 触摸屏,新闻稿写给开发者的一句话是:很多现有应用无需改动就能使用触控,因为 macOS 会自动把触摸转换成点击。对按钮、表格和 SwiftUI 手势来说,这是对的。对在 mouseDown(with:) 里写跟踪循环的自定义视图,只对了一半。这样的视图收到的是模拟出来的鼠标:点击、滚动,长按时的右键。它看不到手指本身,系统接管手势时也没有办法撤回操作。
苹果发布了三个 Tech Talk 和一篇技术说明 TN3212。下面整理它们对代码的影响,以及做 MacBook Pro 触摸屏支持该从哪里着手。先把两次发布区分清楚:10 月 27 日发布的 14 英寸 M6 MacBook Pro 没有触控。触控只属于搭载 M5 Pro 或 M5 Max 的 14 英寸和 16 英寸 PROLED XDR Display 机型,10 月 28 日开始预购,11 月 6 日开始发货。
MacBook Pro 触摸屏:哪些无需修改#
苹果把按钮、控件、表格、集合视图、拆分视图、标签页和滚动称为“touch native”:AppKit 用手势识别器构建它们,所以手指的表现与点击一致。按演讲所说,现有的 SwiftUI 和 Mac Catalyst 手势“just work”。
其余部分由鼠标模拟兜底。TN3212 针对第三方 UI 框架和手写的 mouseDown(with:) 代码描述如下:
| 触摸 | 应用收到的事件 |
|---|---|
| 轻点 | mouseDown,随后 mouseUp |
| 立即移动的触摸 | 触控板式的滚动事件 |
| 不移动的长按 | rightMouseDown,移动则 rightMouseDragged,抬起时 rightMouseUp |
| 双指捏合或旋转 | 同时产生 magnify 和 rotate |
演讲还补充:单指带惯性滚动,按住片刻后才开始拖动。有一条设计事实同样重要:在带触控的机器上,macOS 看起来没有变化。窗口、文字和控件尺寸不变,触控只是次要输入。
Catalyst 应用仍用旧的 UITouch.touchType 和 UIGestureRecognizer.allowedTouchTypes 区分鼠标与触摸。macOS 27 还修复了一个 Catalyst 缺口:同时带有 UIDragInteraction 和 UIContextMenuInteraction 的视图,现在用手指也能触发两者。
直接触摸只会送到手势识别器#
AppKit 工程师 Raleigh Ledet 的原话:“Gesture recognizers are the only way to get direct touches on macOS.”TN3212 写明了后果:本地和全局事件监视器收不到触摸,触摸不经过 NSApp 或 sendEvent(_:),也不走响应者链。键盘和滚轮事件仍然走响应者链。
苹果自己的最小示例,是只接受手指的 pan 识别器:
let touchRecognizer = NSPanGestureRecognizer(target: self, action: #selector(touchPan))
touchRecognizer.buttonMask = 0x0
touchRecognizer.allowedTouchTypes = .direct
wiringPanelView.addGestureRecognizer(touchRecognizer)allowedTouchTypes 的类型是 NSTouch.TouchTypeMask,从 macOS 10.12.2 起就有,并不新。变化在于:它现在是唯一的入口。
TN3212 里有两个陷阱。识别器只能加到视图上,想在视图控制器里接收触摸,就加到它的 view 上。另外,AppKit 从手指下最顶层的视图向上收集候选识别器,所以一个透明的重叠视图会挡住识别器,包括标准控件内部的那些。在该透明视图里重写 hitTest(_:) 并返回 nil。
跟踪循环可以保留,在旁边增加仅限触摸的识别器即可。苹果称这是“the fastest and least risky path for a dot release”。对较大的版本,它建议把跟踪拆成 start、continue、end、cancel 四个函数,让两条路径都调用它们。
用 NSControl 事件取代 mouseDown 跟踪#
对自己的按钮和滑块,苹果的路线是用 addTarget(_:action:for:) 订阅 NSControl.Events,而不是在 mouseDown 里写循环。演讲中的示例:
ignitionButton.addTarget(self, action: #selector(buttonDidStartTracking), for: [.trackingBegan, .trackingEntered])
ignitionButton.addTarget(self, action: #selector(buttonDidEndTracking), for: [.trackingExited, .trackingEndedInside])
ignitionButton.addTarget(self, action: #selector(buttonCancelledTracking), for: .trackingCancelled)第三行在鼠标时代没有对应物。当 Exposé 之类的系统手势或滚动打断跟踪时,会触发 trackingCancelled,在这里撤销做了一半的改动。此外还有 valueChanged、primaryActionTriggered、menuActionTriggered,以及聚合项 allTrackingEvents 和 allEvents。
三个提醒。如果继承标准控件并重写任何左键响应者方法,它会退回到跟踪循环;NSTableView 和 NSCollectionView 同理,苹果建议改用代理方法。迁移之后,NSApplication.currentEvent 不再返回触发动作的事件,请改读 NSEvent.modifierFlags 和 NSEvent.pressedMouseButtons,而触摸不会出现在 pressedMouseButtons 里。最后,版本标注有些混乱:TN3212 把 trackingRepeated、valueChanged、primaryActionTriggered、menuActionTriggered 和 applicationReserved 列为 macOS 27 新增,而文档把 NSControl.Events 本身标为 macOS 11。请逐个成员确认可用性。
让多个手势同时进行#
默认情况下,只有一个视图层级可以有活动的识别器。这是在模仿模态跟踪循环,滚动是例外。对于手指同时操作多个对象的面板(设计演讲举的例子是 Logic 里的两个滑块),要关掉它:
stoveControlsView.exclusiveGestureBehavior = .notExclusiveNSView.exclusiveGestureBehavior(macOS 27.0)默认 .inherit,也可以是 .exclusive 或 .notExclusive,子视图继承该值。AppKit 标准的颜色面板已经允许同时拖动多个滑块。三个 Info.plist 键,都是 macOS 27.0:
| 键 | 类型 | 作用 |
|---|---|---|
NSViewGestureRecognizerIsExclusive | Boolean,默认 YES | 应用级默认值;NO 允许多个视图的识别器同时激活 |
NSGestureRecognizerSuppressesMainMenuActions | Boolean | 非独占手势进行时是否忽略菜单快捷键;独占手势总会抑制 |
NSIsTouchNative | Boolean | 对内嵌非 AppKit UI 的应用,阻止 AppKit 注入鼠标模拟识别器;苹果:“Most AppKit apps don't need this key” |
如果应用里没有需要两个手势同时进行的界面,这些都不用动。
触控目标:演讲说 36 pt,HIG 写 44 pt#
设计师 Cas Lemmens 给出了数字:“Touch on macOS uses a touch target of 36 points.”它在屏幕上的物理大小与 iOS 的 44 点相同。框架控件带有不可见的 36 pt 触控目标,视觉尺寸不变。有一处出入需要知道:已发布的 HIG 按钮页面至今仍要求不小于 44x44 pt 的点击区域。36 pt 出自设计 Tech Talk 和苹果示例代码里的一条注释,没有进入指南。
对自定义控件,请放大触控的点击区域而不是控件本身,并且只在触摸路径上放大。苹果的示例里,expandHitRect 逻辑由触摸识别器调用,所以鼠标仍然精确。FormStyle 的行默认 36 pt,检查器沿用中号控件,小号和迷你控件同样“fully support touch”。
hover 没有触摸对应物。工具提示不必支持触摸,悬停才出现的滚动按钮在触摸时保持隐藏。只能靠悬停出现的重要操作,需要另一条路径,比如上下文菜单。苹果在 AppKit 中的做法,是装一个仅限触摸的点击识别器,轻点时临时显示该元素。
滚动视图:绘图画布用两根手指#
macOS 27 给 NSScrollView 增加了 isTouchScrollingEnabled(默认 true)、minimumNumberOfTouchesForScrolling(默认 1)、maximumNumberOfTouchesForScrolling,以及用于设置与滚动的失败或互斥关系的 scrollGestureForRelationships。对于一根手指绘制、同时仍需滚动的画布,苹果的示例只有一行:
scrollView.minimumNumberOfTouchesForScrolling = 2另有用于下拉刷新的 NSRefreshController,通过 NSScrollView.refreshController 接入,带有 target/action、beginRefreshing() 和 endRefreshing()。需要给滚动让路的自定义识别器可以设置 isCancellableByScrollGesture,默认是 false。
在 SwiftUI 里指明输入来源#
GestureInputKinds(macOS 27.0)是一个选项集:.directTouch、.indirectTouch(触控板多点触控、Siri Remote)、.pointer、.pencil 和 .all。带 inputKinds 的初始化方法存在于 DragGesture、LongPressGesture、MagnifyGesture、RotateGesture、SpatialTapGesture、TapGesture 等。手指绘制、鼠标选择的画布示例:
let draw = DragGesture(minimumDistance: 0, coordinateSpace: .local, inputKinds: .directTouch)
let select = DragGesture(minimumDistance: 4, coordinateSpace: .local, inputKinds: .pointer)如果已经有自定义的 NSGestureRecognizer,用 NSGestureRecognizerRepresentable 包装给 SwiftUI 即可,不必重写。它的方法是 makeNSGestureRecognizer(context:)、updateNSGestureRecognizer(_:context:)、handleNSGestureRecognizerAction(_:context:) 和 makeCoordinator(converter:),用 .gesture(_:) 附加。文档把它标为 macOS 26.0,而 WWDC26 称它是新的,所以写“可用”比写“新增”稳妥。
系统手势会先拿走手指#
设计演讲列出了屏幕上的手势:三指上滑是调度中心,下滑是 App Exposé,左右滑切换空间;拇指加三指捏合显示 App,张开显示桌面。从上边缘下滑呼出隐藏的菜单栏,从下边缘上滑呼出隐藏的程序坞,从右边缘向左滑打开通知中心,从右上边缘下滑打开控制中心。在灵动岛上滑动会唤起 Siri。按新闻稿所说,双击窗口可填满屏幕,轻扫可平铺窗口。
实际要注意的是:同时向同一方向移动三个或更多滑块,会触发系统手势。对少数被它妨碍的窗口,苹果的示例代码给出了三个关闭方式:AppKit 的 NSWindow.disablesDirectTouchSystemGestures,Catalyst 的 UIViewController.prefersDirectTouchSystemGesturesDisabled,SwiftUI 的 View.directTouchSystemGesturesDisabled()。它们只出现在示例代码里,目前还没有文档页面。苹果的建议是:“only if the interaction capabilities they open up are critical for the window”才关闭,并且最好只在关键时刻关闭。
显示屏与灵动岛,简述#
ProMotion 在 10 至 120 Hz 之间自适应。亮度为全屏持续最高 1000 尼特、峰值 2000 尼特(SDR 为户外场景,XDR 为 HDR 内容)。EDR 的 API 没有变化,NSScreen.maximumExtendedDynamicRangeColorComponentValue 和 CAMetalLayer.wantsExtendedDynamicRangeContent 照旧可用。文档中看不到面向 AppKit 的新灵动岛几何 API,现有的是 safeAreaInsets 和 auxiliaryTopLeftArea。关于烧屏,苹果只说显示引擎会补偿像素的使用损耗,没有给开发者的指南。
关于 Mac 上的 Live Activities,Tech Talk 说,已有 Live Activities 的通用应用无需改代码即可运行,原生 macOS 应用可以直接创建。在带灵动岛的 Mac 上它们位于顶部边缘,其他 Mac 则显示在菜单栏。但 ActivityKit 文档尚未列出 macOS,苹果示例里的 alertConfiguration: .announcement(...) 也不在 AlertConfiguration 的成员列表中。等文档出来后再依赖它。
没有新 Mac 怎么测试#
苹果说明,装有最新 macOS 测试版和开发者工具时,通过随航(Sidecar)连接的 iPad 可以使用全部触控功能。在 macOS 27 上,来自运行 iPadOS 27 的随航 iPad 的触摸,同样只能通过手势识别器获得。
小心 NSScreen.touchCapabilities(.multiTouch):连接支持触控的随航显示器后,它对所有显示器都返回 true。苹果建议不要依据它分支,而是不论来源都处理输入。
两个调试用的 defaults:NSCrashOnStuckGestureTimeout 会在卡住的独占手势于输入结束数秒后被自动取消时让应用崩溃,NSGestureRecognizerCrashOnMissingOverrides 会标出缺少必需重写的子类。重新编译时还有一个陷阱:用 macOS 27 SDK 构建的应用里,如果子类没有重写,基类的 NSGestureRecognizer.location(in:) 会返回 NSZeroPoint 并记录错误。自定义识别器必须到达 .ended、.failed 或 .cancelled,否则会阻塞其他识别器,请实现 mouseCancelled(with:) 和 touchesCancelled(with:)。
这里的新符号都是 macOS 27.0,如果还支持旧系统,请把调用包在 #available(macOS 27, *) 里。
第一周清单#
- 用 macOS 27 SDK 重新构建,找出没有重写
location(in:)的NSGestureRecognizer子类。 - 在调试 scheme 中打开
NSCrashOnStuckGestureTimeout和NSGestureRecognizerCrashOnMissingOverrides。 - 用随航连接 iPad,用手指走一遍主要界面:轻点、拖动、长按、滚动。
- 找出基于
mouseDown的自定义跟踪,在旁边加一个allowedTouchTypes = .direct的仅触摸识别器。 - 把自制控件的跟踪迁移到
NSControl.Events,并处理trackingCancelled。 - 只在触摸路径上,把偏小的自定义控件点击区域放大到 36 pt。
- 找出只能靠 hover 使用的操作,为它们提供第二条路径。
- 检查有同时手势的窗口,判断是否需要
.notExclusive。
苹果还没解释的部分#
developer.apple.com/touch-on-mac/ 这个页面在发布当天看起来并未完成。“Documentation resources”和“Design resources”写着“Coming soon”,Xcode 27.3 beta 也是。文章“Adopting gesture recognizers in your app”在 HTML 里被注释掉,返回 404。HIG 没有 Mac 触控的页面,Gestures 页面仍写着 Mac 用户依靠键盘、鼠标和触控板。关闭系统手势的 API 没有文档,原生 macOS 应用能否使用 Live Activities 也未体现在文档中。演讲提到开发者工具里有一个“gesture modernization skill”,目前还找不到。有疑问可参加 11 月 18 日(太平洋时间上午 9 至 10 点)和 11 月 19 日(太平洋时间下午 6 至 7 点)的小组答疑。
Hacker News 上的反应主要围绕指纹、手臂疲劳和缺少平板模式,做移动开发的人则看好直接在 Mac 屏幕上测试 iOS 手势。这些都不改变上面的清单,但与苹果自己的设计立场一致:触控是次要输入,手势也应该能在触控板和菜单中使用。
如果你正在为新材质更新界面,可以对照SwiftUI 中的 Liquid Glass 适配。适配新硬件的类似经历见 iPhone Duo 适配指南,菜单栏应用可以看用 SwiftUI 写 macOS 菜单栏应用。一手资料:Touch on Mac、TN3212,以及 Tech Talk Design for touch on Mac、Bring Touch to your Mac app 和 Bring Live Activities to Mac。



