WWDC Quick Look 💓 By SwiftGGTeam
Squeeze the most out of Apple Pencil

Squeeze the most out of Apple Pencil

观看原视频

Highlight

iPadOS 18 的 PencilKit 工具栏终于支持自定义工具——你的印章、橡皮、修图工具可以直接放进系统工具栏,并自动获得 Apple Pencil Pro 的 squeeze、barrel roll 和触觉反馈。

核心内容

做绘画 App 的开发者一直面临一个选择:用 PencilKit,工具栏体验好但工具种类受限于系统内置的笔、橡皮、套索;自己做画布和工具栏,工具灵活了,但 squeeze、hover 预览、barrel roll 这些 Apple Pencil Pro 特性要从零实现,工作量巨大。

今年 Apple 给出了一个折中方案——PKToolPickerCustomItem。你可以把自定义工具注册到 PencilKit 工具栏里,用户在系统工具栏中就能切换你的工具。自定义工具选中时,PencilKit 的画布自动停止绘制,你的代码接管渲染。工具栏的颜色选择器、宽度滑块依然可用,你还能传入自己的 UIViewController 来放额外控件。barrel roll、squeeze 这些交互,自定义工具自动获得,零额外代码。

同时,Apple Pencil Pro 带来了三个新的交互能力:squeeze 手势(捏笔杆触发)、barrel roll 角度(转笔杆控制笔触方向)、以及基于位置信息的触觉反馈(对齐辅助线和形状识别时给手感)。PencilKit 内置工具自动支持这些,自定义画布也可以通过新的 UIPencilInteractionUICanvasFeedbackGenerator 和 SwiftUI 的 .onPencilSqueeze.sensoryFeedback 来接入。

详细内容

工具栏自定义:toolItems 初始化器

从 iOS 13 起,PKToolPicker 只有默认的全套工具。iPadOS 18 新增了 toolItems 初始化器,让你指定工具栏中工具的顺序和种类(02:39)。你可以放多个同类型的墨水工具——比如一支红笔、一支蓝笔——这对标注类 App 非常实用。

工具栏条目类型包括:PKInkingToolItem(墨水)、PKEraserToolItem(橡皮)、PKLassoToolItem(套索)、PKRulerItem(标尺,点击切换开关)、PKScribbleItem(手写转文字)、PKToolPickerCustomItem(自定义工具),还有一个尾部 UIBarButtonItem 用来放非绘制操作(04:55)。

自定义工具:PKToolPickerCustomItem

自定义工具的核心是 PKToolPickerCustomItem 及其 Configuration 结构体(06:10)。Configuration 定义工具支持的属性——颜色、宽度、不透明度由 PencilKit 提供控件,额外属性通过你传入的 UIViewController 展示。工具图标由你提供一个闭包来渲染,当颜色、宽度或不透明度变化时 PencilKit 自动调用该闭句更新图标;自定义属性变化时你需要手动调用 reloadImage()

选中自定义工具后,所有关联的 PKCanvasView 自动停止绘制,你的代码接管触摸事件和渲染。PencilKit 只负责工具选择 UI。

Squeeze 手势

Apple Pencil Pro 的 squeeze 手势让用户不用抬手就能调出工具栏或上下文面板(09:20)。系统有一个全局偏好设置,决定 squeeze 触发什么:显示上下文调色板、执行快捷指令、或自定义行为。如果用户选了快捷指令,你的 App 收不到 squeeze 事件。

UIKit 中响应 squeeze:

class MyViewController: UIViewController, UIPencilInteractionDelegate {

    func pencilInteraction(_ interaction: UIPencilInteraction,
               didReceiveSqueeze squeeze: UIPencilInteraction.Squeeze) {

        if UIPencilInteraction.preferredSqueezeAction == .showContextualPalette &&
           squeeze.phase == .ended {
           let anchorPoint = squeeze.hoverPose?.location ?? myDefaultLocation
           presentMyContextualPaletteAtPosition(anchorPoint)
        }
    }
}
  • UIPencilInteractionDelegatepencilInteraction(_:didReceiveSqueeze:) 方法接收 squeeze 事件(10:24
  • preferredSqueezeAction 检查用户在系统设置中的偏好
  • squeeze.phase == .ended 确保只在手势结束时触发,避免重复响应
  • squeeze.hoverPose?.location 获取 squeeze 时笔尖悬停位置,用来定位面板

SwiftUI 中响应 squeeze:

@Environment(\.preferredPencilSqueezeAction) var preferredAction
@State var contextualPalettePresented = false
@State var contextualPaletteAnchor = MyPaletteAnchor.default

var body: some View {
    MyView()
        .onPencilSqueeze { phase in
            if preferredAction == .showContextualPalette, case let .ended(value) = phase {
                if let anchorPoint = value.hoverPose?.anchor {
                    contextualPaletteAnchor = .point(anchorPoint)
                }
                contextualPalettePresented = true
            }
        }
}
  • @Environment(\.preferredPencilSqueezeAction) 读取系统偏好(10:46
  • .onPencilSqueeze 修饰符接收 squeeze 事件,phase 区分手势阶段
  • value.hoverPose?.anchor 获取悬停锚点,用于定位面板弹出位置

触觉反馈:UICanvasFeedbackGenerator

Apple Pencil Pro 的触觉反馈新增了两种画布专用类型:对齐辅助线(alignment)和路径完成(pathComplete)(11:36)。反馈 API 现在需要传入位置信息,让系统根据上下文选择最佳反馈方式。

UIKit 中使用:

class MyViewController: UIViewController {
    @ViewLoading var feedbackGenerator: UICanvasFeedbackGenerator

    override func viewDidLoad() {
        super.viewDidLoad()
        feedbackGenerator = UICanvasFeedbackGenerator(view: view)
    }

    func dragAlignedToGuide(_ sender: MyDragGesture) {
        feedbackGenerator.alignmentOccurred(at: sender.location(in: view))
    }

    func snappedToShape(_ sender: MyDrawGesture) {
        feedbackGenerator.pathCompleted(at: sender.location(in: view))
    }
}
  • UICanvasFeedbackGenerator(view:) 初始化时传入画布视图(11:50
  • alignmentOccurred(at:) 在线条对齐辅助线时触发触觉反馈
  • pathCompleted(at:) 在形状识别完成、笔画吸附到规整形状时触发反馈

SwiftUI 中使用:

@State var dragAlignedToGuide = 0
@State var snappedToShape = 0

var body: some View {
    MyView()
        .sensoryFeedback(.alignment, trigger: dragAlignedToGuide)
        .sensoryFeedback(.pathComplete, trigger: snappedToShape)
}
  • .sensoryFeedback(.alignment, trigger:) 在 trigger 值变化时触发对齐反馈(12:29
  • .sensoryFeedback(.pathComplete, trigger:) 在 trigger 值变化时触发路径完成反馈
  • 将修饰符挂在最具体的子视图上,反馈更精准

Barrel roll 角度

Apple Pencil Pro 的 barrel roll 让转笔杆能控制笔触角度(08:04)。PencilKit 的 marker 和 fountain pen 已内置支持。自定义画布可以通过 UITouch.rollAngleUIHoverGestureRecognizer.rollAngle 读取角度值。Apple 建议将 rollAngle 和 azimuth 角度相加使用,这样在不支持 roll 的设备上 azimuth 还能作为回退(13:32)。注意 rollAngle 初始是估算值,后续通过蓝牙更新,需要在 touchesEstimatedPropertiesUpdated 中获取最终精确值。

核心启发

  • 做什么:把自定义绘制工具注册到 PKToolPickerCustomItem 中。为什么值得做:用户在系统工具栏里就能切换你的工具,squeeze 和 barrel roll 自动支持,不用自己实现交互逻辑。怎么开始:创建 PKToolPickerCustomItem.Configuration,定义颜色/宽度属性,提供图标渲染闭包,然后用 toolItems 初始化器构建工具栏。

  • 做什么:在自定义画布中接入 squeeze 手势,弹出上下文工具面板。为什么值得做:用户不抬手就能切换工具或调出选项,绘制流程不中断。怎么开始:UIKit 中实现 UIPencilInteractionDelegatepencilInteraction(_:didReceiveSqueeze:);SwiftUI 中使用 .onPencilSqueeze 修饰符。记得检查 preferredSqueezeAction 尊重系统设置。

  • 做什么:在形状识别和对齐吸附时触发触觉反馈。为什么值得做:反馈让用户在视觉确认之前就能感知到吸附成功,手感更自然。怎么开始:UIKit 用 UICanvasFeedbackGenerator,调用 alignmentOccurred(at:)pathCompleted(at:);SwiftUI 用 .sensoryFeedback(.alignment, trigger:).sensoryFeedback(.pathComplete, trigger:)

  • 做什么:在自定义画笔工具中读取 barrel roll 角度,控制笔触旋转。为什么值得做:模拟真实画笔的转笔效果,让数字绘画手感更接近纸笔。怎么开始:从 UITouch.rollAngle 读取角度,加上 azimuthAngle 作为总旋转值,在 touchesEstimatedPropertiesUpdated 中更新最终精确值。

关联 Session

评论

GitHub Issues · utterances