Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Leveraging touch input for drawing apps

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

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

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