SwiftUI for Beginners ยท Lesson 4 of 9
Working With Lists of Data
So far, we've written views individually.
For example:
Text("Apple")
Text("Banana")
Text("Orange")
That works when we have three items.
But imagine an app containing:
10 tasks 50 contacts 200 products 1,000 messages
We obviously don't want to write 1,000 Text views ourselves.
Instead, real applications usually work like this:
DATA โ SwiftUI โ UI
In this lesson, we'll learn how to represent data with Swift models and turn collections of that data into views using:
Array โ Model โ ForEach โ List โ Identifiable
By the end, we'll build a simple Task List:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ My Tasks โ โ โ โ โ Learn SwiftUI โ โ โ โ โ Build a small app โ โ โ โ โ Practice every day โ โ โ โ 1 of 3 completed โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
But the important lesson isn't the task list itself.
It's this:
Your data can determine how many views SwiftUI creates.
1. The Problem With Hardcoded Views
Suppose we want to display three fruits.
We could write:
VStack {
Text("Apple")
Text("Banana")
Text("Orange")
}
And we'd get:
Apple Banana Orange
Nothing is wrong with this code.
But what happens when our data changes?
Suppose tomorrow we have:
Apple Banana Orange Mango Grapes Watermelon Strawberry
Now we'd have to modify the UI manually:
VStack {
Text("Apple")
Text("Banana")
Text("Orange")
Text("Mango")
Text("Grapes")
Text("Watermelon")
Text("Strawberry")
}
That's not how we want to build a real application.
The problem is that our data is mixed directly into our UI structure.
Instead, we'd like to say:
Here is my data. SwiftUI, create one view for each item.
2. Meet Arrays
Swift gives us an Array for storing multiple values.
For example:
let fruits = ["Apple", "Banana", "Orange"]
You can imagine it like this:
fruits โ โโโ "Apple" โโโ "Banana" โโโ "Orange"
Instead of having three separate variables:
let fruit1 = "Apple"
let fruit2 = "Banana"
let fruit3 = "Orange"
we have one collection:
let fruits = ["Apple", "Banana", "Orange"]
We can access individual values using their position:
fruits[0]
fruits[1]
fruits[2]
which gives us:
Apple Banana Orange
Arrays become extremely useful in SwiftUI because many screens display collections of things:
Messages Contacts Tasks Products Photos Notifications Search Results
So now we have the data.
The next question is:
How do we turn every item in that array into a SwiftUI view?
3. Meet ForEach
This is where ForEach comes in.
Let's start with our fruit array:
let fruits = ["Apple", "Banana", "Orange"]
Now:
VStack {
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
}
SwiftUI creates:
Apple Banana Orange
But notice something important.
We only wrote:
Text(fruit)
once.
SwiftUI repeated that view for every item in fruits.
Conceptually:
fruits
โ
โโโ Apple โโโโโโ Text("Apple")
โ
โโโ Banana โโโโโโ Text("Banana")
โ
โโโ Orange โโโโโโ Text("Orange")
That's our new mental model:
Collection of Data
โ
ForEach
โ
One View Per Item
4. Reading ForEach
This syntax may look strange at first:
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
Let's break it down.
fruits
ForEach(fruits, ...)
means:
Go through the values inside
fruits.
fruit
{ fruit in
means:
For the current item, call it
fruit.
So as SwiftUI moves through the array:
First time: fruit = "Apple" Second time: fruit = "Banana" Third time: fruit = "Orange"
Then:
Text(fruit)
creates the view for that particular value.
You can mentally read the entire thing as:
For each fruit in fruits, create a Text showing that fruit.
That's much less mysterious.
5. Why Do We Need id: \.self?
You probably noticed this:
id: \.self
Why does SwiftUI need it?
Because SwiftUI needs a way to identify each item.
Imagine the array changes:
Before Apple Banana Orange
Then:
After Apple Mango Orange
SwiftUI needs to understand:
Apple โ still Apple Banana โ removed Mango โ new Orange โ still Orange
Identity helps SwiftUI understand which piece of data corresponds to which view.
For simple unique strings in a beginner example, we can use:
id: \.self
which roughly means:
Use the value itself as its identity.
So:
"Apple" โ identified by "Apple" "Banana" โ identified by "Banana" "Orange" โ identified by "Orange"
But real app data is usually more complicated than strings.
That's where models become useful.
6. Real Apps Use Models
Imagine we're building a task app.
A task isn't just a piece of text.
It might contain:
Title Completed status Priority Due date Notes
We could represent a simple task with a Swift struct:
struct Task {
let title: String
let isCompleted: Bool
}
Now a Task represents one piece of app data.
For example:
let task = Task(
title: "Learn SwiftUI",
isCompleted: true
)
Think of the model like a small container:
Task โโโ title: "Learn SwiftUI" โโโ isCompleted: true
We can access its values:
task.title
task.isCompleted
This is a big step toward how real apps are structured.
Instead of our UI containing all the information directly, we have:
DATA MODEL
โ
UI
7. Creating Multiple Tasks
Now let's create several tasks:
let tasks = [
Task(title: "Learn SwiftUI", isCompleted: true),
Task(title: "Build a small app", isCompleted: false),
Task(title: "Practice every day", isCompleted: false)
]
Conceptually:
tasks
โ
โโโ Task
โ โโโ Learn SwiftUI
โ โโโ true
โ
โโโ Task
โ โโโ Build a small app
โ โโโ false
โ
โโโ Task
โโโ Practice every day
โโโ false
Now our screen has actual structured data to work with.
But we still need one thing.
SwiftUI needs to know the identity of each Task.
8. Meet Identifiable
Swift has a protocol called:
Identifiable
We can make our model conform to it:
struct Task: Identifiable {
let id = UUID()
let title: String
let isCompleted: Bool
}
Now every task gets its own unique id.
You can imagine:
Task โโโ id: A1F2... โโโ title: "Learn SwiftUI" โโโ isCompleted: true
Another task:
Task โโโ id: B7C4... โโโ title: "Build a small app" โโโ isCompleted: false
Even if two tasks happen to have the same title, their IDs can still be different.
For example:
Task id: 101 title: "Practice" Task id: 202 title: "Practice"
Same title.
Different identity.
That's much more reliable than using the title itself as the identity.
9. ForEach Gets Cleaner With Identifiable
Earlier we wrote:
ForEach(fruits, id: \.self) { fruit in
Text(fruit)
}
But when our model conforms to Identifiable, SwiftUI already knows how to identify each item.
So we can write:
ForEach(tasks) { task in
Text(task.title)
}
That's it.
No:
id: \.self
needed.
SwiftUI uses:
task.id
behind the scenes.
Our data flow now looks like:
tasks โ โผ ForEach โ โโโ Task 1 โโโโ View โโโ Task 2 โโโโ View โโโ Task 3 โโโโ View
This pattern appears constantly in SwiftUI applications.
10. Let's Make Each Row More Interesting
We don't have to create only a Text.
Each item can produce an entire layout.
For example:
ForEach(tasks) { task in
HStack {
Image(
systemName: task.isCompleted
? "checkmark.circle.fill"
: "circle"
)
Text(task.title)
Spacer()
}
}
Now our data controls multiple parts of the UI.
If:
task.isCompleted == true
we show:
โ Learn SwiftUI
Otherwise:
โ Build a small app
Conceptually:
Task Data โ โโโ title โโโโโโโโโโโโ Text โ โโโ isCompleted โโโโโโ Image
This is the same idea we learned in Lesson 3:
State and data determine the UI.
We're simply applying it to collections of data now.
11. VStack vs List
We could display our tasks using:
VStack {
ForEach(tasks) { task in
Text(task.title)
}
}
That's perfectly valid.
But SwiftUI also provides a container specifically designed for displaying collections of rows:
List
For example:
List {
ForEach(tasks) { task in
Text(task.title)
}
}
Now the interface behaves more like a standard scrolling list.
You can think of the difference like this:
VStack โ โโโ General vertical layout โโโ You control the surrounding layout โโโ Useful for smaller groups of views List โ โโโ Designed for collections of rows โโโ Scrolls naturally โโโ Useful for list-style interfaces
We'll learn more advanced List features later.
For now, the important idea is simply:
Listis a container designed for displaying collections of data.
12. List Can Work Directly With Data
Because our Task model is Identifiable, we can make the code even shorter.
Instead of:
List {
ForEach(tasks) { task in
Text(task.title)
}
}
we can write:
List(tasks) { task in
Text(task.title)
}
Read that as:
Create a list using these tasks, and create this row for each task.
Conceptually:
tasks โ List โ Task โ Row
This is a very common SwiftUI pattern.
13. Let's Create a Reusable Row
Our task row is starting to contain several views:
HStack {
Image(...)
Text(...)
Spacer()
}
Rather than keeping all of that inside our List, we can create another SwiftUI view.
struct TaskRow: View {
let task: Task
var body: some View {
HStack {
Image(
systemName: task.isCompleted
? "checkmark.circle.fill"
: "circle"
)
Text(task.title)
Spacer()
}
}
}
Notice this line:
let task: Task
TaskRow receives a task and uses it to build its UI.
Now our list becomes:
List(tasks) { task in
TaskRow(task: task)
}
Much cleaner.
The flow is:
TaskListView
โ
โผ
tasks
โ
โผ
List
โ
โผ
each Task
โ
โผ
TaskRow
This is our first taste of building interfaces from small reusable views.
14. Let's Build the Task List
Now let's combine everything.
struct Task: Identifiable {
let id = UUID()
let title: String
let isCompleted: Bool
}
Then our row:
struct TaskRow: View {
let task: Task
var body: some View {
HStack(spacing: 12) {
Image(
systemName: task.isCompleted
? "checkmark.circle.fill"
: "circle"
)
Text(task.title)
Spacer()
}
}
}
And finally the screen:
struct TaskListView: View {
let tasks = [
Task(
title: "Learn SwiftUI",
isCompleted: true
),
Task(
title: "Build a small app",
isCompleted: false
),
Task(
title: "Practice every day",
isCompleted: false
)
]
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("My Tasks")
.font(.largeTitle)
.fontWeight(.bold)
List(tasks) { task in
TaskRow(task: task)
}
}
.padding()
}
}
Now look at how the pieces fit together:
Task โ defines one item tasks โ stores many Task values List โ goes through the tasks TaskRow โ turns each Task into UI
The important thing is that we aren't manually creating:
TaskRow 1 TaskRow 2 TaskRow 3
Our data determines how many rows exist.
15. What Happens If We Add Another Task?
This is where the benefit becomes obvious.
Suppose we add:
Task(
title: "Read Swift documentation",
isCompleted: false
)
Our array now contains four tasks.
We don't need to add another TaskRow.
We don't need to change the List.
We don't need to change ForEach.
The data changed:
3 Tasks โ 4 Tasks
and therefore the UI becomes:
3 Rows โ 4 Rows
That's the key idea.
DATA CHANGES
โ
NUMBER OF ITEMS CHANGES
โ
SWIFTUI BUILDS THE APPROPRIATE UI
16. Counting Completed Tasks
Because our information is now represented as data, we can do more with it.
Suppose we want to show:
1 of 3 completed
We can calculate the completed tasks:
let completedCount = tasks.filter { task in
task.isCompleted
}.count
Then display:
Text("\(completedCount) of \(tasks.count) completed")
Our screen can now derive information from the same data used to build the list.
Conceptually:
tasks
โ
โโโโโโโโโดโโโโโโโโโ
โผ โผ
List completedCount
โ โ
โผ โผ
Rows "1 of 3 completed"
One collection of data can drive multiple parts of the interface.
17. Models Keep UI and Data Clear
Compare these two approaches.
Hardcoded UI
Text("โ Learn SwiftUI")
Text("โ Build a small app")
Text("โ Practice every day")
The information is buried directly inside the views.
Now compare that with:
let tasks = [
Task(title: "Learn SwiftUI", isCompleted: true),
Task(title: "Build a small app", isCompleted: false),
Task(title: "Practice every day", isCompleted: false)
]
and:
List(tasks) { task in
TaskRow(task: task)
}
Now we have a much clearer separation:
DATA
Task
tasks
โ
UI
TaskRow
TaskListView
This becomes increasingly important as applications grow.
18. A Very Important Distinction
You might be wondering:
Why are our tasks declared with
let? Didn't Lesson 3 teach us@State?
Good question.
In this lesson, we're focusing on representing and displaying collections of data.
Our tasks aren't changing yet:
let tasks = [...]
The user cannot add, delete or complete tasks.
That is intentional.
If we tried to introduce:
Arrays Models ForEach List Identifiable @State arrays Bindings Editing Deleting Adding
all at once, we'd bury the important idea.
For now:
Lesson 3 State can change UI. Lesson 4 Collections of data can create UI.
Later, we'll combine those ideas.
That's when things get really interesting.
19. ForEach or List?
Beginners often wonder which one they should use.
They aren't really competitors.
ForEach means:
Create views for each item in a collection.
List means:
Display content using a list-style container.
You can use them together:
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
}
Or List can work directly with identifiable data:
List(tasks) { task in
TaskRow(task: task)
}
And ForEach can be used outside a List:
VStack {
ForEach(tasks) { task in
TaskRow(task: task)
}
}
So remember:
ForEach โ Repeats views from data List โ Provides a list-style container
20. Lesson 4 Mental Model
If you remember one diagram from this lesson, remember this:
DATA
โโโโโโโโโโโโโโโโ
โ tasks โ
โโโโโโโโฌโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโ
โ List/ForEach โ
โโโโโโโโฌโโโโโโโโ
โ
โโโโโโโโโผโโโโโโโโโ
โผ โผ โผ
Task Task Task
โ โ โ
โผ โผ โผ
Row Row Row
Or even more simply:
DATA โ ForEach / List โ UI
Lesson 3 taught us:
STATE โ UI
Lesson 4 adds:
COLLECTIONS OF DATA
โ
UI
Those two ideas are going to come together repeatedly throughout SwiftUI.
Your Challenge
Build a simple Favourite Movies screen.
Your data should contain at least four movies.
Start with a model:
struct Movie: Identifiable {
let id = UUID()
let title: String
let year: Int
}
Create an array:
let movies = [
Movie(title: "Movie One", year: 2022),
Movie(title: "Movie Two", year: 2023),
Movie(title: "Movie Three", year: 2024),
Movie(title: "Movie Four", year: 2025)
]
Your screen might look like:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โ Favourite Movies โ โ โ โ ๐ฌ Movie One โ โ 2022 โ โ โ โ ๐ฌ Movie Two โ โ 2023 โ โ โ โ ๐ฌ Movie Three โ โ 2024 โ โ โ โ ๐ฌ Movie Four โ โ 2025 โ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Requirements:
- Create a
Moviemodel. - Make it conform to
Identifiable. - Store several movies in an array.
- Display them using
ListorForEach. - Show both the movie title and year.
- Create a separate
MovieRowview.
Bonus challenge
Add another property:
let isFavorite: Bool
Then display:
โฅ
for favourite movies and:
โก
for the others.
Don't make the heart interactive yet.
For now, let the model's data determine which image the UI displays.
What You Now Know
You can now represent collections of information and turn them into SwiftUI views.
You've learned:
Array
struct
ForEach
List
Identifiable
UUID
filter
You also understand why models matter.
Instead of:
UI UI UI UI UI
we can think:
DATA
โ
SwiftUI
โ
MANY VIEWS
And our first four lessons now form a very useful progression:
Lesson 1
What is a SwiftUI View?
โ
Lesson 2
How do we arrange views?
โ
Lesson 3
How does user interaction change the UI?
โ
Lesson 4
How does data create repeated UI?
We're now ready for something every real application eventually needs:
Lesson 5 โ Navigation & Multiple Screens
So far, everything we've built lives on one screen.
Next we'll learn how to move from:
Task List
to:
Task List
โ
Task Details
using NavigationStack, NavigationLink, and passing data from one screen to another.
That's when our small SwiftUI examples begin turning into a multi-screen application.