Highlight
UIKit 可以用 UIHoverGestureRecognizer、prefersPointerLocked、allowedScrollTypesMask、buttonMask、modifierFlags 和 UIApplicationSupportsIndirectInputEvents 精确处理 iPad 与 Mac Catalyst App 中的触控板、鼠标滚轮、额外按键和键盘修饰键输入。
核心内容
iPadOS 13.4 让鼠标和触控板进入了普通 iPad App 的日常操作。用户把指针移到视频播放器边缘时,期待控制条自动出现;用两指滑动抽屉时,期待内容跟着移动;右键点击时,期待打开更直接的上下文操作。这些行为不能只靠触摸路径兜底,UIKit 为间接输入提供了单独的事件和手势入口。
这场 session 的主线很清楚:先让每个 App 处理常见输入,再给重度用户增加高级控制。常见输入包括 pointer movement、pointer lock、scroll input、pinch 和 rotate。高级控制包括鼠标按钮、键盘修饰键、手势接收过滤,以及从兼容模式切换到新的 indirect input 事件模型。
关键差别在于事件来源。手指触摸仍然走传统 UITouch 路径;触控板滚动会驱动 EventType.scroll;精确的捏合和旋转会走 EventType.transform;鼠标按钮和键盘修饰键会挂在 UIEvent 与 UIGestureRecognizer 上。App 需要在正确的层级读取这些信息,避免把所有交互都压回 touchesBegan 一条路径。
详细内容
1. 用 UIHoverGestureRecognizer 响应指针移动
(01:49)UIHoverGestureRecognizer 在 Mac Catalyst 的 Catalina 中引入,并在 iPadOS 中可用。它适合响应指针进入或离开某个 view 的边界,例如显示或隐藏视频播放控制条。修改指针外观或 hover effect 时,应使用 UIPointerInteraction。
let controlsHover = UIHoverGestureRecognizer(target: self, action: #selector(handleHover))
@objc func handleHover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began:
// Pointer entered our view - show controls
self.showsPlaybackControls = true
case .ended:
// Pointer exited our view - hide controls
self.showsPlaybackControls = false
default:
break
}
}
关键点:
UIHoverGestureRecognizer和其他 gesture recognizer 一样,通过 target-action 工作。.began对应指针进入当前 gesture view 的边界。.ended对应指针离开当前 gesture view 的边界。- transcript 明确区分了 gesture state 和 window 级别的 pointer phases,例如
RegionEntered、RegionMoved、RegionExited。
2. 用 prefersPointerLocked 申请 pointer lock
(05:33)iPadOS 14 和 Mac Catalyst Big Sur 增加了锁定指针的 API。典型场景是游戏:用户不想让指针在屏幕上移动,而是希望相对位移直接驱动视角或角色。App 通过 view controller 表达偏好,最终是否锁定由系统根据场景状态决定。
class GameViewController: UIViewController {
var shouldLockPointer: Bool = true
override var prefersPointerLocked: Bool {
return self.shouldLockPointer
}
func disablePointerLock() {
self.shouldLockPointer = false
self.setNeedsUpdateOfPrefersPointerLocked()
}
}
关键点:
prefersPointerLocked的默认值是false。- 需要锁定指针的 view controller 覆写该属性并返回当前偏好。
- 状态变化后调用
setNeedsUpdateOfPrefersPointerLocked(),让系统重新读取偏好。 - iPadOS 上 scene 需要全屏且处于
foregroundActive;Mac Catalyst 上 App 需要是 frontmost application。
(05:53)如果业务逻辑依赖真实锁定状态,读取 scene 的 pointerLockState,并观察 UIPointerLockState.didChangeNotification。session 强调,App 不能假设偏好一定会被满足。
if let pointerLockState = self.window.windowScene?.pointerLockState {
self.observer = notificationCenter.addObserver(forName: UIPointerLockState.didChangeNotification,
object: pointerLockState,
queue: OperationQueue.main) { (note) in
guard let lockState = note.object as? UIPointerLockState else { return }
gameEngine.performExpensiveOperationWhile(lockState.isLocked)
}
}
关键点:
pointerLockState可能为nil,表示当前 scene 不支持 pointer lock。isLocked是系统解析后的真实状态。- 当 Slide Over、Control Center、Notification Center 或前台 App 状态变化影响锁定条件时,通知会触发。
3. 给 UIPanGestureRecognizer 打开 scroll input
(09:54)UIScrollView 的 pan gesture recognizer 会处理所有 scroll input,但普通 UIPanGestureRecognizer 默认没有 scroll mask。自定义抽屉、pull-to-refresh、画布拖动这类交互,需要按场景设置 allowedScrollTypesMask。
// Enable scroll input for touch surface devices
self.drawerPan.allowedScrollTypesMask = [.continuous]
// Enable scroll input for scroll wheel devices as well
self.pullToRefreshPan.allowedScrollTypesMask = [.all]
关键点:
.continuous用于触控板这类 touch surface 的连续滚动。.all同时接收连续滚动和鼠标滚轮输入。- session 里的抽屉示例只接受连续滚动,因为设计师判断滚轮拉开侧边内容不自然。
- 自定义 pull-to-refresh 可以选择
.all,让触控板和滚轮都能触发。
4. 让 pinch 和 rotate 使用精确 transform 事件
(10:41)UIPinchGestureRecognizer 和 UIRotationGestureRecognizer 会自动处理触控板上的捏合和旋转。旧项目默认处在兼容模式,UIKit 会模拟两根手指的 touch。加入 UIApplicationSupportsIndirectInputEvents 后,App 会收到来自设备的 EventType.transform,手势精度更高。
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
关键点:
- 这个 Info.plist key 不是启用 pointer interaction、点击、scroll input 或 trackpad gestures 的前提。
- 它会启用
TouchType.indirectPointer和EventType.transform。 - 在 transform 事件驱动下,
numberOfTouches会返回0,locationOfTouch:inView可能抛出异常。 - 处理
UIPanGestureRecognizer、UIPinchGestureRecognizer、UIRotationGestureRecognizer时,要把依赖事件的判断迁移到新的shouldReceive方法。
5. 用 buttonMaskRequired 接收指定鼠标按钮
(14:48)UIEvent.ButtonMask 表示点击时按下的 pointing device 按钮。UITapGestureRecognizer 增加了 buttonMaskRequired,可以直接要求某个按钮才能触发手势。
self.thirdMouseButtonTap.buttonMaskRequired = .button(3)
关键点:
.button(3)返回第三个鼠标按钮对应的 mask。- 这种写法适合给专业用户增加额外操作,例如在编辑器里把第三键绑定到临时工具。
buttonMask同时存在于UIEvent和UIGestureRecognizer,但读取时机不同。
(15:07)键盘修饰键通过 UIKeyModifierFlags 表达,并暴露为 modifierFlags。gesture callback 中可以根据修饰键改变响应方式。
func handleHover(_ recognizer: UIHoverGestureRecognizer) {
// Show chapter controls if alt is pressed
let showChapterControls = recognizer.modifierFlags.contains(.alternate)
// ...
}
关键点:
.alternate对应 Option/Alt 修饰键。- 示例里同一个 hover gesture 可以在普通 hover 时显示播放控制,在按住 Option 时显示章节控制。
- 这类高级交互适合隐藏频率较低但效率很高的功能。
6. 在 shouldReceive(_ event:) 里过滤事件
(16:38)新的 UIGestureRecognizer 和 UIGestureRecognizerDelegate 方法允许手势在处理前接受或拒绝 UIEvent。session 特别提醒:这些方法发生在 gesture 完整处理事件之前,所以要读取 UIEvent 上的属性,而非 UIGestureRecognizer 上的滞后属性。
class SecondaryClickGesture: UIGestureRecognizer {
override func shouldReceive(_ event: UIEvent) -> Bool {
// Must look at the event’s mask, not the gesture’s
return event.buttonMask == .secondary
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// Touch handling code ...
}
}
关键点:
shouldReceive(_ event:)可以让自定义 gesture 只接收 secondary click。- 这个阶段
event.buttonMask是最新值。 gestureRecognizer(shouldReceive touch:)不会覆盖 scroll 或 transform 这类非 touch 事件。
(17:36)如果要让 Control + 主键点击等价于 secondary click,可以同时检查 buttonMask 和 modifierFlags。
class SecondaryClickGesture: UIGestureRecognizer {
override func shouldReceive(_ event: UIEvent) -> Bool {
// Must look at the event’s properties, not the gesture’s
let secondaryClick = event.buttonMask == .secondary
let controlClick = event.buttonMask == .primary && event.modifierFlags == .control
return secondaryClick || controlClick
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// Touch handling code ...
}
}
关键点:
secondaryClick覆盖右键或两指点击。controlClick覆盖主按钮加 Control 修饰键。- 这个模式适合上下文菜单、检查器、专业编辑工具等操作。
(18:10)delegate 方法也可以过滤 hover 事件。下面的示例只在按住 Option/Alt 时接收 closed caption hover。
let ccHover = UIHoverGestureRecognizer(target: self,
action: #selector(handleClosedCaptionHover))
ccHover.delegate = self
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive event: UIEvent) -> Bool {
if gestureRecognizer == self.closedCaptionHover {
return event.modifierFlags.contains(.alternate)
}
return true
}
关键点:
- gesture delegate 可以为不同 gesture 定义不同过滤条件。
event.modifierFlags.contains(.alternate)是接收 closed caption hover 的唯一条件。- 其他 gesture 返回
true,保持原有事件流。
核心启发
1. 视频播放器的指针浮层
做什么:指针进入播放区域时显示控制条,按住 Option/Alt 时显示章节或字幕入口。
为什么值得做:session 的 hover 示例和 modifier flags 示例正好覆盖这两个交互层级。
怎么开始:给播放器 view 加 UIHoverGestureRecognizer,在 callback 中读取 recognizer.state 和 recognizer.modifierFlags。
2. 游戏或 3D 视图的 pointer lock 模式
做什么:在全屏游戏、3D 建模或地图漫游场景中,把鼠标相对位移用于视角控制。
为什么值得做:prefersPointerLocked 和 UIPointerLockState.isLocked 让 App 能表达锁定偏好,并按系统解析后的状态调整渲染或输入逻辑。
怎么开始:在主 view controller 覆写 prefersPointerLocked,观察 UIPointerLockState.didChangeNotification,在 isLocked 为 true 时启用相对位移控制。
3. 自定义抽屉和画布的触控板滚动
做什么:让侧边抽屉、时间线、画布平移、pull-to-refresh 支持触控板两指滑动和鼠标滚轮。
为什么值得做:普通 UIPanGestureRecognizer 默认不接收 scroll input,用户会遇到触控板能滚列表却拖不动自定义区域的问题。
怎么开始:为每个自定义 pan gesture 设置 allowedScrollTypesMask,需要触控板连续滚动时用 .continuous,需要滚轮支持时用 .all。
4. 专业工具的多按钮快捷操作
做什么:在绘图、剪辑、IDE、表格类 App 中,把第三鼠标键、右键、Control + 点击映射到不同工具或上下文操作。
为什么值得做:buttonMaskRequired、event.buttonMask 和 event.modifierFlags 能把精确输入交给对应 gesture,减少模式切换。
怎么开始:简单 tap 用 UITapGestureRecognizer.buttonMaskRequired;复杂过滤写在 shouldReceive(_ event:) 或 delegate 的 gestureRecognizer(_:shouldReceive:) 中。
5. 从触摸兼容模式迁移到 indirect input
做什么:让触控板捏合和旋转直接走 EventType.transform,并用 TouchType.indirectPointer 区分鼠标点击和手指点击。
为什么值得做:旧兼容模式会模拟 touch,精确手势可能误触其他 recognizer;新的 indirect input 路径能让手势语义更清楚。
怎么开始:在 Info.plist 加 UIApplicationSupportsIndirectInputEvents,检查依赖 numberOfTouches、locationOfTouch:inView 和 gestureRecognizer(shouldReceive touch:) 的代码。
关联 Session
- Build for the iPadOS pointer — Customize pointer effects, regions, and shapes with UIPointerInteraction so iPad controls respond to hover.
- Design for the iPadOS pointer — Apply iPadOS pointer design principles, effects, shapes, gestures, and keyboard modifiers to app workflows.
- Bring keyboard and mouse gaming to iPad — Use Game Controller keyboard and mouse events, pointer lock, and full-screen behavior for iPad games.
- Support hardware keyboards in your app — Use the responder chain, UIKeyCommand, modifier flags, and raw keyboard events in iPad and Mac Catalyst apps.
评论
GitHub Issues · utterances