Debugging in iOS
Get the best out of the LLDB in Xcode
Debugging is one of the most essential processes for developers while developing any kind of program. It allows you to detect and remove existing and potential bugs. Thereby, iOS development is not an exception.
Like many of you, my first approach to the amazing world of debugging was using the very well-known print
statement. However, this way of debugging can be overwhelming, and your code can get cluttered with multiple lines of print
statements.
Breakpoints
But sometimes, when trying to debug something, we want to see what is going on in every step of our program. Of course, we can do it, and this is where breakpoints make their appearance. A breakpoint is a point in our program where the code will stop executing. In Xcode, we can use LLDB (low-level debugger), the default debugger for developers on Apple platforms, and make awesome things that we did not even imagine.
po Command
After we set a breakpoint and our program’s execution stops, we can use commands to get all the information we need. The most common command is probably po
, which is an alias for expression -- object-description —-
command. This command gives us the object description that we are inspecting. In the following example, we print a textual representation of an instance of our type, in this case of a Book
.
We have a default object description that the system runtime provides us, but we can create our own description using the protocol CustomDebugStringConvertible
and using the variable debugDescription
. Example:
And po
command can do even more than that. It can compute practically any expression that you would compile in your program. Look at the following example, where we are using the same book instance but this time, we calculate how many days passed since the book’s launch.
p Command
The second way that we have to print variables is with the command p
, which is the alias for expression --
. The information we get in result with this command is basically the same as po
but, as you can imagine, this time the object is printed without description. The main difference is that p
makes a process under the hood that po
does not. This process is called Dynamic Type Resolution, which I’ll explain with an example:
In Swift, the static representation of a type in the code can be different from the dynamic type at runtime. In this example, we can see that the static type of the instance maria is Human
, but at runtime, this variable will have an instance of type Student
. If we try to print the instance maria using p
, we get an instance of type Student
, because p
is computing the result’s dynamic type; it gives us a more accurate type for a given variable.
However, it won’t be possible to access the properties of the instance without making a cast.
v Command
There is a third way to print variables, which is using the v
command. The output of the command is the same as p
, but the main difference relies on that the v
command does not compile code, which makes it very fast. Taking the previous example, with v
, we don’t need to make a cast because it reads the values of an instance from memory and then uses the dynamic resolution multiple times to make a better interpretation.
So, in my opinion, I would use this commands in the following scenarios:
· po
if you want to compile your code and get the object description.
· p
if you only want to compile your code
· v
if you want to print variables in the current stack frame
Custom Breakpoints
Now it is time to talk about the power of breakpoints. Let’s see a more practical example with a simple app. This app calculates the total of a check adding the tip percentage, and then prints the result, but as you can see, there is a bug in it.
The result we expect is, of course $1100. Because we wanted to add a 10% tip to the check of $1000, but the total says $1000. Let’s use a breakpoint to see what is going on.
At this point you can see that we are printing the amount instead of the amount + tipValue
. But because we don’t want to make this change, compile, and then rerun the app to make sure we are correct, we can edit our breakpoint and make our change on the fly.
- We double click the breakpoint to edit it
- We add the following expression:
expression amount = amount + tipValue
- We click the option Automatically continue after evaluating actions. This triggers the expression and continues the execution of the program.
Now we tap the Calculate button and we see that the total amount is printed correctly.
First bug is fixed ✅
Modify Our View Using the Debugger
Now we have a second requirement. We wanted to move our Calculate button and total label (which are both in a stack view) 100 points down to the bottom.
Of course, now that we are debug-developers, we want to do this with the console as well. To do that, we need to have the memory address of the stack, we could use the code visual view debugger to get that address, but we can also use the console!
To get all the info related to the child views contained in our view controller’s view we will use this new command: expression -l objc -O -- [`self.view` recursiveDescription]
.
With the -l objc -O
we are telling the debugger that we are about to run Objective-C code.
With the result, we can easily find the address for the stack view.
The next thing we are going to do is to inspect the description of this stack view by using a function called unsafeBitCast
. Then, we can access to all the properties of our stack view.
Having all this freedom, we can simply update the frame of our stack. We are adding 100 points to the y
value of the frame.
Wait a minute, nothing happened 🙃. This is because after any UI changes on the debugger we need to manually trigger a render to update our view using the class method flush()
of the CATransaction
class.
We have now finished the second requirement ✅
As you saw, the LLDB is a very powerful tool that can help us as developers to boost our productivity and improve our times while debugging code, fixing issues or even just having fun discovering everything that we can do without recompiling and rerunning our app several times. 🤓
P.S. Don’t forget to add your bug fixes in the code. 😄