At a glance
| Item |
Summary |
| Purpose |
Visualizes Pencil/finger force, location, altitude, azimuth, coalesced samples, predictions, and estimated-property updates. |
| App architecture |
The controller receives UIKit touch callbacks; CanvasView converts each touch stream into Line models and renders them, while ReticleView presents current diagnostics. |
| Main patterns |
Input pipeline, identity map, transient-versus-frozen rendering, option-set sample tags, and protocol-based formatting. |
| Project style |
Storyboard host with custom view/model files; no separate service or persistence layer. |
Project structure
TouchCanvas/
├── ViewController.swift
├── CanvasView.swift
├── Line.swift
├── ReticleView.swift
├── Formatters.swift
├── SeparatorView.swift
├── AppDelegate.swift
└── Base.lproj/Main.storyboard
Structure observations
- Raw touch routing stays in the controller, but stroke lifecycle and rendering state stay in
CanvasView.
Line.swift groups the aggregate, point value, OptionSet tags, update matching, and drawing behavior.
- Reticle geometry and high-frequency label formatting are isolated from the canvas pipeline.
Overall architecture
flowchart LR
UIKit["UIKit touch callbacks"] --> Controller["ViewController"]
Controller --> Canvas["CanvasView"]
Controller --> Reticle["ReticleView and labels"]
Event["UIEvent"] --> Coalesced["coalesced touches"]
Event --> Predicted["predicted touches"]
Coalesced --> Canvas
Predicted --> Canvas
Canvas --> Active["activeLines touch map"]
Canvas --> Pending["pendingLines update map"]
Active --> Line["Line / LinePoint"]
Pending --> Line
Line --> Live["live line rendering"]
Line --> Frozen["frozen CGContext image"]
UIKit -->|estimated updates| Pending
Reference code
TouchCanvas/CanvasView.swift:125 — each event replaces stale predictions, then appends coalesced and new predicted samples to the line for that touch identity.
func drawTouches(_ touches: Set<UITouch>, withEvent event: UIEvent?) {
var updateRect = CGRect.null
for touch in touches {
let line: Line = activeLines.object(forKey: touch) ?? addActiveLineForTouch(touch)
updateRect = updateRect.union(line.removePointsWithType(.predicted))
let coalescedTouches = event?.coalescedTouches(for: touch) ?? []
let coalescedRect = addPointsOfType(.coalesced, for: coalescedTouches,
to: line, in: updateRect)
updateRect = updateRect.union(coalescedRect)
let predictedTouches = event?.predictedTouches(for: touch) ?? []
let predictedRect = addPointsOfType(.predicted, for: predictedTouches,
to: line, in: updateRect)
updateRect = updateRect.union(predictedRect)
}
setNeedsDisplay(updateRect)
}
Interpretation
Predictions are disposable latency hints, while coalesced/standard points become durable line data. Estimated properties create a third phase: an ended touch may remain in pendingLines until UIKit supplies final values.
Ownership and state
classDiagram
ViewController o-- CanvasView : storyboard outlet
ViewController *-- ReticleView : creates
CanvasView *-- Line : active and finished arrays
CanvasView *-- CGContext : frozen backing store
CanvasView o-- UITouch : strong map keys
Line *-- LinePoint : owns samples
Line *-- NSNumber : estimation index map
LinePoint *-- PointType : option flags
ReticleView *-- CALayer : indicator layers
CGVector ..|> DisplayFormat
CGPoint ..|> DisplayFormat
CGFloat ..|> DisplayFormat
Ownership evidence
TouchCanvas/CanvasView.swift:25 — the canvas privately owns the complete line lifecycle and its frozen rendering cache.
private var needsFullRedraw = true
/// Array containing all line objects that need to be drawn in `drawRect(_:)`.
private var lines = [Line]()
/// Array containing all line objects that have been completely drawn into the frozenContext.
private var finishedLines = [Line]()
private let activeLines: NSMapTable<UITouch, Line> = NSMapTable.strongToStrongObjects()
private let pendingLines: NSMapTable<UITouch, Line> = NSMapTable.strongToStrongObjects()
| Owner |
State |
Relationship |
Mutation authority |
CanvasView |
Active, pending, live, and finished lines; frozen context/image |
Creates and retains |
Touch pipeline and draw callbacks |
Line |
Live/committed points and estimation-index lookup |
Owns aggregate state |
Canvas calls semantic add/update/commit methods |
LinePoint |
Captured touch properties and point tags |
Owned by a line |
Initializer and estimated-update method |
ReticleView |
Actual/predicted angles, vectors, and layers |
Controller supplies current values |
Property observers/layout |
Class and protocol design
| Type |
Responsibility |
Design note |
CanvasView |
Correlate touch identities, manage line phases, and render efficiently |
Feature state owner below controller |
Line |
Aggregate points, apply estimated updates, draw/commit segments |
Model with rendering behavior |
LinePoint / PointType |
Store one sample and orthogonal classification flags |
OptionSet avoids an exploding state enum |
ReticleView |
Render actual and predicted orientation diagnostics |
Presentation-only custom view |
DisplayFormat |
Give numeric geometry values stable-width text |
Small app-defined capability protocol |
DisplayFormat is genuine protocol-oriented design: unrelated Foundation/Core Graphics scalar types gain the same read-only presentation capability through extensions, with no wrapper hierarchy.
Access control
| Symbol |
Access |
Verified effect |
Rationale |
| Canvas line maps/cache/helpers |
private |
Only CanvasView can move a line through active, pending, committed, and finished phases. |
Protects lifecycle and redraw invariants. |
Line point arrays/update map |
private |
Callers use semantic methods instead of mutating point storage. |
Preserves estimation-index consistency. |
| Reticle layers/math helpers |
mostly private |
Controller sets only diagnostic inputs and two predicted layers it must hide/show. |
Hides rendering implementation while exposing required collaboration. |
| Formatters |
file-level private |
Protocol conformances in the same file reuse them; other files cannot. |
High-frequency formatting policy is file-local. |
| Types/protocol |
implicit internal |
Shared only inside the sample target. |
No public framework surface. |
Reference code
TouchCanvas/Formatters.swift:10 — the protocol is target-visible, but the formatter object supporting each conformance is file-private.
protocol DisplayFormat {
var valueFormattedForDisplay: String? { get }
}
private let vectorFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.allowsFloats = true
formatter.minimumFractionDigits = 3
formatter.maximumFractionDigits = 3
formatter.positivePrefix = "+"
return formatter
}()
Logic ownership and placement
| Logic |
Owner |
Why it belongs there |
| Raw responder callbacks and diagnostic routing |
ViewController |
UIKit delivers touches to the controller and it owns labels/reticle. |
| Touch-to-line correlation and phase changes |
CanvasView |
Requires active/pending maps and redraw decisions. |
| Estimated-property matching |
Line and LinePoint |
Estimation indexes identify model samples, not UI elements. |
| Frozen/live drawing |
CanvasView plus Line |
Canvas owns contexts; line knows segment appearance. |
| Numeric label formatting |
Formatters.swift |
Reusable across vector, point, and scalar diagnostics. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Input pipeline |
TouchCanvas/CanvasView.swift:125 |
Separates event expansion from sample modeling and drawing. |
| Identity map |
TouchCanvas/CanvasView.swift:40 |
Correlates non-NSCopying UITouch objects to line aggregates. |
| Two-tier rendering |
TouchCanvas/CanvasView.swift:81 |
Draws stable work from a frozen bitmap and only live lines each frame. |
| Option-set tagging |
TouchCanvas/Line.swift:285 |
A point can simultaneously be predicted, pending, finger input, or cancelled. |
| Protocol extension formatting |
TouchCanvas/Formatters.swift:26 |
Shares display semantics without a model superclass. |
Naming conventions
activeLines, pendingLines, and finishedLines name lifecycle phases directly.
Line, LinePoint, and nested PointType express aggregate-to-value containment.
- Update methods say what changed:
updateEstimatedPropertiesForTouches and updateWithTouch.
- The source uses
gague... for gauge outlets/helpers; architecture notes retain the source spelling only when naming symbols.
Architecture takeaways
- Treat predicted input as replaceable state and final samples as durable model state.
- Keep an explicit pending phase when framework values may arrive after touch end.
- Correlate UIKit identity objects at the custom-view boundary, then pass stable model values deeper.
- Protect rendering caches and sample indexes with private ownership and semantic mutation methods.
Source map
| Source file |
Relevant symbols |
TouchCanvas/ViewController.swift |
Touch routing, labels, and reticle |
TouchCanvas/CanvasView.swift |
Line lifecycle and frozen/live renderer |
TouchCanvas/Line.swift |
Line aggregate, samples, estimation updates |
TouchCanvas/ReticleView.swift |
Orientation visualization |
TouchCanvas/Formatters.swift |
DisplayFormat protocol and conformances |