Adopting hover support for Apple Pencil
At a glance
| Item |
Summary |
| Purpose |
Shows a distance-sensitive Apple Pencil hover preview and commits drawn strokes to a persistent bitmap. |
| App architecture |
A storyboard-hosted HoverDrawView owns gesture, path, preview, and rendering state; its view controller is only a host. |
| Main patterns |
Custom view state owner, gesture recognizer adapter, two-phase rendering, and lifecycle-managed graphics resources. |
| Project style |
Small UIKit target with drawing types grouped under Drawing/. |
Project structure
ApplePencilSampleApp/ApplePencilSampleApp/
├── AppDelegate.swift
├── SceneDelegate.swift
├── ViewController.swift
├── Drawing/
│ ├── HoverDrawView.swift
│ └── DrawGestureRecognizer.swift
└── Base.lproj/Main.storyboard
Structure observations
- The empty host controller confirms that interaction logic belongs to the reusable view.
- A custom long-press recognizer exists only to retain the current touch/event needed for coalesced Pencil samples.
Overall architecture
flowchart LR
Host["ViewController"] --> Canvas["HoverDrawView"]
Pencil["Apple Pencil input"] --> DrawGR["DrawGestureRecognizer"]
Pencil --> HoverGR["UIHoverGestureRecognizer"]
DrawGR --> Canvas
HoverGR --> Canvas
Canvas --> Live["CAShapeLayer live stroke or preview"]
Canvas --> Bitmap["CGContext committed strokes"]
Bitmap --> Layer["UIView.layer contents"]
HoverGR -->|zOffset| Preview["Preview opacity and tail"]
Preview --> Live
Reference code
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift:65 — the view creates and retains both recognizers because they drive one shared drawing state machine.
private func configureGestureRecognizers() {
if drawGestureRecognizer == nil {
let drawGesture = DrawGestureRecognizer(target: self, action: #selector(drawGesture(_:)))
drawGesture.minimumPressDuration = 0
addGestureRecognizer(drawGesture)
drawGestureRecognizer = drawGesture
}
if hoverGestureRecognizer == nil {
let hoverGesture = UIHoverGestureRecognizer(target: self, action: #selector(hoverGesture(_:)))
addGestureRecognizer(hoverGesture)
hoverGestureRecognizer = hoverGesture
}
}
Interpretation
Hover and draw update the same path/layer, but only completed drawing is flattened into the bitmap. The view guards hover while drawing so preview state cannot overwrite an active stroke.
Ownership and state
classDiagram
UIViewController *-- HoverDrawView : storyboard hierarchy
HoverDrawView *-- DrawGestureRecognizer : creates
HoverDrawView *-- UIHoverGestureRecognizer : creates
HoverDrawView *-- CAShapeLayer : creates live layer
HoverDrawView *-- CGContext : creates bitmap context
HoverDrawView *-- UIBezierPath : owns current path
DrawGestureRecognizer o-- UITouch : weak currentTouch
DrawGestureRecognizer o-- UIEvent : weak currentEvent
Ownership evidence
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift:23 — all mutable drawing and preview state is private to the canvas.
private var bitmapContext: CGContext?
private var bitmapContextSize = CGSize(width: 0, height: 0)
private var bezierPath = UIBezierPath()
private var shapeLayer: CAShapeLayer?
private var drawGestureRecognizer: DrawGestureRecognizer?
private var hoverGestureRecognizer: UIHoverGestureRecognizer?
private var isPreviewing = false
private var previewAlpha = 1.0
private var isDrawing = false
private var strokePathTail: [CGPoint] = []
| Owner |
Object or state |
Relationship |
Mutation authority |
| Storyboard hierarchy |
Host controller and drawing view |
Creates/retains |
UIKit lifecycle |
HoverDrawView |
Recognizers, paths, bitmap context, shape layer, mode flags |
Creates and retains |
Private view methods |
| Custom recognizer |
Current touch/event |
Weak observation during a gesture |
Touch overrides/reset |
| View layer |
Flattened bitmap contents and live sublayer |
Retains presentation resources |
Canvas rendering methods/Core Animation |
Class and protocol design
| Type |
Responsibility |
Depends on |
HoverDrawView |
Own input state, derive paths, render preview/live stroke, commit bitmap |
UIKit gestures, Core Graphics, Core Animation |
DrawGestureRecognizer |
Expose current touch/event for coalesced input |
UILongPressGestureRecognizer |
ViewController |
Host storyboard content |
UIViewController only |
No app-defined protocol abstraction is present. Subclassing is used where UIKit requires custom view and gesture behavior.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
| Rendering constants/state/helpers |
private |
Only HoverDrawView can alter preview/drawing invariants or graphics resources. |
The view is the sole interaction/rendering owner. |
DrawGestureRecognizer.currentTouch/currentEvent |
implicit internal weak |
Canvas can read them across files; recognizer does not prolong event/touch lifetime. |
Coalesced-touch bridge between recognizer and canvas. |
maxPreviewZOffset / fadeZOffset |
implicit internal constants |
Same-module code could tune thresholds. |
Sample-visible tuning points. |
| Types |
implicit internal |
Target-local, not a reusable framework API. |
App sample boundary. |
Reference code
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/DrawGestureRecognizer.swift:13 — only the bridge values cross the two-type boundary, and both are weak.
class DrawGestureRecognizer: UILongPressGestureRecognizer {
weak var currentTouch: UITouch?
weak var currentEvent: UIEvent?
override func reset() {
super.reset()
currentTouch = nil
currentEvent = nil
}
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Gesture installation/teardown |
HoverDrawView view lifecycle |
Resources exist only while attached to a hierarchy. |
| Coalesced Pencil sample access |
DrawGestureRecognizer |
UIKit sends raw touch/event there. |
| Draw and hover state transitions |
HoverDrawView gesture handlers |
Both modes share path/layer state. |
| Live preview/stroke |
CAShapeLayer helpers |
Fast transient rendering. |
| Persistent strokes |
CGContext helpers |
Completed paths are flattened into bitmap content. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Custom view state owner |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift:11 |
Keeps input and rendering invariants below the controller. |
| Gesture recognizer adapter |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/DrawGestureRecognizer.swift:13 |
Exposes coalesced-touch context absent from a plain action callback. |
| Two-phase rendering |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift:157 |
Uses a live vector layer, then commits finished drawing to bitmap. |
| Lifecycle-managed resources |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift:48 |
Creates when attached and clears graphics resources when detached. |
Naming conventions
HoverDrawView names both supported interaction modes; DrawGestureRecognizer names its focused adapter role.
- Boolean state uses
isPreviewing and isDrawing; thresholds use max... and fade....
- Rendering commands use
update, reset, and current to distinguish mutation from derivation.
Drawing/ groups the only feature-specific view and recognizer.
Architecture takeaways
- Put tightly coupled input and rendering state in the custom canvas instead of a passive host controller.
- Keep hover preview transient and commit only completed strokes to durable backing storage.
- Use weak references when exposing UIKit-owned touch/event objects beyond an override.
- Release bitmap/layer resources when the view leaves its hierarchy.
Source map
| Source file |
Relevant symbols |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/HoverDrawView.swift |
Input state machine and two-phase renderer |
ApplePencilSampleApp/ApplePencilSampleApp/Drawing/DrawGestureRecognizer.swift |
Current touch/event adapter |
ApplePencilSampleApp/ApplePencilSampleApp/ViewController.swift |
Passive host controller |
ApplePencilSampleApp/ApplePencilSampleApp/Base.lproj/Main.storyboard |
View hierarchy composition |