Recommended Reading
9 Minutes
Debugging in Xcode: Tools, Techniques, and Workflow
Fix Bugs Faster! Log Collection Made Easy
Xcode debugging tools integrate smoothly with the rest of the Xcode ecosystem and offer myriad benefits to developers, including powerful breakpoints and source-level visibility. This guide will help you unlock them.
We’ll equip you with the knowledge to:
- Start a debugging session in Xcode.
- Read frames, threads, variables, watches, and the console.
- Use breakpoints and step controls without guessing.
- Inspect runtime values and testing fixes without recompiling.
- Diagnose performance issues with the Instruments profiling tool.
- Understand where Xcode stops being enough in production
If you’ve come for a specific piece of knowledge, here’s the full list of contents so you can go straight there.
What Xcode debugging is
At its simplest level, Xcode debugging is how we find and fix bugs in Apple platform apps by inspecting how the app behaves while it runs.
In reality, the topic covers a range of performance issues including crashes, broken logic, layout issues, runtime values, memory behavior, and performance problems across iOS, macOS, watchOS, and tvOS apps.
An Xcode debugging regime usually borrows from all the following tools and tips:
- Breakpoints to pause execution.
- Variables to inspect runtime state.
- Console output to read logs and run LLDB commands.
- View Debugger to inspect UI hierarchy.
- Instruments to profile memory and performance.
This works well during development. For production bugs, we usually need crash reports, remote logs, and real device context too (don’t worry, we’ll get into that).
How to start the Xcode debugger
This bit’s super-simple. Run the app with the scheme set to Debug, and Xcode will build, install, and connect the debugger automatically for you.
- Open the project. Launch Xcode and open the iOS app you want to debug.
- Choose a device. Use the scheme toolbar at the top to select a connected iPhone or simulator.
- Open the relevant file. Navigate to the ViewController, View, ViewModel, or function where the bug is likely happening.
- Set a breakpoint. Click the empty gutter to the left of the line number. A blue marker confirms it’s active.
- Start the debug session. Click the Run button or press Cmd + R with the scheme set to Debug.
- Trigger the bug. Interact with the app until execution reaches the breakpoint.
- Open the debug area. Xcode pauses the app and the Debug Area appears at the bottom of the IDE.
- Inspect paused state. Check Frames, Threads, Variables, Watches, and the Console to understand what happened.
Xcode debugger interface overview
Once the debugger has attached, Xcode will show the debugger interface at the bottom of the IDE.
If you’re coming from Visual Studio, IntelliJ IDEA or other IDEs, the Xcode debugging interface may feel a little different. Everything’s tightly integrated, and the 3D view is particularly striking (and useful).
Here’s a quick tour of the interface.
| Debugger area | What it shows |
|---|---|
| Debug navigator | Active threads, call stacks, and runtime resource information such as CPU and memory usage. |
| Variables view | Values available in the current scope, including local variables and object properties. |
| Console | print() output, system logs, errors, and LLDB commands. |
| Debug toolbar | Continue, step over, step into, step out, and other execution controls. |
| View debugger | A visual hierarchy of the app’s UI when layout issues need inspection. |
A basic Xcode debugger workflow
The core workflow for debugging in Xcode is pretty similar to other debuggers and IDEs. And it’s really simple:
- Pause execution.
- Inspect variables.
- Read the call stack.
- Run a command in the console.
- Continue or step through code.
If you’re looking for a bug in your app, this workflow will reveal it 99% of the time.
Using breakpoints in Xcode
Breakpoints tell Xcode where to stop, so we can examine the app at the exact moment something goes wrong. This is really useful when we need to check:
- Which code path is running.
- Which values are available.
- Which function called the current line.
- Whether a condition is true at runtime.
To add a breakpoint, simply click the gutter beside a Swift or Objective-C line. Xcode marks active breakpoints in blue, and disables them when we click the marker again.
If you want a more advanced option, Xcode also supports conditional breakpoints, symbolic breakpoints, and exception breakpoints. To explore these options in depth, see our dedicated Xcode breakpoints article.
Inspecting variables, watches, and expressions
Once you’ve paused your app, you can inspect runtime values using the Variables view. This shows values currently available in scope, including
- Objects.
- Arrays.
- Dictionaries.
- Optionals.
- Arguments.
- Properties.
Watches are the specific variables or expressions that you’ve flagged as important. You add these manually, and they’re particularly useful when a value is buried inside a larger object or changes across several steps.
Evaluating expressions with LLDB
The LLDB console lets us print values, inspect threads, check backtraces, and test small runtime changes manually.
| LLDB command | What it does |
|---|---|
po object | Prints an object description. |
p value | Prints a primitive value. |
expr value = true | Evaluates an expression or changes a value. |
bt | Prints the current thread backtrace. |
thread list | Shows active threads and their state. |
The expr command is particularly important when a condition looks wrong. We can test a new value live, confirm the behavior and then make the real fix in code.
Reading logs in the Xcode console
The Xcode console will display messages from your app and the debugger while your app is running or being debugged.
This shows us the app output, runtime warnings, errors and system messages without needing to keep pausing our app. It’s particularly useful for lifecycle bugs, layout warnings, network failures, and asynchronous behavior where pausing execution may change the timing.
To open the console, click View > Debug Area > Activate Console, or press Command + Shift + Y. Here’s a simple workflow:
- Reproduce the issue. Trigger the screen, action, or flow that behaves incorrectly.
- Search the output. Look for error text, function names, custom log messages, or warning categories.
- Check runtime warnings. Watch for Auto Layout issues, missing assets, permissions, or lifecycle warnings.
- Compare before and after. Add structured logs around the suspected code path if needed.
- Confirm with breakpoints. Use logs for sequence, then breakpoints for exact runtime state.
Debugging UI issues with the View debugger
Xcode’s View debugger helps us inspect the live UI hierarchy when a screen looks wrong but the code seems correct. Specifically, it allows us to check:
- Frames, bounds, and constraints.
- Hidden or transparent views that block interaction.
- Overlapping elements and z-order issues.
- Auto Layout conflicts.
- SwiftUI and UIKit layout structure.
To open the View debugger, run the app, reproduce the UI issue, then click Debug View Hierarchy in the debug toolbar.
Use it when a button disappears, a view overlaps another, a constraint behaves unexpectedly, or a layout only breaks on certain devices.
Debugging performance with Instruments
Instruments is Xcode’s profiling suite, and it helps us debug performance problems that don’t show up clearly in breakpoints or console logs.
Here’s a more granular look at the functionality we can look at:
| Instruments template | When to use it |
|---|---|
| Time Profiler | Screens load slowly or scrolling stutters. Record the flow and inspect which functions consume the most CPU time. |
| Allocations | Memory keeps growing. Track object creation and check which objects remain allocated longer than expected. |
| Leaks | Memory is not being released. Record the session and look for abandoned allocations or retain cycles. |
| Core Animation | Animations drop frames. Inspect rendering performance, layout cost, and GPU-related delays. |
| Network | Slow requests affect the experience. Inspect request timing and data transfer behavior. |
To open it, go to Product > Profile, or press Command + I, then choose a template based on the issue.
Remember though: For final performance measurements, always run Instruments on a real device. The simulator does not match real CPU, memory, GPU, battery, or thermal behavior, so performance data can be unreliable for shipping decisions.
Xcode debugging best practices
As we’ve said time and again in our debugging guides, consistent habits will bring greater efficiency gains than any single tool. These habits are particularly worth holding on to:
- Use debug builds for debugging. Debug builds keep symbols and allow better inspection, while release builds may optimize code in ways that change the debugging experience.
- Set exception breakpoints by default. Add an All Exceptions breakpoint so crashes pause closer to the source.
- Use symbolic breakpoints for UIKit issues.
UIViewAlertForUnsatisfiedConstraintshelps catch Auto Layout conflicts when they happen. - Prefer LLDB over temporary prints. Console commands avoid unnecessary edits, rebuilds, and forgotten debug code.
- Profile before optimizing. Use Instruments before changing performance-critical code.
- Test on real devices – hardware behavior, memory pressure, permissions, and thermal throttling can differ from the simulator.
Debugging iOS crashes in production
Xcode debugging works well while the app is running locally. But once the app reaches TestFlight, App Store Connect or your enterprise users, direct debugger access is gone.
Production crash reports come from Xcode Organizer or App Store Connect and include the stack trace, app version, device model, iOS version, and crash frequency.
Here’s the key point though. A stack trace shows where the app failed. It rarely shows the user action, network response, navigation path, or device condition that caused it.
For deeper advice on reading and resolving production crashes, see our iOS crash debugging guide.
Finding iOS issues with remote logging
If your users are reporting issues with your app, remote logging enables you to inspect their logs and runtime information without requiring physical access, or relying on emulators. You can also monitor the app’s performance continuously, even when there’s no flaw to fix.
Our own product, Bugfender, is specifically designed for this niche. Bugfender captures logs from real user sessions, so you can monitor your app on every device in every country.
We like to think we’re pretty fair when we talk about Bugfender, but you don’t have to take our word for it. Try Bugfender for free and start collecting logs in under five minutes.
Frequently asked questions about Xcode debugging
Can Xcode debug an app already running?
Yes. Xcode can attach to an already running app from Debug > Attach to Process. This is useful when the app was launched manually, triggered by a system event, or started before the debugger was ready.
Why does Xcode skip my breakpoint?
Xcode usually skips a breakpoint when the running build does not match the source code, the line is not executed in the current code path, or the compiler optimized the code away. Clean the build folder, rebuild the app, confirm the correct scheme is selected, and test the exact flow again.
Can Xcode debug SwiftUI apps?
Yes. Xcode can debug SwiftUI apps with breakpoints, variables, the console, and the view debugger. SwiftUI bugs can be harder to follow because state changes may trigger view redraws indirectly, so logs and breakpoints around state updates are often the fastest entry point.
Can Xcode debug Kotlin Multiplatform or cross-platform code?
Xcode’s debugger handles native Swift and Objective-C code directly. For Kotlin Multiplatform shared modules, debugging the iOS layer still works through Xcode, but stepping into shared Kotlin code requires a separate setup. For React Native or Flutter, each framework has its own debugging toolchain that runs alongside or separately from Xcode.
What is the difference between the Allocations and Leaks templates in Instruments?
Allocations tracks every memory allocation over the session, which helps identify operations that create excessive objects even when nothing is leaking. Leaks specifically detects memory that was allocated, is no longer referenced, but has not been released, which usually points to a retain cycle or a missing deallocation. Use Allocations to understand memory growth patterns, and Leaks to confirm a specific cycle exists.
Expect The Unexpected!
Debug Faster With Bugfender