A few useful Xcode debugging tricks

Photo by Kirk Morales / Unsplash

The previous post was about a few powerful and useful lldb commands we can use to help our debugging process. This article is about how Xcode can help us in debugging process. Last time I mentioned that we can work with lldb commands in a more convenient way. This post, beyond any doubt, will prove my point.

⚠️ If you missed the previous post A few useful lldb tricks I encourage you to check it out before going further. It explains in detail lldb commands I will use in this post.

I will use the same code sample as the last time:

final class PhotosViewController: UIViewController, Embedding {
    private var data: String?
    private var isShowingLoading = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if isShowingLoading {
            embed(LoadingViewController())
        }
        
        // Simulate loading data delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self?.removeAllEmbedded()
            if self?.data != nil {
                self?.embed(PhotoListViewController())
            } else {
                self?.embed(ErrorViewController())
            }
        }
    }
}

First I want to print the value of isShowingLoading feature flag and data:

(lldb) e print("IsShowingLoading: \(isShowingLoading) IsDataAvailable: \(data != nil)")

IsShowingLoading: false IsDataAvailable: false

It's useful but to make it happen constantly we would have to paste the same command over and over again. I would prefer to add regular print in code. But as promised, we can do better, without printing in code.

Let's place a breakpoint somewhere in the viewDidLoad() function i.e. in super.viewDidLoad() line. Instead of running the application, we double-tap on the breakpoint first:

There is a bunch of interesting things here. We can set a condition and the breakpoint will trigger when it's met or set how many times the breakpoint will be ignored before stopping the execution. But what we are looking for is Add Action button. When we tap it:

The default Debugger Command is the one we want but I encourage you to check the other options. We paste our printing expression in the text field:

Now when we open PhotosViewController and the breakpoint is triggered we see this in the console:

IsShowingLoading: false IsDataAvailable: false

Without any code changes. The command can be changed on the fly and next time breakpoint is triggered it will execute a new command. The only thing is that this is annoying. We want to have values printed but stopping at this breakpoint is a waste of time. Not for long. When we check Automatically continue after evaluating actions this breakpoint will perform any commands we add to it but won't stop the execution:

Let's think about the scenario I described in the last post. We need to create a new PhotoListViewController and want to be able to validate it as we go and compare it with the old one. We don't want to make any permanent code changes because it's purely experimental at this point and we want to do a quick proof of concept. Last time we were able to do achieve this but not in a convenient way.

We need to simulate we have the data first. Which means to alter the data value using an expression. But this time we add:

e self?.data = "some_data"

As a breakpoint action:

With this breakpoint in place, the PhotoListViewController will be shown. If we decide to disable it we will see an error screen instead.

Not bad for one breakpoint with the lldb command.

First part is done. We are in the flow of showing PhotoListViewController now. Next, we want to embed our NewPhotoListViewController to make it visible:

This should do it. But... it didn't. We see PhotoListViewController and not our experimental screen. Previously I said it's because the old screen is embedded over the new one. It's all true. But how did I know that? This is where another great feature of Xcode comes in - Debugging View Hierarchy

We did this fancy breakpoint with a command but sadly it didn't work for us. We assumed that the old screen is covering the new one but we need to verify this assumption first. Tap on the icon in the debug area:

Then wait for a moment. It will be worth it. This is how the view hierarchy debugging works in Xcode. The views are rendered in a 3D space. We can move them around, rotate, zoom in and out and decide which views should be visible and which shouldn't:

That confirms it. We embedded our new view controller but the old one was embedded on top. That's why we didn't see it. To fix this issue we will skip the line which does the embedding of the old screen:

thread jump --by 1

We need to add the second action to our breakpoint. Open the embedding breakpoint and tap on the plus button:

And add the second command:

This time it worked perfectly. The screen we were working on was the only one embedded and, finally, we can see it in action.

What is even better the commands are executed when the breakpoint is active. This means when we deactivate it the code works as before.

A small reminder about how awesome this is - all this can be done without recompiling the application. We can switch back end forth between screens without any delay. All we need to do is to activate or deactivate a breakpoint.

We managed to test our proof of concept code without any modifications in the existing flows. But what happens if we want to show our work to a colleague? Or we added clever breakpoints that might help other developers?

We can share our awesome breakpoints with others. Open Breakpoint Navigator in Project Navigator section (or press cmd+8):

This is a list of all breakpoints in the application. But it's difficult to know which is which since they all are in the viewDidLoad() function. Let's tap on a breakpoint and add the name parameter and do the same for the other one:

Much better:

When we know which breakpoint is which and even more important, anyone else will be able to understand what these breakpoints are for. We can share them. Open the options window for the breakpoint (two-finger tap on a breakpoint) and select Share Breakpoint.

When we commit the changes to the repository everyone will see these breakpoints in Xcode.

Thank you for reading!

P.S The example application is super simple and the view hierarchy debugger may not look that amazing. This is how it looks in more regular UI:

Kamil Tustanowski

Kamil Tustanowski

I'm an iOS developer dinosaur who remembers times when Objective-C was "the only way", we did memory management by hand and whole iPhones were smaller than screens in current models.
Poland