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:

List is 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

Your turn

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 Movie model.
  • Make it conform to Identifiable.
  • Store several movies in an array.
  • Display them using List or ForEach.
  • Show both the movie title and year.
  • Create a separate MovieRow view.

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.