A few useful lldb tricks

Photo by Jeppe Hove Jensen / Unsplash

LLDB stands for low-level debugger and is the default debugger in Xcode. You can find more information here if you like. If you don't know what I'm talking about. Don't worry. It's easy to miss in the beginning.

Time to meet lldb:

  • Add a breakpoint in your code.
  • Run the application.
  • Wait until Xcode reaches your breakpoint and pauses the execution.

At this point, in Xcode debugging console, you should see green text:

(lldb) 

This is where the magic happens.

Consider the following code. Slightly modified example from Responsible code sharing using the power of protocol extensions :

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())
            }
        }
    }

Let's add a breakpoint at the line super.viewDidLoad() and run the app. When (lldb) appears again type po isShowingLoading:

(lldb) po isShowingLoading
false

PO stands for the print object and it does what the name implies. IsShowingLoading is false. This isn't unexpected.

Later you can check p and v instead po and see what happens.

Treat IsShowingLoading as a feature toggle. Imagine we are working on the Photos screen on loading spinner part and someone else is working on data loading. The loading spinner is hidden behind a feature toggle therefore we can merge the code as we go instead of having it on a separate branch for the whole development process. Noting bad will happen because until it's ready the isShowingLoading will be set to false and no one besides us will see it. The problem is that we won't see it either. We could hardcode somewhere isShowingLoading = true but what will happen if we forgot about it and the work-in-progress loading screen will land on production? Nothing good. The alternative is to use another command lldb provides us ‚ÄĒ e or expression. We set a breakpoint before the value of isShowingLoading is checked and type this into the console when the breakpoint is hit:

(lldb) e isShowingLoading = true

Then we use po again to make sure it worked:

(lldb) po isShowingLoading
true

We changed the value of a variable in a debugger console. While the app was running! Without rebuilding the application!

Expression can do much more but even this is impressive.

We can show or hide LoadingViewController but the problem is that it gets covered by the content pretty fast and it's hard to verify whether the spinner animation is working properly. Fortunately, there is a way around it. We add a breakpoint after the code which is embedding the LoadingViewController and when it's hit we type:

(lldb) thread return

The thread return makes the current function return. It won't execute more code. It won't cover our LoadingViewController with anything. Again, we can do it without recompiling!

Let's go even further. Users complain about the current PhotoListViewController and we are assigned to prepare a new, better, version of this screen. We are working on NewPhotoListViewController and would like to take it to the test run. It's not done yet. There is no code to surface it to the user anywhere. We would like to compare the current screen look & feel with the new one. We will add a feature toggle in a moment.

The problem is we are currently embedding our regular screen and we have no code to do the same for a new one. We additionally want to be able to switch between both of the screens to "feel" them. Let's add a breakpoint in the line where we embed PhotoListViewController` and when the breakpoint is activated we type:

(lldb) e self?.embed(NewPhotoListViewController())

Nothing happened. At least this is our first thought. The truth is we managed to embed our new view controller but immediately the old one was embedded on top of it. We have them both embedded now. Let's try something new:

(lldb) e self?.embed(NewPhotoListViewController())
(lldb) thread jump --by 1

The first command embeds a new screen. The thread jump --by 1 skips one line (nothing stops you to do i.e. --by 5) and ensures that code embedding the old screen won't be executed. This doesn't interrupt the rest of the function.

LLDB is great for printing:

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

IsShowingLoading: true IsDataAvailable: true

Making temporary comments is one of my favorite ways of using lldb.

I hope I managed to convince you that lldb is great. I know that what I presented may look cumbersome. You need to remember what command to use when the breakpoint is activated, you have to do it every time you run the code. It's hard.

Fortunately, We can do a lot better. If you want to know how to apply the magic of the lldb you learned today in a much more convenient fashion check my next post A few useful Xcode debugging tricks.

Thank you for reading!

This article was featured in SwiftLee Weekly 75 ūüéČ

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