Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Debugging Swift with Compiler Messages, Breakpoints, and the Console

Recommended resources

Good to see you again. In the previous lesson, you created the PlantIdentifier SwiftUI project, selected an iPhone simulator, and established the core loop: edit, build, run, observe. This lesson adds the other half of that loop: when the result is not what you expect, use Xcode to find out why.

A bug is not always a crash. It may prevent the app from building, cause a button to do nothing, or produce a plausible but wrong result. You will diagnose one controlled compiler error and one controlled behavior bug using three everyday Xcode tools:

  • Compiler messages for code Xcode cannot build.
  • Breakpoints to pause a running app at a chosen line.
  • The debug console to inspect values while execution is paused.

By the end, you will have a repeatable method rather than relying on guesses or randomly changing code.


Start with the type of failure

Before opening tools, classify what happened. This keeps debugging focused.

What you observe Likely category First tool
The app does not build or launch Compile-time error Issue Navigator and editor diagnostics
The app launches but unexpectedly stops or crashes Runtime failure Console, breakpoint, stack trace
The app runs but gives the wrong result Logic bug Breakpoints and variable inspection

A compiler error is a rule violation the compiler can prove before the app runs: a misspelled variable, missing argument, incompatible type, or malformed syntax. Until it is fixed, there is no new app binary to debug.

A logic bug is different. The code is legal Swift, but it expresses the wrong instruction. For example, assigning a counter the value 1 is perfectly valid Swift—even if the intended behavior was to increase the counter by one.

A useful process is RIFT:

  1. Reproduce — trigger the problem consistently.
  2. Investigate — gather evidence about code flow and values.
  3. Fix — make the smallest change justified by that evidence.
  4. Test — repeat the original scenario and check nearby behavior.

Watch this short introduction from Paul Solt before working through the examples. Focus on the distinction between having a theory and gathering evidence to test it.

Xcode 16 Debugging Tutorial for Beginners (2025)

In “Xcode 16 Debugging Tutorial for Beginners (2025),” Paul Solt introduces the RIFT workflow that will structure this lesson.

Watch the RIFT method. Notice why a reliably repeatable bug is much easier to investigate than a one-off symptom.



Compiler messages: turn a red error into a precise question

Xcode checks Swift as you type and again when you build. A red underline in the editor is an error; a yellow underline is generally a warning. Errors prevent a successful build, while warnings allow a build but point to something worth understanding.

Read the following sections from CodeWithChris for a quick orientation to the Issue Navigator, breakpoint gutter, and bottom debug area.

Xcode Tutorial for Beginners (2026) : CodeWithChris

This CodeWithChris guide provides a compact map of the Xcode areas you will use: the Issue Navigator for compiler messages, the breakpoint gutter for pausing execution, and the debug area for variables and console output.

First, in the “Issue Navigator” subsection of the navigator overview, read the error and warning explanation. Then, in Chapter 7, read the “Breakpoints” subsection from the breakpoint gutter description. Finally, in Chapter 11, read from the debug-area overview. Focus on what each tool tells you, rather than memorizing every button.

A safe compiler-error experiment

Open ContentView.swift. If you changed the template greeting previously, that is fine. Replace the ContentView struct with the following code. This gives us a deliberately tiny feature to investigate: a counter for completed plant identifications.

struct ContentView: View {
    @State private var identificationCount = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Identifications: \(identificationCount)")

            Button("Record identification") {
                recordIdentification()
            }
        }
        .padding()
    }

    private func recordIdentification() {
        let increment = 1
        identificationCount += increment
    }
}

You do not need to fully understand @State yet; a later lesson will cover it properly. For now, treat identificationCount as the value shown by the interface, and recordIdentification() as the code run when the button is tapped.

Build and run with Command-R. Tapping Record identification twice should show:

Identifications: 2

Now create a compiler error. In the Text line only, change the variable name from identificationCount to identificationcount, using a lowercase c:

Text("Identifications: \(identificationcount)")

Build again using Command-B or run with Command-R. Xcode should report an error similar to:

Cannot find identificationcount in scope

The precise wording can vary between Swift and Xcode releases. The important parts are:

  • Cannot find tells you Swift is looking for a name it does not know.
  • identificationcount identifies the missing name.
  • in scope means the name is unavailable at that point in the code.

Open the Issue Navigator with Command-5. Select the first red error. Xcode takes you to the relevant location, often with a red underline and an issue bubble in the editor.

Do not blindly accept a suggested fix. Instead, compare the name in the error to the actual declaration:

@State private var identificationCount = 0

Swift is case-sensitive: identificationCount and identificationcount are different names. Restore the uppercase C, build again, and confirm the app runs.

A practical compiler-message routine

When a build fails:

  1. Read the first red error in the Issue Navigator.
  2. Click it to find the source location.
  3. Identify the code element named in the message: a variable, type, argument label, or syntax token.
  4. Compare it with its declaration or with the API Xcode expects.
  5. Make one focused correction.
  6. Build again.

One error can create several follow-on errors, especially after a missing brace or malformed declaration. Fixing the first meaningful error often removes much of the rest.

Apple’s documentation also explains how editor diagnostics and suggested “Fix-it” changes work.

Fixing issues in your code as you type | Apple Developer Documentation

Apple’s documentation explains the live red/yellow diagnostics in Xcode’s editor and how to evaluate an offered fix before applying it.

In the “Apply fixes to your code” section, read the full diagnostic and Fix-it workflow. Pay particular attention to the distinction between seeing a suggested correction and deciding whether it matches your intent.

Code completion also prevents many spelling mistakes: after typing enough of a known symbol, use Xcode’s suggestion list rather than retyping a long name. But code completion is prevention, not a substitute for understanding an error message when one appears.



Breakpoints: pause at the moment the state changes

Compiler messages answer, “Why can’t this code build?” A breakpoint helps answer, “What values does my app have when it behaves incorrectly?”

A breakpoint is a marker you put beside an executable line of code. When the running app reaches that line, Xcode pauses it before executing that line. The simulator may appear frozen; that is expected. A pause labeled something like Thread 1: breakpoint is not a crash.

The breakpoint gutter is the narrow margin to the left of the line numbers. Click it next to this line:

identificationCount += increment

A blue marker appears. Now run the app and tap Record identification. Xcode should return to the foreground, highlight the marked line, and open the debug area at the bottom.

Xcode is paused at a breakpoint on a SwiftUI counter-update line; the simulator remains frozen while the debug area exposes the counter’s current value and execution context.

At a breakpoint, separate two questions:

  1. Did execution reach this code?
    If the breakpoint pauses, yes. The button action and function call occurred.

  2. Are the values what the code needs?
    Inspect the variables currently in scope.

The left part of the debug area is usually the Variables View. It lists local variables and properties Xcode can inspect at this moment. In this function, the relevant values are:

  • increment, the local constant.
  • identificationCount, the property the function will update.

The highlighted line has not run yet. That timing matters: if Xcode pauses on identificationCount += increment, the displayed counter is the value before the update.

You can then use the debugger controls to:

  • Step over — execute the current line, then pause on the next line in the current function.
  • Continue/resume — let the app run normally until it reaches another breakpoint or stops for another reason.
  • Stop — terminate the app process.

For this small function, use Step over once. Then resume the app. It should show the next number.

The exact icons and their placement vary a little across Xcode versions, so hover over an icon in the debug area to see its label. What matters is the behavior: step over advances one executable statement without diving into its implementation.



The debug console: ask the paused app for evidence

The debug console is the right side of the bottom debug area. When your app is paused, you can type short debugger expressions after the (lldb) prompt.

For everyday Swift debugging, begin with po, which evaluates and prints an expression in the current paused context.

With the breakpoint still on the update line, tap the button so execution pauses. In the console, enter:

po identificationCount

Then enter:

po increment

For the first tap, you should see values equivalent to:

0
1

That means the program is about to add one to zero. Use Step over, then evaluate the count again:

po identificationCount

Now it should report 1.

This is fundamentally different from merely adding print() statements:

  • print() requires you to edit source code, rebuild, and run again.
  • The debugger lets you pause, inspect values, and advance one line at a time.
  • A breakpoint can establish both which path executed and how a value changed.

Sean Allen demonstrates precisely this use of a basic breakpoint and po in Xcode’s console. The surrounding project is more advanced than ours, but the mechanics are the same.

Xcode Breakpoints - Basic Debugging | Swift

In “Xcode Breakpoints - Basic Debugging | Swift,” Sean Allen demonstrates the key breakpoint state: the app is paused, a line is about to execute, and variables can be evaluated in the LLDB console.

Watch stepping and inspection. Focus on the highlighted current line, the step-over action, and how po tests a hypothesis about boolean values.

A helpful rule: inspect values; do not casually change app state in the console. The debugger can evaluate more complicated expressions, but during a first investigation, mutating values there can make the evidence harder to trust.



Diagnose a behavior bug from evidence

Now use the same tools on a bug that compiles successfully but gives the wrong result.

Change the final line in recordIdentification() from this:

identificationCount += increment

to this:

identificationCount = increment

The app still builds. On the first tap, the display changes from 0 to 1, so it may look correct at first. But tap again: it remains at 1. This is a logic bug.

Follow RIFT rather than jumping straight to a correction.

1. Reproduce

Stop and run the app again so the counter starts at zero.

  • Tap once: the display becomes 1.
  • Tap a second time: it stays at 1.

The behavior is consistent: each tap overwrites the value with 1.

2. Investigate

Set a breakpoint on:

identificationCount = increment

Tap once, then resume. On the second tap, execution pauses before the assignment.

In the console, enter:

po identificationCount

You should see:

1

Then inspect:

po increment

You should also see:

1

Now step over the assignment and inspect the count again:

po identificationCount

It remains 1.

That observation is strong evidence:

  • The button action is running, because the breakpoint was hit.
  • The existing count has the expected value before the update.
  • The increment has the expected value.
  • The update line replaces the old count with 1, rather than adding 1 to it.

3. Fix

Change the assignment back to an addition assignment:

identificationCount += increment

The two forms express different operations:

Code Meaning when identificationCount is 1 and increment is 1
identificationCount = increment Replace the count with 1
identificationCount += increment Add 1 to the existing count, producing 2

4. Test

Build and run again. Tap three times. The display should progress through 1, 2, and 3.

This final step is not ceremonial. A fix is only a hypothesis until the original failure no longer occurs. Test the exact sequence that reproduced the bug, then perform a nearby sanity check—for example, one additional tap—to make sure the behavior continues correctly.

When finished, remove the breakpoint by dragging its marker out of the gutter, or disable it if you may want to reuse it. Leaving active breakpoints scattered around a project can make normal testing unexpectedly pause later.



A compact debugging decision guide

Use this as your default response when your PlantIdentifier app misbehaves:

Avoid these common traps:

  • Fixing every error at once. Start with the first meaningful compiler error.
  • Treating a breakpoint as a crash. A breakpoint is an intentional pause.
  • Placing a breakpoint after the suspicious line. Put it on the line that changes state so you can see before-and-after values.
  • Changing code without reproducing the issue. You may accidentally hide the symptom while misunderstanding the cause.
  • Forgetting to test after fixing. Compilation proves syntax and types, not intended behavior.

You can now distinguish a compile-time error from a logic bug, use the Issue Navigator to interpret a compiler message, pause a SwiftUI app with a breakpoint, and inspect its current values using the Variables View and po in the debug console.

The central habit is simple: replace speculation with observation. First reproduce the symptom; then ask whether the code was reached, what values were present, and exactly how they changed.

Next, you will move from behavior to data modeling by representing plant information with Swift structs and stored properties.