Chapters

Hide chapters

Real-World iOS by Tutorials

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

13. Debugging
Written by Aaqib Hussain

Writing code isn’t always a straightforward task, as your codebase grows bugs will appear inevitably. Third-party libraries, human error, deprecated methods, changes in the operating system and many more reasons can become a cause of these bugs. Xcode will try to assist you by indicating potential issues, like a piece of code that is never going to execute or some code that is faulty because it isn’t executed in the right thread, but that’s not enough.

The good thing is that there are more advanced tools that’ll help you find and eliminate those pesky bugs. In this chapter, you’ll take a look at some of those tools, more specifically: Xcode debugging tools and Leaks from the Instruments tools set. Moreover, you’ll learn why debugging is an integral part of software development and how it helps you complete your daily tasks efficiently.

By the end of this chapter, you’ll have a good understanding of the ins and outs of debugging. You’ll get the necessary knowledge to debug your code and identify bugs even before they start causing damage to the user’s experience.

Please note that this chapter is optional. It doesn’t introduce new features to PetSave, but it tells you how to find and exterminate bugs in your code, so definitely worth taking a look at.

Are you ready to squash some bugs? Here you go!

Debugging

Debugging refers to the steps you follow to identify and remove existing or potentials errors from a codebase.

Why do you need to debug your code?

Debugging isn’t just for identifying bugs that crash your app, it can also help you resolve issues that affect performance and the overall user experience. Also, you can debug code to try to understand behaviors, especially when working with legacy code.

Xcode debugging tools

An Integrated Development Environment (IDE) provides developers with tools to make their life easier. Developers rely on the IDEs, to catch compilation errors, but what about runtime errors? Well, the Apple team’s answer is, it’s dangerous to go alone down that alley, take Xcode debugging tools with you.

Xcode has a wide variety of tools to pull you out of these entanglements. Debugging tools is how you refer to the group formed by the following tools:

  • Breakpoints
  • Method call stack
  • Debugging views
  • Memory graph

More generically, you can say Instruments are also part of this toolset. You’ll learn more about Instruments later in this chapter.

Each tool provides its own set of benefits. Start by getting into these tools and learning about breakpoints.

Breakpoints

Breakpoints is the first and most basic tool in the Xcode tool belt. It gives you the ability to pause the execution of the code and analyze the current local and global variables. With the help of breakpoints, you can analyze code line by line.

Open this chapter’s starter project and build and run. You’ll notice a minor bug:

Displaying a list of animals.
Displaying a list of animals.

Did you notice something wrong? Everything looks fine. Nothing’s wrong there, right?

Wait, hold on.

If you remember correctly, you designed the tags to have their view color correspond to the animal’s age. Here you see the same age but different colors of tags. Something is wrong with the code that needs debugging.

Identifying and highlighting the bug.
Identifying and highlighting the bug.

Start with AnimalsNearYouView.swift. Inside NavigationView, place a breakpoint on:

AnimalListView(animals: animals)

To place a breakpoint, go to the line and click the margin or the line number on the left.

Note: To see the line number on the left, go to Xcode ▸ Preferences. Select Text Editing. Under Display, check Line numbers.

You’ll see a blue rectangular arrow like this:

Applying breakpoint.
Applying breakpoint.

Build and run. Notice the code execution stop at the breakpoint. In the image below, you’ll see some buttons on the Debug bar:

Getting to know about breakpoint buttons.
Getting to know about breakpoint buttons.

So, what’s the purpose of these buttons? They make the breakpoints more powerful. Take a look at each button, from left to right:

  1. Deactivate breakpoints: If you don’t want to debug anymore, toggle this button to disable all the breakpoints in the app.
  2. Continue program execution: Jumps to the next breakpoint if there are any. Otherwise, it runs the app normally.
  3. Step over: Takes you to the next line of execution, ignoring the current context.
  4. Step into: Takes you inside the current context of the line of execution.
  5. Step out: Takes you outside the current context of the line of execution.

Click Step into. This will let you into AnimalListView(animals:footer:). Now, inside NavigationLink, place a breakpoint on:

AnimalRow(animal: animal)

Using step into.
Using step into.

Then, click Continue program execution. Now code execution stops at AnimalRow(animal: animal).

Breakpoint stopping at the next line.
Breakpoint stopping at the next line.

It’s time to investigate the animal model. Ensure you see the Variables View window by clicking Show the Variables view at the bottom right.

Variables view window.
Variables view window.

Select animal in Variables view.

Inspecting animal.
Inspecting animal.

Then, click ⓘ, that is the Print Description, located at the bottom-left bar.

Printing description using the ⓘ.
Printing description using the ⓘ.

There’s another way you can print the object description. In the Console, type po animal and press enter. po extends to print the object. You’ll see a similar result.

Printing description using po.
Printing description using po.

Click Continue program execution again, so you can check more animals as the breakpoint pauses the execution. You’ll see the animal’s age parses correctly. That means there’s something wrong with presenting the data.

Open AnimalRow.swift, that takes care of presenting the data. Inside body, find this line:

Text(NSLocalizedString(Age.baby.rawValue, comment: ""))

The age is hardcoded here, that’s the problem! Replace the hardcoded age with the age from the model. Update the line to:

Text(NSLocalizedString(animal.age.rawValue, comment: ""))

Now you take age from the AnimalEntity. Deactivate the breakpoints, then build and run again. The bug is gone!

The bug fixed.
The bug fixed.

Woohooo! You did it. You squashed the bug that’s been bugging the app. ;]

You can also use breakpoints to add expressions, making your breakpoint stop only if a condition is met.

Open AnimalListView.swift and place a breakpoint on:

AnimalRow(animal: animal)

Double-click the breakpoint, and you’ll see:

Breakpoint window.
Breakpoint window.

Here’s a breakdown:

  1. You can enable or disable this breakpoint by checking this box.
  2. Enter the name of the breakpoint.
  3. Add the condition that stops the breakpoint.
  4. You can ignore the breakpoint as many times as you indicate.
  5. Use Add Action to add some actions, like running the debugger command po animal.name or executing a shell command.
  6. Check Options to continue after actions are evaluated.

Click Add Action and you’ll see:

Knowing about add action.
Knowing about add action.

  1. Use the + or - buttons to add more actions.
  2. Enter the debugging commands here. Commands like po animal.name will execute during debugging.

Now add the following data:

  • In the Name field, add LookingforAnimal.
  • Enter (animal.name?.count ?? 0) > 5 in the Condition field. Xcode’s code completion is available in these text fields to help you avoid typos.
  • Leave Ignore at 0.
  • The drop-down in Action adds different actions that Xcode provides. For simplicity, you’ll use Debugger Command here, but you can also use:

Action options.
Action options.

  • Enter po animal.name in the text field below the action drop-down.
  • Leave Options unchecked.

The breakpoint expression dialog looks like this:

Finishing conditional breakpoint.
Finishing conditional breakpoint.

Build and run. You’ll see the breakpoint stops when the animal’s name has more than five letters. Xcode also runs the action and prints the animal name in the console:

Testing the conditional breakpoint.
Testing the conditional breakpoint.

Notice that continuing the breakpoint doesn’t stop on any animal that has less than five letters. This can be a useful mechanism when you need to debug iterative code.

Method call stack

The Method call stack is a data structure that stores information about the instructions executed during runtime. It keeps the order of methods and their states in the memory. It also passes local variables to another method if needed.

Each thread has a stack that the OS maintains. The OS controls how methods are called and pass the variables between methods.

You can use the method call stack when you’re debugging to find the places where the error is happening and understand the flow that caused the issue.

Enable the breakpoint in AnimalsNearYouView.swift on the following line:

 AnimalListView(animals: animals)

Build and run. When the app stops at the breakpoint, check the Debug navigator on the left, it should look similar to this:

List of methods in the callstack.
List of methods in the callstack.

Here, you’ll see the call stack. Go ahead and navigate to each method. You can also find what’s triggering a specific method by retracing the steps.

Debugging views

Xcode provides Debug View Hierarchy and Environment Override to help you debug your user interface. Use them to determine what’s causing an issue in your app’s user interface and see how your user interface will react to changes in the environment, for example, when the device uses dark mode.

Look at the buttons next to Step out. You’ll see a view like this:

More Xcode features.
More Xcode features.

Take a closer look at these buttons:

  1. Debug View Hierarchy: Use this button to visualize your entire screen sometimes even component by component.
  2. Debug Memory Graph: Helps you visualize all the active objects in the app.
  3. Environment Overrides: This button can help you override some of the apps’ environment properties. For example, you can change the appearance or test accessibility features in real-time.

Debug view hierarchy

With the app still running, click Debug View Hierarchy. You’ll see a new bar appear on top of the Debug bar:

Debug View Hierarchy bar.
Debug View Hierarchy bar.

Here you see the app’s debug view of the current screen:

Studying view hierarchy.
Studying view hierarchy.

Here’s a breakdown of this screen:

  1. On the left, in the Debug navigator, you see the entire view hierarchy.
  2. Use this slider to increase or decrease spaces between the views and observe each view closely.
  3. Use the Show Clipped Content to see any views going out of the view bounds.
  4. Show Constraints displays the layout constraints of the views.
  5. Use Adjust view mode to select Contents, Wireframes or Wireframes and Contents. The default is Wireframes and Contents.
  6. You can change the background color of the Canvas that shows the views. Use the Change canvas background color to switch between either light or dark mode, depending on the Xcode theme.
  7. Orient to 3D presents the views in a 3D manner.
  8. Zooms out the views on canvas.
  9. Actual Size brings the view to the actual size of the device.
  10. To zoom in on the views, click Zoom In.
  11. You can Adjust the range of visible views by altering the slider. It’ll show you only the views you want to see at a given moment.
  12. The Object inspector window shows you the properties of a selected view.

Select the first Label in the Debug navigator. You’ll see its properties on the right in the Object inspector. Also, increase the spaces between the views using the slider. You’ll see:

Using the slider.
Using the slider.

Then, select any view in the canvas, and click and drag the view like this:

Applying click and drag to the canvas.
Applying click and drag to the canvas.

Now, click Orient to 3D. The button now appears as Orient to 2D. Your view orients itself in 2D.

Orientation mode.
Orientation mode.

To check the constraints, click Show Constraints. You’ll see:

Looking at the constraints.
Looking at the constraints.

Inspect everything closely to check your constraints. If you click the same button again, you’ll see your normal view.

Orientation mode.
Orientation mode.

Click Adjust view mode and you’ll see a drop-down:

Adjust view mode options.
Adjust view mode options.

Currently, the default option is Wireframes and Contents. Select Contents, and you’ll see the views without wireframes.

Seeing the content.
Seeing the content.

You can also play around with the range to focus only on the views of interest.

Using the range slider.
Using the range slider.

Move the range to its default position again. Now, click Show Clipped Content. You’ll see all the views that are going out of bounds.

Examining the clipped content.
Examining the clipped content.

Isn’t that great? You can have a deep look at how your views are rendered and identify user interfaces bugs, without using an external tool.

Memory graph

Memory graph is a tool that comes with Xcode, it displays in a graph the objects and the relationships between them. Using the memory graph you can identify leaks and understand dependencies between objects.

To see the memory graph of the objects, on the Debug bar, click Debug Memory Graph :

Object memory graph with references.
Object memory graph with references.

Note: Click Zoom in to see the names of the objects.

Check the Debug navigator on the left. You can view all the objects in memory. When there are memory issues or leaks, Xcode shows a purple triangle with an exclamation mark in front of the object.

Expand the graph by using the two-way arrow button shown on the object:

RequestManager object referencing to AccessTokenMananger.
RequestManager object referencing to AccessTokenMananger.

That button shows the objects referencing this RequestManager. Click it, and you’ll see:

Inspecting the expanded memory graph.
Inspecting the expanded memory graph.

These are all the objects pointing to RequestManager and their addresses in the memory. You can even expand these further. It also shows the amount of space it’s taking in the memory.

Also, note the two buttons you see here:

Buttons in memory graph.
Buttons in memory graph.

  1. Jump to the definition takes you to the code that defines the selected object.
  2. Print the description prints the object description in the console.

Click the AnimalsNearYouViewModel object. You’ll see another button enabled next to the print description object button. This button, Focus on this instance, helps further focus on the selected object.

Focusing on a selected object.
Focusing on a selected object.

Environment overrides

Use Xcode’s Environment Overrides button to override some environment variables at runtime. Click Environment Overrides, and you’ll see the following popup:

Environment override popup.
Environment override popup.

By default, all the variables are off. You can select the corresponding switch to enable Appearance, Text or Accessibility.

Play around with these variables to see how they affect the app.

Instruments

Instruments is one of the most essential tools Xcode provides. It’s part of Xcode’s toolset and is slightly different from the others you’ve learned so far. From Xcode, Instruments opens as an app on its own.

Instruments depends on giving results based on trace data, also referred to as trace. The tool collects traces from important parts of apps that are harder to debug, like the app’s internal infrastructure, processes and operating system. You can also save all the profiling traces and share them with your team or colleagues.

Open Instruments by selecting Xcode ▸ Open Developer Tool.

Instruments.
Instruments.

Select Instruments. A window appears:

Main window of the instruments tool.
Main window of the instruments tool.

Instruments bundles in several profiling templates. Each profile works according to your needs. Here is the complete list:

  • Blank
  • Activity Monitor
  • Allocations
  • Animation Hitches
  • App Launch
  • Core Data
  • CPU Counters
  • CPU Profiler
  • File Activity
  • Game Performance
  • Leaks
  • Logging
  • Metal System Trace
  • Network
  • SceneKit
  • SwiftUI
  • System Trace
  • Tailspin
  • Time Profiler
  • Zombies

Now that’s a nice set of tools! Here you have a wide set of templates, from Core Data that helps investigate core data related bugs, like faults or problems when saving records, to SwiftUI a tool you can use to identify issues like slow frames affecting your user experience. Also, more agnostic tools like Logging or Activity Monitor to monitor system-level processes. For now, you’ll focus on one of these tools, Leaks.

Leaks

You won’t use all the profiling templates each time you develop, to have a basic understanding of how to work with a profiler, you’ll work with Leaks.

Leaks measures memory usage in general and detects leaked memory. It also records all the allocations by a class and other references to memory addresses, including active allocations and leaked code blocks.

On the Instruments window, select Leaks and click Choose. You’ll see:

Demystifying the Leaks tool.
Demystifying the Leaks tool.

Here’s a breakdown of what’s on the screen:

  1. With Start an immediate mode recording, you can start recording the trace.
  2. Use this button to pause the started profiling.
  3. This button selects the simulator or the process.
  4. You can add more instruments with this button.
  5. This button helps you hide or show the detail area.
  6. Use this button to hide or show the inspector area.
  7. To focus on one instrument at a time. You can put an instrument filter.
  8. You can click duplicate to save the current traces data.
  9. This area shows the list of instruments. The leaks template comes with an Allocations instrument. Every newly added instrument gets added here.
  10. Displays the app’s graph for the instrument selected.
  11. You can view the objects in the memory and space they take.
  12. Shows you the stack trace.

At the top, select the simulator and PetSave, then click Start an immediate mode recording. It opens the app on the simulator, and you’ll see something like this:

Profiling the app.
Profiling the app.

Here, it:

  1. Shows the app’s memory allocation.
  2. Shows all the leaks. A green checkbox means no leaks. A red cross means new leaks. A gray dash means no new leaks.
  3. Displays all the objects causing a memory leak. Notice that Leaks is selected in Instruments, and the filter is set to Leaks just below.
  4. Shows the stack trace for the object selected in the left pane.

Double-click the red icon to display all the leaks in the section below.

Leaks data.
Leaks data.

The leaks you see here are system-generated.

Retain cycle

A retain cycle occurs when two objects hold references to each other. Both objects stay in memory and aren’t released. You can check for these in the Leaks instrument or debug memory graph.

To eliminate this problem, you make the dependent object weak. By default, these references are strong. There’s also another type of reference, unowned. The main difference between weak and unowned is that weak can be nil whereas unowned can’t be nil throughout its lifecycle.

Look at the example below:

// 1
class PetOwner {
  var name: String?
  var pet: Pet?
  deinit {
    print("Petowner removed!")
  }
}
// 2
class Pet {
  var name: String?
  var owner: PetOwner?
  deinit {
    print("Pet removed!")
  }
}
// 3
var pet: Pet? = Pet()
pet?.name = "Snowfy"
// 4
let petOwner = PetOwner()
petOwner.name = "Ray"
petOwner.pet = pet
pet?.owner = petOwner

Here’s a code breakdown:

  1. A PetOwner class contains the owner’s name and the Pet.
  2. The Pet class contains the pet’s name and owner.
  3. This is the Pet object.
  4. The PetOwner object references Pet. Pet references its owner object.

Now, imagine at some point the variable pet becomes nil. In this scenario, you may think the object will deallocate and deinit will be called. However, it won’t because both objects strongly reference each other. To free the memory, you can make one of them weak, like this:

class PetOwner {
  var name: String?
  weak var pet: Pet?
  deinit {
    print("Petowner removed!")
  }
}

Now the deinit gets called.

A closure may also create a retain cycle. For example, when referencing self in a closure, it’s best to mark it with a [weak self] to avoid retain cycles. Take a look at the code below:

DispatchQueue.main.asyncAfter(deadline: .now()) {[weak self] in
  self?.doSomeUIUpdates()
}

You can also use [unowned self] depending on your requirements.

Key points

  • Breakpoints help you debug code line by line.
  • Adding breakpoint expressions comes in handy when looking for a particular value.
  • Use Xcode’s Memory graph to find retain cycles and leaks in your code.
  • Call stack shows you all the methods in the memory stack. You can navigate to the initial method using the stack.
  • Use Instruments to profile your apps. Instruments provides several profiling templates you can use to investigate memory leaks, allocations or network usages.
  • Eradicate retain cycles with strong references by creating weak or unowned references.

Where to go from here?

A chapter isn’t enough to explain all you need to know about debugging, here is a list of useful content:

This brings you to the end of this chapter. You became familiar with debugging and learned to use breakpoints, visualize views in the view hierarchy and play with Instruments.

Your hard work paid off, and you’re almost at the end of this real-world journey. So far, you’ve done well, and there’s just one more milestone to complete. In the next and final chapter, you’ll learn about Deploying an app to the AppStore.

Excited to complete the last milestone?

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.