Skip to content

Authoring Lessons

Daniel Budd edited this page Apr 11, 2026 · 1 revision

Authoring Lessons

How to build your own geometry lessons on top of the Swift Playgrounds book. This page is a teacher-facing summary of the authoring API exposed by the Geometry Playground runtime, distilled from the full PlaygroundBookv12 README.

If you want the raw API reference with every function signature, read the README directly. This page is shorter and organised around what you might want to teach.

Who this page is for

You've read the rest of this wiki. You've probably taught at least one chapter of Geometry Playground to real students. You now want to build something of your own: a bespoke exercise for your class, a revision activity, a visualisation of a specific theorem, or a whole chapter of your own.

You do not need to be an iOS developer. You need:

  • A Mac with Xcode installed (for the full authoring workflow) OR an iPad with Swift Playgrounds (for quick authoring directly on the device).
  • Basic Swift comfort equivalent to Chapters II, III, and V of Geometry Playground.
  • A clear idea of the geometric idea you want to demonstrate.

If you only want to write quick exercises for students to copy, you don't need Xcode at all. Swift Playgrounds on an iPad lets students (and teachers) add extra pages to any book, including Geometry Playground, and write code there. See Classroom Management for that workflow.

Two authoring paths

Path 1: In-place (on iPad)

Add a page inside Geometry Playground itself and use the library's built-in API. This is the path for teachers who want to write a one-off exercise for a class without setting up any development environment.

Requirements: just Swift Playgrounds on an iPad with Geometry Playground subscribed.

What you can do: everything on this page. The runtime is already there, so Pen, Input, Scene, addCircle, and every other API described below works immediately.

What you can't do: modify the book's navigation or cover, add new chapters, or ship your work to other teachers as a standalone book.

Path 2: Fork the authoring repo (on Mac)

Clone the Geometry-Swift-PlaygroundBook repository, open it in Xcode, and build your own .playgroundbook. This is the path for teachers who want to write a whole custom chapter or ship their own version of the book.

Requirements: a Mac, Xcode, and enough Swift comfort to run an Xcode project.

What you can do: everything. New chapters, custom covers, modified navigation, shipping a new .playgroundbook to your own students, cross-platform debugging via the LiveViewTestApp.

See the "Authoring notes for forks" section at the end of the PlaygroundBookv12 README for repo structure and configuration.

The core API at a glance

The Geometry Playground runtime exposes three main concepts:

  1. Pen: a command recorder for drawing. You set properties (colour, width) and call methods (addLine, turn, drawCircle).
  2. Input(...): interactive controls shown in the live view panel. Students drag or type to change a value and the drawing updates.
  3. Scene { ... }: a reactive block that reruns whenever any Input changes. This is how you get live-updating lessons.

The simplest possible lesson:

Scene {
    let side = Input(decimal: 120, label: "Side Length")

    var square = Pen()
    square.penColor = .systemRed

    for _ in 0..<4 {
        square.addLine(distance: side)
        square.turn(degrees: 90)
    }

    addShape(pen: square)
}

A square. The student drags a slider labelled "Side Length" and the square resizes in real time. That's the entire pattern: Scene, Input, Pen, addShape.

Geometric primitives

The API goes far beyond the Pen that students use in Chapters I through VI. Once you're authoring, you get access to a full set of geometric primitives:

Basic objects

  • Point(x:y:): a point in the Cartesian plane.
  • Line(start:end:): a line segment.
  • Circle(center:radius:): a circle.
  • Triangle(a:b:c:): a triangle defined by three points.
  • Polygon(vertices:[...]): a polygon defined by an ordered list of vertices.

Each of these has a corresponding addPoint, addLine, addCircle, addTriangle, addPolygon helper with colour, line width, and z-order parameters.

Example: draw a triangle with labelled vertices

Scene {
    let a = Point(x: -100, y: -60)
    let b = Point(x: 100, y: -60)
    let c = Point(x: 0, y: 80)

    addTriangle(Triangle(a: a, b: b, c: c), color: .systemRed, lineWidth: 2)
    addPoint(a, color: .systemGreen, radius: 5)
    addPoint(b, color: .systemGreen, radius: 5)
    addPoint(c, color: .systemGreen, radius: 5)
}

Three lines for three vertices, three lines for the labelled points. Simpler than the equivalent Pen code, and more natural for lessons about specific triangles.

Transformations

Real transformation lessons (rotate this figure by 30 degrees, reflect it across this line) are trivial with the transform helpers.

Direct helpers (one shape, one transform):

  • translate(..., dx:, dy:)
  • rotate(..., around:, degrees:)
  • scale(..., around:, factor:)

Composable helpers via Transform2D:

  • .translation(dx:, dy:)
  • .rotation(degrees:, around:)
  • .scaling(factor:, around:)
  • .reflectionAcrossX()
  • .reflectionAcrossY()
  • .reflectionAcross(line:)
  • .concatenating(...) to combine multiple transforms

Example: rotate a triangle around its centroid

Scene {
    let angle = Input(decimal: 30, label: "Rotate Degrees")

    let base = Triangle(
        a: Point(x: -80, y: -40),
        b: Point(x: 80, y: -40),
        c: Point(x: 0, y: 80)
    )

    addTriangle(base, color: .systemGray3, lineWidth: 1)

    let pivot = centroid(base)
    let rotated = base.transformed(by: .rotation(degrees: angle, around: pivot))

    addTriangle(rotated, color: .systemBlue, lineWidth: 2)
    addPoint(pivot, color: .systemOrange, radius: 4)
}

The student drags a slider labelled "Rotate Degrees". They see the blue rotated copy sweep around the grey original, pivoting on the centroid. This is a live demonstration of a rotation, and it took 15 lines of code.

Triangle centres and constructions

For lessons on triangle centres and classical constructions, the API exposes:

  • centroid(triangle)
  • incenter(triangle)
  • circumcenter(triangle)
  • orthocenter(triangle)
  • intersection(_:_:) for two lines
  • perpendicularBisector(of:)
  • perpendicularLine(through:to:)
  • angleBisector(at:through:and:)

Plus higher-level construction macros that return a full "construction bundle" with all the guide points, lines, and circles a classical compass-and-straightedge construction would produce:

  • constructPerpendicularBisector(of:length:)
  • constructAngleBisector(vertex:firstPoint:secondPoint:length:)
  • constructPerpendicularThroughPoint(point:to:length:)
  • constructParallelThroughPoint(point:to:length:)

These are perfect for IM1 and IM2 lessons on formal geometric construction. You can show the student the construction's guide circles and lines in one colour and the resulting bisector in another, like a textbook diagram but live and interactive.

Plotting curves and functions

For lessons that touch on coordinate geometry, calculus previews, or parametric shapes, the API includes a full set of plotting helpers:

  • plotFunction(xMin:, xMax:, samples:, color:, lineWidth:, function:): plot y = f(x).
  • plotParametric(tMin:, tMax:, samples:, color:, lineWidth:, point:): plot a parametric curve.
  • addSpiral(...), addCycloid(...), addLissajous(...), addRose(...), addEpicycloid(...), addHypocycloid(...): named classical curves.
  • addEllipse(...), addParabola(...), addHyperbola(...): the conic sections.

Plus teaching-oriented helpers:

  • sampleFunction(...) and addSamplePoints(...): plot a curve and show a scatter of sample points on it (great for introducing the concept of sampling).
  • tangentLineToFunction(...) and addTangentToFunction(...): draw the tangent line at a given x-value (great for calculus previews).

Example: plot a sine curve with its tangent at a chosen point

Scene {
    let x0 = Input(decimal: 30, label: "Tangent X")

    plotFunction(
        xMin: -180,
        xMax: 180,
        samples: 400,
        color: .systemBlue
    ) { x in
        60 * sin(x / 40)
    }

    addTangentToFunction(atX: x0, length: 180, color: .systemOrange) { x in
        60 * sin(x / 40)
    }
}

A live tangent line on a sine curve. Drag the slider, the tangent slides along. Useful for introducing derivatives informally without any calculus notation.

Animation

Lessons that need movement use the animation API instead of Scene:

  • animate(duration:, fps:, repeats:) { t in ... }: run an animation loop with normalized time t from 0 to 1.
  • stopAnimation(): stop any running animation.

Within the animate block, you draw as usual. The block runs repeatedly at the given frame rate, so you can tie positions to t and produce motion.

Example: a point orbiting a centre

animate(duration: 6.0, fps: 30, repeats: true) { t in
    let radius = Input(decimal: 120, label: "Orbit Radius")

    let angle = 360.0 * t
    let orbiting = rotate(Point(x: radius, y: 0), degrees: angle)

    addCircle(Circle(center: .zero, radius: radius), color: .systemGray3, lineWidth: 1)
    addPoint(orbiting, color: .systemOrange, radius: 6)
}

Six seconds per orbit, looping forever. The student can still drag the "Orbit Radius" input mid-animation.

For more sophisticated animation timing, the API includes easing curves (.linear, .easeIn, .easeOut, .easeInOut, .smoothStep), ping-pong time, and keyframe interpolation (keyframedValue(at:keyframes:), keyframedPoint(at:keyframes:)).

Draggable points and interactive geometry

Some lessons are better as direct manipulation than as slider-driven. For these, use DraggablePoint:

Scene {
    let a = DraggablePoint(id: "a", initial: Point(x: -80, y: -40))
    let b = DraggablePoint(id: "b", initial: Point(x: 80, y: -40))
    let c = DraggablePoint(id: "c", initial: Point(x: 0, y: 80))

    addTriangle(Triangle(a: a, b: b, c: c), color: .systemBlue, lineWidth: 2)

    if let o = circumcenter(Triangle(a: a, b: b, c: c)) {
        addPoint(o, color: .systemOrange, radius: 5)
        addCircle(Circle(center: o, radius: distance(o, a)), color: .systemOrange, lineWidth: 1)
    }
}

Three draggable vertices. Drag any vertex and the circumcircle updates in real time. This is the equivalent of a GeoGebra investigation, embedded in a Swift Playgrounds page.

Supported snap modes: .none, .grid, .xAxis, .yAxis, .axes. Handles can be hidden, locked, or restyled globally.

For moving a whole group of objects together, use DraggableGroup (Phase 9B of the API).

Labels and measurements

For classroom-quality diagrams, add labels:

  • addCoordinateLabel(point, decimals:, color:): show a point's coordinates next to it.
  • addLengthLabel(line, decimals:, color:): show a segment's length next to it.
  • addAngleMarker(vertex:, firstPoint:, secondPoint:, radius:, color:, showLabel:): draw an arc at an angle's vertex with optional degree label.

These overlay stays synchronised with animated geometry, so you can label a moving triangle's sides and angles and the labels follow the motion.

Regions and shading

For area-focused lessons and composite-figure work:

  • addFilledPolygon(polygon, fillColor:, borderColor:, lineWidth:)
  • addFilledTriangle(triangle, fillColor:, borderColor:, lineWidth:)
  • addFilledCircle(circle, fillColor:, borderColor:, lineWidth:)
  • addHatchedPolygon(polygon, spacing:, angleDegrees:, color:, lineWidth:, crossHatch:)

Good for Chapter VI style work, area-of-composite lessons, and the IM2 outcome on area scale factor.

Groups and composition

For complex scenes with multiple related objects, the grouping API (Phase 9A) lets you bundle geometry and transform the whole bundle at once:

var group = makeGroup { g in
    g.addLine(Line(start: Point(x: -40, y: 0), end: Point(x: 40, y: 0)), color: .systemBlue)
    g.addLine(Line(start: Point(x: 0, y: -40), end: Point(x: 0, y: 40)), color: .systemBlue)
    g.addCircle(Circle(center: .zero, radius: 22), color: .systemMint)
}

group = rotate(group: group, around: .zero, degrees: 30)
group = translate(group: group, dx: 100, dy: 40)
renderGroup(group)

Groups are the right abstraction when you want to teach "here is a compound figure, and now we rotate/translate/scale the whole thing as one".

Teacher view controls

Some lessons want a clean visual without the grid, axes, or labels. Use the preference helpers:

  • setGridVisible(true/false)
  • setAxesVisible(true/false)
  • setLabelsVisible(true/false)
  • setControlsVisible(true/false)

These can be called inside a Scene block to hide the grid for a specific activity, or toggled live by the student via the Preferences panel in the LiveView.

Style presets

For visual consistency across a unit or a whole custom chapter, use named style presets:

  • .classic: black lines on white.
  • .blueprint: technical drawing aesthetic.
  • .chalkboard: white on black, for classroom projection.
  • .sunset: warm palette.
let style = geometryStyle(.blueprint)
addStyledLine(Line(start: Point(x: -120, y: 0), end: Point(x: 120, y: 0)), style: style, role: .guide)
addStyledPoint(.zero, style: style, role: .accent)

This is how you keep your custom chapter looking like a coherent unit instead of a patchwork of random colours.

Trace and playback

For lessons on constructions that take multiple steps, the trace API (Phase 7C) captures frame-by-frame snapshots and lets students step through them like slides:

  • setTraceCaptureEnabled(true) to start capturing.
  • setTraceControlsVisible(true) to show the playback panel.
  • setTraceCaptureMode(.allFrames / .changedOnly) for capture strategy.

Useful for step-by-step compass-and-straightedge constructions where you want the student to go back and forth.

Safety and guardrails

Plotting-heavy lessons can accidentally request millions of samples and grind the iPad to a halt. The runtime has built-in safety:

  • maxSamplesPerCurve = 4000 (default)
  • maxTotalSamplesPerScene = 30000 (default)
  • Non-finite input values (NaN, infinity) are silently skipped rather than crashing.

Override with setPlotSafetyConfiguration(...) if you know what you're doing.

Localisation

Every Input(..., label:) label is resolved through NSLocalizedString. If you fork the book and want to ship a French or Japanese version, you can localise control labels via standard .strings files without changing any of the Swift API.

For explicit localised strings in learner code, use Localized("key").

Picking the right tool for a lesson

A rough guide:

Lesson type Best API
"Draw a polygon by walking the pen" Pen (as used in Chapters I through VI)
"Show a specific triangle and its properties" Triangle + addTriangle + centre helpers
"Demonstrate a transformation" Transform2D with Scene or animate
"Plot a function or curve" plotFunction / plotParametric
"Investigate a theorem by dragging points" DraggablePoint + Scene
"Walk through a compass-and-straightedge construction" constructXxx macros + trace capture
"Animate a geometric idea" animate { t in ... }
"Composite figure with area shading" addFilledPolygon + groups

Worked examples to adapt

The PlaygroundBookv12 README includes worked examples for:

  • Phase 1 basic shapes and points.
  • Phase 1C transformation composition.
  • Phase 3 triangle centres with circumcircle.
  • Phase 4 function and parametric plotting.
  • Phase 4D function sampling and tangent lines.
  • Phase 6A orbital animation.
  • Phase 6D filled and hatched regions.
  • Phase 7B draggable investigations.
  • Phase 8A loci helpers.
  • Phase 8B classical construction macros.
  • Phase 9A grouped transform.
  • Phase 9E camera framing.

The fastest way to author a new lesson is to find the example closest to what you want, copy it, and modify.

Debugging your lesson

Two options:

On the iPad. Open the page, edit the code, tap Run. Errors show inline. Print statements show in the console panel. Live output in the Cartesian plane view.

On the Mac via Xcode. Clone the authoring repo, open it in Xcode, and use the LiveViewTestApp target. This runs the same rendering pipeline as the Playground book but as a standalone debug app, with full Xcode breakpoints and console access. Much faster iteration for complex lessons. Configure your scene in LiveViewTestApp/DebugLiveViewContent.swift.

Keeping your lessons reproducible

Three tips from hard experience:

  1. Use stable input labels. Inputs are keyed by label. If you rename a label mid-development, you'll lose state. Pick your labels and leave them.
  2. Version your custom chapters. If you fork the repo, commit early and often. Swift Playgrounds books accumulate state in PrivateResources that's easy to lose.
  3. Test on the oldest iPad your classroom uses. Plotting-heavy animated lessons run differently on a 2018 iPad than on a 2024 iPad Pro. The safety limits help, but test it.

Limits and known gaps

The authoring API is opinionated. Some things it does not currently do:

  • 3D geometry. Everything is 2D.
  • Text rendering beyond labels. No rich text, no math notation (no LaTeX).
  • Sound or haptics.
  • File I/O or networking.
  • Custom input widgets. The built-in controls (number, decimal, text) are the only options.
  • Exporting finished drawings to image files. Students can screenshot.

If your lesson needs any of these, you'll need to extend the runtime itself, which is outside the scope of this page. See the repository issues to discuss or propose a feature.

Contributing back

If you build a lesson that works well in your classroom and you'd like to share it, the project welcomes contributions. Open a pull request on the authoring repo with your new page, or on the main site repo if you want it mirrored on the companion website.

Links

Clone this wiki locally