SwiftUI for Beginners ยท Lesson 5 of 9

Navigation & Multiple Screens

So far, everything we've built has lived on a single screen.

In Lesson 4, we created a list of data:

My Tasks

โœ“ Learn SwiftUI
โ—‹ Build a small app
โ—‹ Practice every day

But real apps rarely have only one screen.

Think about apps you already use:

Messages
   โ†“
Conversation

Products
   โ†“
Product Details

Contacts
   โ†“
Contact Details

Settings
   โ†“
Account Settings

A very common pattern is:

LIST
  โ†“
SELECT AN ITEM
  โ†“
DETAIL SCREEN

In this lesson, we'll learn how SwiftUI handles navigation using:

NavigationStack โ†’ NavigationLink โ†’ Destination โ†’ Passing Data

By the end, we'll turn our task list into a small multi-screen app:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚          My Tasks            โ”‚
โ”‚                              โ”‚
โ”‚  โœ“ Learn SwiftUI          >  โ”‚
โ”‚                              โ”‚
โ”‚  โ—‹ Build a small app      >  โ”‚
โ”‚                              โ”‚
โ”‚  โ—‹ Practice every day     >  โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

              โ†“ Tap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  < My Tasks                  โ”‚
โ”‚                              โ”‚
โ”‚         Task Details         โ”‚
โ”‚                              โ”‚
โ”‚       Learn SwiftUI          โ”‚
โ”‚                              โ”‚
โ”‚          Completed           โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The important idea is:

Navigation lets the user move between screens while carrying the data needed by the next screen.

1. Why Do Apps Need Multiple Screens?

Imagine putting an entire app on one screen:

Tasks
Task Details
Profile
Settings
Search
Notifications
Help

That would quickly become overwhelming.

Instead, we divide the app into smaller screens.

For example:

Home
 โ”‚
 โ”œโ”€โ”€ Tasks
 โ”‚     โ”‚
 โ”‚     โ””โ”€โ”€ Task Details
 โ”‚
 โ”œโ”€โ”€ Profile
 โ”‚
 โ””โ”€โ”€ Settings

Each screen has a particular responsibility.

In SwiftUI, each screen is usually just another View.

For example:

struct HomeView: View {
    var body: some View {
        Text("Home")
    }
}

and:

struct DetailView: View {
    var body: some View {
        Text("Details")
    }
}

We already know how to create views.

Now we need a way to move between them.

SwiftUI provides:

NavigationStack

A NavigationStack creates an environment where views can navigate from one screen to another.

Let's start with:

struct HomeView: View {

    var body: some View {

        NavigationStack {
            Text("Home")
        }
    }
}

Visually, nothing dramatic happens yet.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              โ”‚
โ”‚             Home             โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

But now the screen is inside a navigation system.

Think of NavigationStack as the container that manages our navigation history.

NavigationStack
      โ”‚
      โ–ผ
    Home

Later, when we move to another screen:

NavigationStack
      โ”‚
      โ”œโ”€โ”€ Home
      โ”‚
      โ””โ”€โ”€ Details

SwiftUI remembers that Details came after Home.

That allows the user to go back.

Screens inside a NavigationStack can have navigation titles.

NavigationStack {

    Text("Welcome!")

        .navigationTitle("Home")
}

Now we get something like:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Home                         โ”‚
โ”‚                              โ”‚
โ”‚          Welcome!            โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The modifier:

.navigationTitle("Home")

tells the navigation system what title this screen should display.

For our task app, we might write:

.navigationTitle("My Tasks")

Now we need something the user can tap.

SwiftUI provides:

NavigationLink

Here's a simple example:

NavigationStack {

    NavigationLink("Show Details") {
        DetailView()
    }
}

When the user sees:

Show Details >

and taps it, SwiftUI displays:

DetailView()

The flow is:

HomeView
   โ”‚
   โ”‚ Tap NavigationLink
   โ–ผ
DetailView

Let's create the destination:

struct DetailView: View {

    var body: some View {
        Text("This is the detail screen")
            .navigationTitle("Details")
    }
}

Now we have two screens.

5. Our First Multi-Screen App

Let's put everything together.

struct HomeView: View {

    var body: some View {

        NavigationStack {

            VStack(spacing: 20) {

                Text("Welcome")
                    .font(.largeTitle)
                    .fontWeight(.bold)

                NavigationLink("Open Details") {
                    DetailView()
                }
            }
            .navigationTitle("Home")
        }
    }
}

And:

struct DetailView: View {

    var body: some View {

        VStack(spacing: 20) {

            Text("Details")
                .font(.largeTitle)
                .fontWeight(.bold)

            Text("You navigated to another screen!")
        }
        .navigationTitle("Details")
    }
}

The user experience becomes:

HOME

Welcome

[ Open Details ]
       โ”‚
       โ”‚ tap
       โ–ผ
DETAILS

You navigated to another screen!

SwiftUI also gives us a Back button automatically.

< Home        Details

We didn't have to create it ourselves.

Because the screens are inside a NavigationStack, SwiftUI understands the navigation history.

6. How the Navigation Stack Works

The word stack is important.

Imagine a stack of cards.

Initially:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚    Home     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Navigate to Details:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Details   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Home     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Details is now on top.

Navigate again:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ More Info   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚   Details   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Home     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When the user taps Back:

More Info
    โ†“ removed

Details becomes visible again

Back again:

Details
   โ†“ removed

Home becomes visible again

Conceptually:

PUSH
 โ†“

Home โ†’ Details โ†’ More Info

                  โ†‘
                 POP

Home โ† Details โ† More Info

You don't need to memorize the words push and pop right now.

The important idea is:

NavigationStack remembers the screens the user moved through.

Opening a generic Detail screen is useful for learning navigation.

But real apps usually need something more.

Imagine our task list:

Learn SwiftUI
Build a small app
Practice every day

If the user taps:

Learn SwiftUI

the detail screen should show:

Learn SwiftUI

If they tap:

Practice every day

the detail screen should show:

Practice every day

We don't want to create:

LearnSwiftUIDetailView
BuildSmallAppDetailView
PracticeEveryDayDetailView

Instead, we'll create one reusable detail screen and pass the selected task into it.

8. Bringing Back Our Task Model

From Lesson 4, we had a model like this:

struct Task: Identifiable {
    let id = UUID()
    let title: String
    let isCompleted: Bool
}

And some data:

let tasks = [
    Task(
        title: "Learn SwiftUI",
        isCompleted: true
    ),
    Task(
        title: "Build a small app",
        isCompleted: false
    ),
    Task(
        title: "Practice every day",
        isCompleted: false
    )
]

Remember the relationship:

Task
  โ†“
represents one item

tasks
  โ†“
contains many Task values

Now we're going to use the same data for navigation.

9. Creating a Task Detail Screen

Let's create:

struct TaskDetailView: View {

    let task: Task

    var body: some View {

        VStack(spacing: 20) {

            Image(
                systemName: task.isCompleted
                    ? "checkmark.circle.fill"
                    : "circle"
            )
            .font(.system(size: 60))

            Text(task.title)
                .font(.title)
                .fontWeight(.bold)

            Text(
                task.isCompleted
                    ? "Completed"
                    : "Not Completed"
            )
            .foregroundStyle(.secondary)

            Spacer()
        }
        .padding()
        .navigationTitle("Task Details")
    }
}

The most important line is:

let task: Task

This means:

TaskDetailView needs a Task in order to display itself.

The screen doesn't own a particular task.

It receives one.

10. Passing Data to Another Screen

Suppose we have:

let task = Task(
    title: "Learn SwiftUI",
    isCompleted: true
)

We can create the detail screen like this:

TaskDetailView(task: task)

We're passing the task into the next view.

Conceptually:

TaskListView

selected Task
     โ”‚
     โ”‚ pass data
     โ–ผ

TaskDetailView

Then the detail screen can read:

task.title

and:

task.isCompleted

So the destination isn't hardcoded.

Its UI depends on the task it receives.

Now we can connect everything.

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 {

        NavigationStack {

            List(tasks) { task in

                NavigationLink {
                    TaskDetailView(task: task)
                } label: {
                    TaskRow(task: task)
                }
            }
            .navigationTitle("My Tasks")
        }
    }
}

Look carefully at this part:

List(tasks) { task in

For every task, SwiftUI creates a row.

Then:

NavigationLink {
    TaskDetailView(task: task)
} label: {
    TaskRow(task: task)
}

The row becomes tappable.

Tap that row and SwiftUI creates:

TaskDetailView(task: task)

using the exact task that was selected.

12. Follow One Task Through the App

This is worth visualizing.

Our array contains:

Task 1
title: Learn SwiftUI
completed: true

Task 2
title: Build a small app
completed: false

Task 3
title: Practice every day
completed: false

List creates:

โœ“ Learn SwiftUI          >
โ—‹ Build a small app      >
โ—‹ Practice every day     >

Now the user taps:

โ—‹ Build a small app      >

That row represents:

Task
โ”œโ”€โ”€ title: Build a small app
โ””โ”€โ”€ isCompleted: false

SwiftUI passes that value into:

TaskDetailView(task: task)

The detail screen reads the data and displays:

Task Details

โ—‹

Build a small app

Not Completed

The flow is:

tasks
  โ†“
List
  โ†“
TaskRow
  โ†“
User taps a task
  โ†“
NavigationLink
  โ†“
Selected Task
  โ†“
TaskDetailView

This is an extremely common app pattern.

13. One Detail Screen, Many Possible Results

The nice part is that we only created:

TaskDetailView

once.

But it can display many different tasks.

TaskDetailView
      โ–ฒ
      โ”‚
 โ”Œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 โ”‚    โ”‚              โ”‚
 โ”‚    โ”‚              โ”‚
Task 1             Task 2             Task 3
 โ”‚                   โ”‚                  โ”‚
 โ–ผ                   โ–ผ                  โ–ผ
Learn SwiftUI   Build small app   Practice every day

The view structure stays the same.

The data changes.

This should feel familiar from previous lessons.

Lesson 3:

State changes
     โ†“
UI changes

Lesson 4:

Collection changes
      โ†“
Repeated UI changes

Lesson 5:

Selected data
      โ†“
Destination UI

We're gradually building the SwiftUI mental model.

14. Let's Create a Better Task Row

We can keep our row reusable:

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()
        }
    }
}

Notice that TaskRow doesn't know anything about navigation.

It only knows:

Give me a task and I'll display a row.

Then TaskListView decides:

This row should navigate somewhere when tapped.

That's a useful separation.

TaskRow
   โ†“
How one task looks


TaskListView
   โ†“
How tasks are listed
and where tapping goes


TaskDetailView
   โ†“
How one selected task
looks on its own screen

Each view has a clear job.

15. The Complete Example

Here's the complete version.

First, our model:

struct Task: Identifiable {
    let id = UUID()
    let title: String
    let isCompleted: Bool
}

Our reusable 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()
        }
    }
}

Our detail screen:

struct TaskDetailView: View {

    let task: Task

    var body: some View {

        VStack(spacing: 20) {

            Image(
                systemName: task.isCompleted
                    ? "checkmark.circle.fill"
                    : "circle"
            )
            .font(.system(size: 60))

            Text(task.title)
                .font(.title)
                .fontWeight(.bold)
                .multilineTextAlignment(.center)

            Text(
                task.isCompleted
                    ? "Completed"
                    : "Not Completed"
            )
            .foregroundStyle(.secondary)

            Spacer()
        }
        .padding()
        .navigationTitle("Task Details")
    }
}

And our main 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 {

        NavigationStack {

            List(tasks) { task in

                NavigationLink {
                    TaskDetailView(task: task)
                } label: {
                    TaskRow(task: task)
                }
            }
            .navigationTitle("My Tasks")
        }
    }
}

Our application now has an actual screen flow:

TaskListView
     โ”‚
     โ”‚ User selects Task
     โ–ผ
TaskDetailView
     โ”‚
     โ”‚ Back
     โ–ผ
TaskListView

16. Where Should NavigationStack Go?

A common beginner question is:

Should every screen have its own NavigationStack?

Usually, no.

For this example, the stack starts around the main screen:

NavigationStack {
    List {
        // ...
    }
}

Then the destination is pushed inside that same navigation stack.

NavigationStack
      โ”‚
      โ”œโ”€โ”€ TaskListView
      โ”‚
      โ””โ”€โ”€ TaskDetailView

We generally don't need:

NavigationStack
    โ†“
TaskListView
    โ†“
another NavigationStack
    โ†“
TaskDetailView

Think of one NavigationStack as managing a particular navigation journey.

For now, our simple rule is:

Put the main screen of this navigation flow inside a NavigationStack, then navigate to destination views from there.

17. Navigation Titles Belong to Screens

Our list screen has:

.navigationTitle("My Tasks")

Our detail screen has:

.navigationTitle("Task Details")

That means each screen describes its own navigation title.

TaskListView
     โ†“
"My Tasks"

TaskDetailView
     โ†“
"Task Details"

This keeps the responsibility close to the screen itself.

18. Passing More Than One Value

A destination doesn't have to receive an entire model.

For example, we could write:

struct ProfileView: View {

    let name: String
    let age: Int

    var body: some View {

        VStack {
            Text(name)
            Text("Age: \(age)")
        }
    }
}

Then:

ProfileView(
    name: "Alex",
    age: 28
)

But when several values belong together, a model often makes more sense.

Instead of:

TaskDetailView(
    title: task.title,
    isCompleted: task.isCompleted
)

we can simply pass:

TaskDetailView(task: task)

Conceptually:

Instead of carrying:

title
status
priority
due date
notes

separately...

carry:

Task

That's another reason models are so useful.

You may eventually see SwiftUI code like:

.sheet(...)

A sheet is another way of presenting content, but it represents a different interaction.

Navigation usually feels like:

List
  โ†“
Details
  โ†“
More Details

A sheet usually appears temporarily over the current screen:

Current Screen
      โ”‚
      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     โ”‚
โ”‚        Sheet        โ”‚
โ”‚                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

For example:

Navigation
โ†’ Open a product's details

Sheet
โ†’ Show a temporary Add Item form

We don't need to learn sheets yet.

For this lesson, keep the mental model focused on:

NavigationStack
       โ†“
NavigationLink
       โ†“
Destination

20. Don't Put Everything in One View

Now that we're creating multiple screens, it's tempting to write everything inside one giant view.

For example:

TaskListView
โ”œโ”€โ”€ model code
โ”œโ”€โ”€ row layout
โ”œโ”€โ”€ detail layout
โ”œโ”€โ”€ list layout
โ”œโ”€โ”€ navigation
โ””โ”€โ”€ everything else

It may work initially, but it becomes harder to understand.

Our current structure is much clearer:

Task
   โ†“
Data model

TaskRow
   โ†“
One row

TaskListView
   โ†“
List screen

TaskDetailView
   โ†“
Detail screen

This is one of the most useful habits you can develop early:

Break a screen into views that have clear responsibilities.

That doesn't mean every Text needs its own view.

It means meaningful pieces of UI can become reusable components.

21. A Common Beginner Mistake

Imagine we have:

NavigationLink("Learn SwiftUI") {
    TaskDetailView(
        task: Task(
            title: "Learn SwiftUI",
            isCompleted: true
        )
    )
}

Then:

NavigationLink("Build a small app") {
    TaskDetailView(
        task: Task(
            title: "Build a small app",
            isCompleted: false
        )
    )
}

Then another one.

This works, but we've gone back to hardcoding our data into the UI.

Lesson 4 taught us a better approach:

List(tasks) { task in

and then:

NavigationLink {
    TaskDetailView(task: task)
} label: {
    TaskRow(task: task)
}

Now the same structure works for:

3 tasks
30 tasks
300 tasks

The navigation is being created from the data.

22. Data-Driven Navigation

Let's look at the bigger idea.

Our data is:

tasks

That data creates:

rows

Each row creates a navigation opportunity:

row
 โ†“
NavigationLink

And the selected data creates the destination:

selected task
     โ†“
TaskDetailView

So the entire flow is driven by data:

             tasks
               โ”‚
               โ–ผ
              List
               โ”‚
      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
      โ–ผ        โ–ผ        โ–ผ
    Task     Task      Task
      โ”‚        โ”‚         โ”‚
      โ–ผ        โ–ผ         โ–ผ
     Row      Row       Row
      โ”‚        โ”‚         โ”‚
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚
            User taps
               โ”‚
               โ–ผ
         Selected Task
               โ”‚
               โ–ผ
        TaskDetailView

That's a much more powerful mental model than thinking:

Button 1 opens screen 1, Button 2 opens screen 2...

23. Lesson 5 Mental Model

If you remember one flow from this lesson, remember:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ NavigationStack โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   List Screen   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ NavigationLink
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Selected Data  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ”‚ pass data
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Detail Screen  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Or more simply:

SCREEN
  โ†“
USER SELECTS SOMETHING
  โ†“
PASS THE SELECTED DATA
  โ†“
NEXT SCREEN

And SwiftUI's basic tools are:

NavigationStack
      โ†“
NavigationLink
      โ†“
Destination View

Your Challenge

Your turn

Build a small Recipe Browser.

Start with a model:

struct Recipe: Identifiable {
    let id = UUID()
    let name: String
    let time: Int
    let difficulty: String
}

Create several recipes:

let recipes = [
    Recipe(
        name: "Pasta",
        time: 25,
        difficulty: "Easy"
    ),
    Recipe(
        name: "Vegetable Curry",
        time: 40,
        difficulty: "Medium"
    ),
    Recipe(
        name: "Pancakes",
        time: 20,
        difficulty: "Easy"
    )
]

Your first screen should look roughly like:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Recipes                      โ”‚
โ”‚                              โ”‚
โ”‚ ๐Ÿ Pasta                  >  โ”‚
โ”‚    25 minutes                โ”‚
โ”‚                              โ”‚
โ”‚ ๐Ÿ› Vegetable Curry        >  โ”‚
โ”‚    40 minutes                โ”‚
โ”‚                              โ”‚
โ”‚ ๐Ÿฅž Pancakes               >  โ”‚
โ”‚    20 minutes                โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When the user taps:

Vegetable Curry

navigate to:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ < Recipes     Recipe Details โ”‚
โ”‚                              โ”‚
โ”‚      Vegetable Curry         โ”‚
โ”‚                              โ”‚
โ”‚        40 minutes            โ”‚
โ”‚                              โ”‚
โ”‚          Medium              โ”‚
โ”‚                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Requirements:

  • Create a Recipe model.
  • Make it conform to Identifiable.
  • Store multiple recipes in an array.
  • Put the main screen inside a NavigationStack.
  • Display the recipes using List.
  • Make each recipe navigable using NavigationLink.
  • Create a separate RecipeRow.
  • Create a separate RecipeDetailView.
  • Pass the selected Recipe into the detail screen.
  • Display its name, cooking time, and difficulty.

Bonus challenge

Add another property:

let isFavorite: Bool

Show:

โ™ฅ Favourite

on the detail screen when it's true.

Otherwise show:

โ™ก Not Favourite

Don't make the favourite status interactive yet.

The goal is still:

Pass data from one screen to another and let that data determine the destination UI.

What You Now Know

You can now build a basic multi-screen SwiftUI application.

You've learned:

NavigationStack

NavigationLink

.navigationTitle()

Destination Views

Passing Data Between Views

And you've combined them with concepts from previous lessons:

struct

Identifiable

List

ForEach

Reusable Views

Our course progression now looks like:

Lesson 1
What is a SwiftUI View?
        โ†“
Lesson 2
How do we arrange views?
        โ†“
Lesson 3
How does interaction change state and UI?
        โ†“
Lesson 4
How does data create repeated UI?
        โ†“
Lesson 5
How do we move between screens
and pass selected data?

We've now reached an important point.

Our app can:

Display UI
   โœ“

Respond to interaction
   โœ“

Represent collections of data
   โœ“

Create repeated views
   โœ“

Navigate between screens
   โœ“

Pass data forward
   โœ“

But there's a new problem.

So far, when a child view receives something like:

let task: Task

it can read that data.

What if the child view needs to change data owned by another view?

That's where our next lesson begins.

Lesson 6 โ€” Sharing State Between Views

We'll learn how a parent and child view can work with the same state using concepts such as:

@State
@Binding

and build the mental model:

Parent owns the state
        โ†“
Child receives a binding
        โ†“
Child changes the value
        โ†“
Parent and child stay in sync

That's when the @State we learned in Lesson 3 and the reusable views we've been building start working together across multiple views.