Debugging Applications With Xcode
Where to Start
Everyone makes mistakes and developers are no different. As a developer, you spend a significant portion of your time debugging the code you write. It's an inextricable aspect of software development. Some bugs are easy to find while others can make you scratch your head.
A proficient developer needs to know how to efficiently debug the problems they face along the way. Becoming familiar with Xcode's debugging tools and workflows is an aspect that's very often overlooked because you already know how to use Xcode. Right?
Xcode ships with a range of debugging tools that can make your life easier and less frustrating. And believe me when I say that debugging can be very frustrating. Knowing where to start and which tools to use can make a world of difference.
In this series, we explore the debugging tools Xcode ships with. You learn how to read stack traces, use breakpoints, and interact with the LLDB debugger that powers the Xcode debugger under the hood.
Where to Start
Xcode's debugging tools can be a bit overwhelming if you're new to them. You can use breakpoints, inspect the output in the console, or debug the view hierarchy of your application. But it goes without saying that not every debugging tool is a good fit for every scenario. The view debugger won't be very useful if you're tracking down a memory leak.
That brings us to debugging workflows. It's important to emphasize that there isn't one workflow that works for every problem you face. That can be frustrating to hear if you're new to software development. When you're starting out, you prefer to use a proven recipe that works. Welcome to software development.
I'd like to start by outlining the workflow I use whenever I encounter an issue or receive a bug report from a user. If you're new to Swift development, then this workflow is a good starting point. If you have several years of experience, then it may give you some ideas to improve or tweak your own workflow.
Define the Problem
The framework I use time and time again is simple and straightforward. Whenever I run into a bug or a report hits my inbox, I start by defining the problem. That should always be the first step. Or to quote Sun Tzu, "Know your enemy."
You need to collect as much information as possible, such as device information, operating system, application build, and, most importantly, steps to reproduce. If you're not able to reproduce the issue, then everything that follows becomes more difficult.
As a developer, you want to fix your mistake as quickly as possible. While that's understandable, it's important to take your time. Don't move too fast. You need to understand how the problem is triggered. The patch you implement should squash the bug, not one of its manifestations.
That's why it's essential that you understand the problem and its root cause. The goal is to squash or eliminate the bug, not patch it with a band-aid.
Pinpoint the Problem
As you collect information, it starts to becomes clear what type of issue you're dealing with. Is the problem related to the user interface or is the user missing data due to a problem with the backend?
Pinpointing the problem should be your priority at this point. You need to make sure the issue is caused by a bug in the project. Some problems are caused by usability issues.
One of my own applications uses gestures to make several actions easier and faster. Those gestures were enabled by default in earlier versions of the application. Several users reported what seemed like a bug. The root cause was a usability issue I needed to fix. These users triggered one of the gestures by accident and were confused by what seemed unexpected behavior.
For applications that communicate with a remote server, it isn't uncommon that backend problems manifest themselves on the client, that is, your application. Make sure you're not spending hours or days debugging an issue that isn't yours to fix.
Before you write a single line of code that could fix the bug, you need to know the root cause of the problem. This isn't always possible, but that should be your goal. If your car breaks down, replacing the engine isn't your first option. Is it? The solution might be trivial, and it very often is, if you can put a finger on the problem's underlying cause.
Choose Your Tools
The more information you have about the problem, the easier it is to choose the right tools for the job. If I can reproduce the issue, I usually start with a few print statements to better understand the problem. Once I know what type of issue I'm dealing with, I can bring in more specialized tools that help me zoom in on the root cause.
Implement a Solution
Once you have a clear picture of the issue, it's time to come up a solution. This can mean fixing a typo, but it might also mean hours or days of work.
You don't always have the time to implement a proper solution. That's fine as long as you make sure you implement a more robust solution in a future release. Create an issue to make sure the problem stays on your radar. A proper bug or issue tracker is indispensable in software development.
Test the Solution
Most developers test the solution they implemented. What's often overlooked is adding one or more automated tests that prevent the problem from happening again in the future. This idea isn't revolutionary, though. A bug that makes its way into the project reveals a weakness of the project's test suite.
By writing one or more tests that reproduce the problem, you feel more confident that the solution you implemented is solid and you don't have to worry about it again. If the problem does pop up again, the project's test suite automatically warns you about it.
I'd like to take it one step further and suggest to write a test case before implementing your solution. If you're able to reproduce the issue, you should have the information you need to write a failing test case. This idea isn't revolutionary either. This strategy is at the heart of TDD or test-driven development.
Reproducibility Is Key
Fixing a bug without the ability to reproduce the issue is like finding a needle in a haystack. Every developer has fixed, or tried to fix, bugs that weren't reproducible. This is a frustrating and expensive process for everyone involved, the project manager, the client, and you.
Experienced developers often argue that you shouldn't write a line of code until you've been able to reproduce the bug and that makes a lot of sense. Whenever you're fixing an issue, you want the patch to be as small and lightweight as possible. That is only possible if you know exactly what problem you're trying to fix.
Implementing a solution to fix a random bug results in obscure commits and unnecessary code changes. Avoid it whenever possible.
Don't Feel Discouraged
Debugging can be very, very frustrating. As a developer, you sometimes spend hours or days finding the root cause of a nasty bug. It's important to go easy on yourself. If you're not able to find the root cause of the bug, then that doesn't mean you're not a good developer. Cut yourself some slack.
If you're lucky and you're surrounded by a team of developers, then don't hesitate to ask for help. A fresh pair of eyes can often make a difference.
Xcode's debugging tools are powerful, but it takes time to master them. I discover new tips and tricks on a regular basis. It's important that you focus on the fundamentals first and step up your game as you need more power and gain experience. In the next episode, we take a look at the debugging tools Xcode ships with.