Build for the iPadOS pointer
Swift & UI 进阶 28m

为 iPadOS 指针交互构建体验

Build for the iPadOS pointer

2020年6月23日

在 Apple 官方观看视频

一句话判断

iPadOS 13.4 引入了触控板和鼠标支持,但很多 App 还没有适配指针交互。这个 Session 教你如何利用 UIPointerInteraction 让你的 App 在连接外设时有原生级的指针体验。

这场 Session 讲了什么

iPadOS 的指针系统设计理念是”indirect touch”——指针是手指的延伸而非传统桌面系统的鼠标指针。系统默认会为所有 UIKit 控件提供基本的指针反馈(比如悬浮时的高亮),但自定义的视图需要你手动适配 UIPointerInteraction。

Session 详细讲解了指针交互的三个层次。第一层是基本适配:为自定义视图添加 UIPointerInteraction,让指针悬浮时有视觉反馈。第二层是区域定制:通过 UIPointerRegion 精确定义哪些区域可以触发指针效果。第三层是自定义指针样式:通过 UIPointerStyle 完全控制指针的视觉效果,包括自动隐藏、缩放动画等。Session 还介绍了如何在 UICollectionView 和 UITableView 中利用指针增强交互体验。

值得深挖的点

指针的区域映射(Region Mapping)

iPadOS 指针的核心机制是”区域映射”:指针进入一个交互区域时,整个区域的形状会”吸附”到指针上,形成一个视觉上统一的”瞄准”效果。通过 UIPointerRegion 你可以精确控制哪些区域响应悬浮,还可以设置区域的 rect 来调整吸附范围。如果你不提供 Region,系统会使用视图的 bounds 作为默认区域。

UITargetedPointer vs UIPointerEffect

Apple 提供了两种内置的指针效果。UITargetedPointerEffect(原名 highlight 效果)会在目标视图上显示一个覆盖层,适合按钮等离散控件。UIPointerEffect(原名 lift 效果)会让目标视图轻微抬起并增加阴影,适合卡片和可拖拽的元素。正确选择效果类型对用户体验至关重要。

代码片段

为自定义视图添加指针交互

场景:为自定义的圆形按钮添加悬浮效果。

import UIKit

class CircleButton: UIView {
    private let pointerInteraction = UIPointerInteraction()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    private func setupView() {
        // 添加指针交互
        addInteraction(pointerInteraction)
        pointerInteraction.delegate = self
        
        // 基础样式
        backgroundColor = .systemBlue
        layer.cornerRadius = bounds.width / 2
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) 未实现")
    }
}

// 实现指针交互代理
extension CircleButton: UIPointerInteractionDelegate {
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            styleFor region: UIPointerRegion) -> UIPointerStyle? {
        // 创建目标区域
        let targetedPreview = UITargetedPreview(view: self)
        
        // 使用 highlight 效果
        let effect = UIPointerEffect.highlight(
            UITargetedPreview(view: self)
        )
        
        // 创建指针样式
        let style = UIPointerStyle(effect: effect)
        return style
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            regionFor request: UIPointerRegionRequest) -> UIPointerRegion? {
        // 定义可交互的圆形区域
        return UIPointerRegion(rect: bounds)
    }
}

在 UICollectionView 中增强指针体验

场景:为 CollectionView 的 Cell 添加悬浮效果。

class PhotoGridViewController: UIViewController,
                                UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView,
                        didSelectItemAt indexPath: IndexPath) {
        // 正常的选择处理
    }
}

// 通过 UICollectionViewDelegatePointerInteraction 定制 Cell 的指针效果
extension PhotoGridViewController: UIPointerInteractionDelegate {
    
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            styleFor region: UIPointerRegion) -> UIPointerStyle? {
        guard let cell = interaction.view as? UICollectionViewCell else {
            return nil
        }
        
        // 使用 lift 效果让 Cell 看起来可以被拿起
        let effect = UIPointerEffect.lift(UITargetedPreview(view: cell))
        return UIPointerStyle(effect: effect)
    }
}

自定义指针形状和隐藏行为

场景:在绘图 App 中隐藏系统指针并显示自定义光标。

class CanvasView: UIView {
    private let pointerInteraction = UIPointerInteraction()
    private var customCursorLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addInteraction(pointerInteraction)
        pointerInteraction.delegate = self
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) 未实现")
    }
}

extension CanvasView: UIPointerInteractionDelegate {
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            styleFor region: UIPointerRegion) -> UIPointerStyle? {
        // 隐藏系统指针,使用自定义的光标
        return UIPointerStyle(hiddenPointerStyle)
    }
    
    // 追踪指针位置来更新自定义光标
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            willEnter region: UIPointerRegion) {
        // 显示自定义十字光标
        showCrosshairCursor()
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction,
                            willExit region: UIPointerRegion) {
        // 隐藏自定义光标
        hideCrosshairCursor()
    }
}

最佳实践

已有项目:优先为所有可点击的自定义视图添加 UIPointerInteraction。UIKit 的标准控件(UIButton、UISwitch 等)已经内置了指针支持,但自定义的按钮和卡片需要手动适配。在 iPad 上测试你的 App,用触控板操作一遍所有核心流程,找出缺少指针反馈的地方。

新项目:从设计阶段就考虑指针交互。为不同类型的交互元素选择合适的指针效果——按钮用 highlight,卡片用 lift,绘图区域用自定义光标。确保指针效果不会与手势冲突。

还有什么值得关注

  • pointerInteraction.isEnabled 可以动态控制指针交互的开启和关闭。
  • iPadOS 14 新增了 pointer shapes API,可以为不同工具设置不同的指针形状(如画笔、橡皮擦)。
  • 搭配 Session 10094(Handle trackpad and mouse input)可以了解更多手势相关的适配细节。
WWDC 2020