Build a Duolingo-Style Learning Tree UI in SwiftUI
How to build a Duolingo-style learning tree in SwiftUI, from the winding node path and lesson states to a data model and the animation that drives motivation.
TL;DR
A Duolingo-style learning tree is a vertical scrolling path of lesson nodes, each in a state like locked, available, current, or completed, that winds left and right with a drawn connecting line. In SwiftUI you build it from a data model of units and lessons, lay the nodes out with an alternating horizontal offset, draw the connector with Path or Canvas, and animate the current node so it invites a tap. The whole screen is fastest to start from a free VP0 design and build with Claude Code or Cursor.
A Duolingo-style learning tree is a vertical scrolling path of lesson nodes, each in a state like locked, available, current, or completed, that winds left and right with a drawn line connecting them. In SwiftUI you build it from a data model of units and lessons, lay the nodes out in a scroll view with an alternating horizontal offset, draw the connector with Path or Canvas, and animate the current node so it invites a tap. The whole screen, the path, the node styles, the section headers, and the header bar, is fastest to start from a free VP0 design and build with Claude Code or Cursor, leaving you to wire your lesson data and progress logic.
What makes up a Duolingo-style learning tree?
It is a guided path, not a list, and three things make it read that way. The nodes are large tappable circles, usually with an icon, that represent a lesson or skill; the path winds side to side instead of running straight down, which makes progress feel like a journey; and the nodes carry clear states so a learner always knows what is done, what is next, and what is still locked. Section headers break the path into units.
The point of the pattern is motivation through clarity. A learner should see one obvious next step, the current node, with everything before it completed and everything after it locked, so there is never a question of what to do next. That single-next-step focus is what separates a learning tree from a plain menu of lessons, and it is the part worth getting right before any animation.
The node states and how they behave
Every node is in one of a few states, and each state needs a distinct look and interaction. Getting these consistent is most of what makes the tree feel finished.
| State | Look | Interaction |
|---|---|---|
| Locked | Muted, often a lock icon | Not tappable, shows a hint on tap |
| Available | Full color, no progress | Tappable, starts the lesson |
| Current | Highlighted, a START bubble | The primary call to action |
| Completed | Filled, often a progress ring | Replayable for practice |
The current node carries the most weight, since it is the one the design wants the user to tap, so it gets the strongest treatment: a color pop, a small bounce, and often a floating START label. Locked nodes should still respond to a tap with a short explanation rather than feeling dead, and completed nodes stay useful for review. Encoding state with shape and icon, not color alone, also keeps the tree readable for users who cannot rely on color.
Laying out the winding path in SwiftUI
The layout is a scroll view of nodes with an alternating horizontal offset, plus a line drawn behind them. A LazyVStack keeps a long tree performant by only building visible nodes, and you shift each node left or right with a repeating pattern so the path swings rather than runs straight. A horizontal swing of roughly 20% of the screen width reads as a path without pushing nodes off the edge.
ScrollView {
LazyVStack(spacing: 28) {
ForEach(Array(lessons.enumerated()), id: \.element.id) { index, lesson in
LessonNode(lesson: lesson)
.offset(x: windingOffset(for: index))
}
}
}
func windingOffset(for index: Int) -> CGFloat {
let pattern: [CGFloat] = [0, 44, 64, 44, 0, -44, -64, -44]
return pattern[index % pattern.count]
}
The connecting line is the finishing touch, and Path or Canvas is the right tool, drawing a curve between consecutive node centers behind the nodes. Apple’s Path documentation covers the curve commands, and a ScrollView with ScrollViewReader lets you scroll the user to their current node on open so they land exactly where they left off.
Drawing the connector line cleanly
The connecting line is the part that trips people up, because it has to follow nodes whose positions depend on the same winding offset. The reliable approach is to compute each node’s center from the row height and its windingOffset, then draw the whole path in a single Canvas layer placed behind the nodes, rather than drawing a separate segment per row. Drawing it once keeps the curve continuous instead of kinked at every node.
Use a smooth curve, not straight segments, so the path reads as a journey. Quadratic or cubic Bezier curves between consecutive centers give the gentle S-shape the pattern is known for, and a control point nudged toward the midpoint keeps the bend natural. Stroke the path with a rounded line cap and a width that sits comfortably under the node circles, and draw it before the nodes so the nodes sit on top. If your nodes animate their offset, drive the path from the same source values so the line and the nodes never drift apart.
Driving the tree from a data model
The tree should be generated from data, never hand-placed, so it can grow and reflect real progress. Model the content as units that contain lessons, each lesson carrying its own state, and render the path by iterating that data. When a lesson is completed, you update its state and unlock the next one, and the view recomputes, which is far more maintainable than positioning nodes by hand.
That data-driven approach is what lets the tree scale to hundreds of lessons and stay correct. Progress, unlock rules, and section boundaries all live in the model, so the view stays a thin reflection of state. For the broader app this screen sits inside, the language learning app like Duolingo build covers the surrounding structure the tree plugs into.
Units, sections, and checkpoints
Real learning trees are broken into units, and the dividers do useful work beyond decoration. A section header between groups of nodes gives the path a sense of chapters, tells the user how content is organized, and creates natural stopping points. Color-coding each unit, with the nodes in it sharing an accent, also helps a long path stay legible as the user scrolls.
Checkpoint or review nodes are worth adding at the end of a unit. A larger, distinct node that gates progress to the next unit gives the path rhythm and a feeling of milestone, and it maps cleanly onto the same state model as just another node type with its own look. Keep these special nodes in the data model like any other lesson so the layout stays fully data-driven, and the tree gains structure without any new layout code.
Animating the current node and progress
A little motion goes a long way, and it should concentrate on the current node. A gentle, repeating bounce on the current node draws the eye to the next step, and an unlock animation when a node becomes available rewards finishing a lesson. Keep the rest of the tree calm so the motion that exists means something; animating every node at once turns guidance into noise.
Two details add polish without much code. A progress ring around in-progress or completed nodes shows how far along a skill is, and the Duolingo progress ring animation covers building exactly that. A streak indicator in the header ties daily use to the path, and the Duolingo streak flame animation shows that piece. Both reinforce momentum, which is the entire point of the pattern.
Designing the tree screen fast
The path, the nodes, the headers, and the top bar are a known layout, so starting from a finished design beats positioning circles by hand. A complete learning-tree screen has the winding path, four node states that read clearly, section dividers, and a header with progress and a streak, all balanced so the path is the focus. Tuning the spacing, the swing, and the node sizing is iterative work a design starting point removes.
The VP0 library fits this because each screen has a hidden source page an AI builder reads from a pasted link:
Build this Duolingo-style learning tree in SwiftUI.
Read the layout and tokens from this VP0 source page: <pasted VP0 link>.
Drive the nodes from a units-and-lessons model with locked, available,
current, and completed states, and draw the connecting path with Canvas.
That gives Claude Code or Cursor the real layout to follow, so it builds the path and node states against a structure instead of guessing. VP0 gives you the screen and the node components; your lesson content, progress rules, and unlock logic are the parts you supply, which is where your app’s actual learning design lives.
Common mistakes building a learning tree
The biggest mistake is hand-placing nodes instead of generating them from data, which makes the tree impossible to extend and easy to break. Drive everything from a units-and-lessons model so adding content is a data change, not a layout change. The second is making locked nodes feel dead; a tap on a locked node should explain why it is locked, not do nothing.
A few more are about feel and performance. Rendering a long tree without LazyVStack makes scrolling stutter once there are many nodes. Animating every node instead of just the current one buries the one cue that matters. And relying on color alone for state leaves the tree unreadable for some users, where a shape or icon per state fixes it. Each is a small change, and together they are the difference between a path that motivates and one that frustrates.
What to choose
For almost every learning tree, build it in SwiftUI from a units-and-lessons data model: a LazyVStack of nodes with an alternating offset, a connector drawn in Canvas, four clear node states, and animation focused on the current node. Add a progress ring and a streak in the header to reinforce momentum, and scroll the user to their current node on open. That combination scales to a long path and keeps the next step obvious.
For the build, start the screen and the node components from a free VP0 design, generate them with Claude Code or Cursor, and spend your effort on the lesson data and the unlock rules that make the path meaningful.
Frequently asked questions
How do I build a Duolingo-style learning tree in SwiftUI?
Generate the path from a units-and-lessons data model, lay the nodes out in a LazyVStack inside a ScrollView with an alternating horizontal offset, and draw the connecting line with Path or Canvas. Give each node a state, locked, available, current, or completed, and animate the current one. Start the screen and node components from a free VP0 design and build them with Claude Code or Cursor, then wire your lessons and progress.
How does the winding path layout work?
Each node is shifted left or right by a repeating pattern as you go down the list, so the path swings instead of running straight. A swing of around 20% of the screen width reads as a journey without pushing nodes off the edge. A curved line drawn in Canvas behind the nodes connects consecutive centers, and a LazyVStack keeps a long path smooth by only rendering the nodes on screen.
How do I handle locked and unlocked lesson states?
Store each lesson’s state in your data model and render the node from it, so the view always reflects real progress. When a lesson is completed, update its state and unlock the next node, and let the view recompute. Make locked nodes show a short hint on tap rather than being inert, and give each state a distinct shape or icon so it does not depend on color alone.
What gamification adds the most motivation to a learning tree?
A single obvious next step matters most, so the current node should be unmistakable. Beyond that, a progress ring on skills and a daily streak in the header are the highest-value additions, since they tie momentum to the path. Keep animation focused on the current node and on unlock moments, because constant motion everywhere dilutes the cues that actually drive the user forward.
Where can I get a free template for a learning tree screen?
VP0 is a free iOS design library where each screen has an AI-readable source page, so you can browse a learning-tree or gamification layout, copy its link, and have Claude Code or Cursor build it in SwiftUI with the winding path, node states, and header. You supply the lesson data and unlock logic; the screen and node components come from the design.
What VP0 builders also ask
How do I build a Duolingo-style learning tree in SwiftUI?
Generate the path from a units-and-lessons data model, lay the nodes out in a LazyVStack inside a ScrollView with an alternating horizontal offset, and draw the connecting line with Path or Canvas. Give each node a state, locked, available, current, or completed, and animate the current one. Start the screen and node components from a free VP0 design and build them with Claude Code or Cursor, then wire your lessons and progress.
How does the winding path layout work?
Each node is shifted left or right by a repeating pattern as you go down the list, so the path swings instead of running straight. A swing of around 20% of the screen width reads as a journey without pushing nodes off the edge. A curved line drawn in Canvas behind the nodes connects consecutive centers, and a LazyVStack keeps a long path smooth by only rendering the nodes on screen.
How do I handle locked and unlocked lesson states?
Store each lesson's state in your data model and render the node from it, so the view always reflects real progress. When a lesson is completed, update its state and unlock the next node, and let the view recompute. Make locked nodes show a short hint on tap rather than being inert, and give each state a distinct shape or icon so it does not depend on color alone.
What gamification adds the most motivation to a learning tree?
A single obvious next step matters most, so the current node should be unmistakable. Beyond that, a progress ring on skills and a daily streak in the header are the highest-value additions, since they tie momentum to the path. Keep animation focused on the current node and on unlock moments, because constant motion everywhere dilutes the cues that actually drive the user forward.
Where can I get a free template for a learning tree screen?
VP0 is a free iOS design library where each screen has an AI-readable source page, so you can browse a learning-tree or gamification layout, copy its link, and have Claude Code or Cursor build it in SwiftUI with the winding path, node states, and header. You supply the lesson data and unlock logic; the screen and node components come from the design.
Part of the Free iOS Templates, UI Kits & Components hub. Browse all VP0 topics →
Keep reading
Animated Splash Screen in React Native With Lottie
Build an animated splash screen in React Native with Lottie. A free template and the right native-splash handoff so there is no white flash on launch.
Chatbot UI in React Native: A Gifted Chat Alternative
Want a Gifted Chat alternative for React Native? Build your own clean chatbot UI from a free template, with full control over bubbles, streaming, and states.
Figma Auto Layout to React Native Flexbox: The Map
Converting Figma Auto Layout to React Native? The two map cleanly once you know the translation. Here is the property-by-property guide, with a free reference.
Grab Super App UI in React Native: Free Source Start
Want Grab super app UI source code in React Native? Generate clean RN code from a free template, the service hub, ride, and food flows, with Claude Code or Cursor.
Jumia Ecommerce UI Kit in React Native, Free
Want a Jumia style ecommerce UI kit in React Native? Clone the storefront pattern from a free template and build clean code with Claude Code or Cursor. The legal way.
RTL Ecommerce Template in React Native, Free
Build a right-to-left (RTL) ecommerce app in React Native from a free template. Get a properly mirrored storefront, product, and checkout with Claude Code or Cursor.