At a glance
| Item |
Summary |
| Purpose |
Builds a finger/Pencil drawing pipeline using precise, coalesced, predicted, force, altitude, azimuth, and estimated touch values. |
| App architecture |
CanvasMainViewController composes recognizers, model, renderer, scroll container, and tools; custom recognizers create Stroke data consumed by StrokeCGView. |
| Main patterns |
Input adapter, explicit stroke model, sequence-based segment traversal, rendering strategies, and dirty-rectangle updates. |
| Project style |
Storyboard screen with feature-specific model, input, renderer, geometry, canvas, and tool-control files. |
Project structure
SpeedSketch/
├── CanvasMainViewController.swift
├── StrokeGestureRecognizer.swift
├── StrokeCollection.swift
├── CGDrawingEngine.swift
├── CanvasContainerView.swift
├── RingControl.swift
├── RingView.swift
├── CGMathExtensions.swift
└── AppDelegate.swift
Structure observations
- Input capture, stroke data, and Core Graphics rendering are separate types even though one controller composes them.
StrokeCollection.swift groups aggregate/value/segment/iterator types that evolve together.
- Ring-control files own tool selection UI;
CGMathExtensions.swift keeps vector arithmetic out of rendering logic.
Overall architecture
flowchart LR
Touch["Finger or Pencil UITouch"] --> FingerGR["finger StrokeGestureRecognizer"]
Touch --> PencilGR["pencil StrokeGestureRecognizer"]
Event["UIEvent"] --> Coalesced["coalesced samples"]
Event --> Predicted["predicted samples"]
Coalesced --> FingerGR
Predicted --> PencilGR
FingerGR --> Stroke["Stroke"]
PencilGR --> Stroke
Stroke --> Controller["CanvasMainViewController"]
Controller --> Collection["StrokeCollection"]
Collection --> Renderer["StrokeCGView"]
Tool["RingControl"] -->|display option| Renderer
Renderer --> Segments["Stroke Sequence of StrokeSegment"]
Segments --> CoreGraphics["Core Graphics + dirty rects"]
Reference code
SpeedSketch/StrokeGestureRecognizer.swift:70 — the recognizer expands one event into coalesced real samples followed by disposable predictions.
let view = ensuredReferenceView
if collectsCoalescedTouches {
if let event = event {
let coalescedTouches = event.coalescedTouches(for: touchToAppend)!
let lastIndex = coalescedTouches.count - 1
for index in 0..<lastIndex {
saveStrokeSample(stroke: stroke, touch: coalescedTouches[index],
view: view, coalesced: true, predicted: false)
}
saveStrokeSample(stroke: stroke, touch: coalescedTouches[lastIndex],
view: view, coalesced: false, predicted: false)
}
} else {
saveStrokeSample(stroke: stroke, touch: touchToAppend,
view: view, coalesced: false, predicted: false)
}
if usesPredictedSamples && stroke.state == .active {
if let predictedTouches = event?.predictedTouches(for: touchToAppend) {
for touch in predictedTouches {
saveStrokeSample(stroke: stroke, touch: touch, view: view,
coalesced: false, predicted: true)
}
}
}
Interpretation
The recognizer is an input adapter, not a renderer. It normalizes UIKit events into stable model values; the controller only moves completed/active strokes into the collection and tells the view which model to display.
Ownership and state
classDiagram
CanvasMainViewController *-- StrokeGestureRecognizer : finger and pencil
CanvasMainViewController *-- StrokeCollection : source model
CanvasMainViewController *-- StrokeCGView : renderer
CanvasMainViewController *-- CanvasContainerView : scroll content
CanvasMainViewController o-- RingControl : storyboard tool
StrokeGestureRecognizer *-- Stroke : current aggregate
StrokeGestureRecognizer o-- UITouch : tracked touch
StrokeCollection *-- Stroke : active and completed
Stroke *-- StrokeSample : real and predicted arrays
Stroke ..|> Sequence
StrokeSegmentIterator ..|> IteratorProtocol
StrokeCGView o-- StrokeCollection : displayed reference
Ownership evidence
SpeedSketch/CanvasMainViewController.swift:19 — the screen controller is the composition root and retains input, model, renderer, and canvas components.
var fingerStrokeRecognizer: StrokeGestureRecognizer!
var pencilStrokeRecognizer: StrokeGestureRecognizer!
@IBOutlet var pencilButton: UIButton!
@IBOutlet var scrollView: UIScrollView!
@IBOutlet var separatorView: UIView!
var strokeCollection = StrokeCollection()
var canvasContainerView: CanvasContainerView!
| Owner |
State |
Relationship |
Mutation authority |
CanvasMainViewController |
Current collection, recognizers, renderer, mode/timing |
Creates/retains composition |
Gesture actions and Pencil lifecycle |
Each StrokeGestureRecognizer |
Current stroke, tracked touch, prediction/update lookup, timer |
Owns one input stream |
Touch overrides/reset |
StrokeCollection |
Active and completed strokes |
Owns drawing model |
takeActiveStroke() and controller reset |
Stroke |
Actual/predicted samples and update ranges |
Owns aggregate |
Recognizer add/update methods |
StrokeCGView |
Display option and dirty-rect rendering state |
Observes collection by reference |
Model property observers/draw callbacks |
Class and protocol design
| Type |
Responsibility |
Design note |
StrokeGestureRecognizer |
Convert UIKit touch lifecycle into stroke samples |
Custom framework adapter with separate finger/Pencil instances |
StrokeCollection / Stroke / StrokeSample |
Represent drawing, one stroke, and one captured value |
Clear aggregate hierarchy |
StrokeSegment / iterator |
Present neighboring samples needed by renderer |
Sequence hides traversal/lookahead mechanics |
StrokeCGView |
Render calligraphy, ink, or debug modes and invalidate dirty regions |
Strategy selected by enum state |
RingControl / RingView |
Select and animate display tools |
Custom hit-testing beyond control bounds |
There is no app-defined protocol. Protocol-oriented behavior uses Swift standard contracts (Sequence, IteratorProtocol, CustomStringConvertible) plus UIKit delegates; the segment iterator is the most architectural conformance because it decouples model storage from rendering traversal.
Access control
| Symbol |
Access |
Verified effect |
Rationale |
| Renderer’s held sample/azimuth-lock state |
private |
Only one draw pass can maintain interpolation and calligraphy invariants. |
Transient algorithm state must not be externally mutated. |
| Iterator’s stroke/index/count/segment |
private |
Consumers can only call next(). |
Encapsulates actual/predicted array traversal. |
| Gesture cancellation interval |
private let |
Input cancellation policy stays inside recognizer. |
Configuration is not exposed as feature state. |
setupPencilUI() |
private |
Only the controller initializes its notification/mode wiring. |
Lifecycle setup detail. |
| Most app/model properties |
implicit internal |
Files collaborate freely inside the app target. |
Sample is not a public drawing framework. |
Reference code
SpeedSketch/StrokeCollection.swift:257 — iterator mechanics are private even though the iterator itself satisfies a target-visible standard protocol.
class StrokeSegmentIterator: IteratorProtocol {
// ...
}
Logic ownership and placement
| Logic |
Owner |
Why it belongs there |
| Touch filtering/expansion/estimated updates |
StrokeGestureRecognizer |
UIKit delivers the raw values there. |
| Sample/update-range bookkeeping |
Stroke |
Ranges are properties of model mutations. |
| Neighbor-segment interpolation |
StrokeSegment / iterator |
Renderer consumes a stable traversal API. |
| Dirty rectangles and Core Graphics styles |
StrokeCGView |
Depends on draw lifecycle and display option. |
| Pencil-versus-scroll mode and tool routing |
CanvasMainViewController |
Coordinates recognizers, scroll view, button, and renderer. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Custom gesture adapter |
SpeedSketch/StrokeGestureRecognizer.swift:12 |
Turns complex UIKit events into a reusable stroke action. |
| Stroke model pipeline |
SpeedSketch/StrokeCollection.swift:11 |
Separates captured data from view/controller state. |
| Sequence iterator |
SpeedSketch/StrokeCollection.swift:176 |
Supplies neighbor-aware segments without exposing traversal to renderer. |
| Rendering strategy enum |
SpeedSketch/CGDrawingEngine.swift:10 |
Switches calligraphy, ink, and debug rendering in one engine. |
| Incremental renderer |
SpeedSketch/CGDrawingEngine.swift:73 |
Invalidates only regions touched by new, predicted, or corrected samples. |
Naming conventions
- The data hierarchy is singular/plural:
StrokeCollection, Stroke, StrokeSample, and StrokeSegment.
- Input roles are explicit in instance names:
fingerStrokeRecognizer and pencilStrokeRecognizer.
activeStroke, predictedSamples, and sampleIndicesExpectingUpdates name lifecycle state precisely.
- Renderer commands use
draw, setNeedsDisplay, dirtyRects, and prepareToDraw consistently.
Architecture takeaways
- Normalize raw touch streams into model values before rendering.
- Give actual, predicted, and pending-estimate samples explicit representation and update paths.
- Use
Sequence to hide renderer-specific lookahead traversal behind the model.
- Keep algorithmic draw state private and let a screen-level composition root coordinate modes and UIKit objects.
Source map
| Source file |
Relevant symbols |
SpeedSketch/CanvasMainViewController.swift |
Composition, gesture routing, Pencil mode |
SpeedSketch/StrokeGestureRecognizer.swift |
Touch-to-sample adapter |
SpeedSketch/StrokeCollection.swift |
Drawing model and segment iterator |
SpeedSketch/CGDrawingEngine.swift |
Dirty-rect renderer and style strategies |
SpeedSketch/CanvasContainerView.swift |
Scrollable canvas composition |
SpeedSketch/RingControl.swift |
Tool-selection control |
SpeedSketch/CGMathExtensions.swift |
Geometry operations |