Act I — Setting the scene

Let’s say we are debugging an issue where the focused view has gone missing. In other words, we are looking for any UIView instance whose isFocused property is true . In this scenario, it’s helpful to us to set that view’s background color to a red so that we can easily see where it is on the screen. In the past, I have achieved this by creating a function to recurse through all views in the app, setting background colours where appropriate:

1. func highlightFocusedView(in target: UIView) {

2. target.subviews.forEach {

3. if $0.isFocused {

4. $0.backgroundColor = .red

5. } else {

6. highlightFocusedView(in: $0)

7. }

8. }

9. }

I would typically put the function at the top of my AppDelegate.swift and simply not commit it to source control. To run the function, I pause execution with ⌃⌘Y and type the following in the debugger:

(lldb) expression -l Swift -O -- highlightFocusedView(in: UIApplication.shared.keyWindow!) error: <EXPR>:3:1: error: use of unresolved identifier 'highlightFocusedView'

Uh oh, seems like I need to import my app’s bundle. Better do that first:

(lldb) expression -l Swift -O -- import MyApp_tvOS

And try again:

(lldb) expression -l Swift -O -- highlightFocusedView(in: UIApplication.shared.keyWindow!) error: <EXPR>:3:26: error: use of unresolved identifier 'UIApplication'

Uh oh again — need to import UIKit

(lldb) expression -l Swift -O -- import UIKit

And finally, running expression -l Swift -O -— highlightFocusedView(in: UIApplication.shared.keyWindow!) and then un-pausing the debugger results in a view on my device’s screen changing colour.

To summarise:

The helper function exists in my codebase and I need to remember not to commit it

I have multiple targets so my helper function needs to be copied and maintained between them

All my debugger commands need to be prefixed with expression -l Swift -O -- in order for lldb to interpret them as Swift code

in order for to interpret them as Swift code Before calling the helper function, I need to import both my app bundle and UIKit.

Act II — Defining lldb aliases

As with many good command-line tools, lldb has an init file. In it, we can define aliases (without Python, of course).

$ vi ~/.lldbinit # or use your second favourite editor

In here, we can make an alias swift that expands to expression -l Swift -O -- (which is, in fact, the same as po however we are going to improve it later). While we are here, let’s create one for good old Objective-C too, just in case. Add the following two lines to your .lldbinit file:

1. command alias objc expression -l objc -O --

2. command alias swift expression -l Swift -O --

Now the next time we launch our app and pause the debugger we can simply:

(lldb) swift import UIKit; import MyApp_tvOS

So, why not create an alias that defines our helper function? By converting our function into a single line (a caveat of this Python-less approach) and prefixing it with $ we can mark the function as belonging to lldb . Let’s also combine it with the two imports we require. Append the following line to your .lldbinit :

1. command alias import_helpers expression -l Swift -O -- import UIKit; import MyApp_tvOS; func $highlightFocusedView(in target: UIView) { target.subviews.forEach { if $0.isFocused { $0.backgroundColor = .red } else { $highlightFocusedView(in: $0) } } };

Now when we first pause our debugger we can run

(lldb) import_helpers

and from that point onwards we can swift $highlightFocusedView(in: UIApplication.shared.keyWindow!) to our heart’s delight! Of course, you can continue to add func $helpers(...) to that same alias — just ensure they are all on one line and separated by ;

Intermission — CATransaction.flush()

Did you know you can force your device’s screen to update after making changes in a paused debugger? After running any UI-mutating commands, calling swift CATransation.flush() at your lldb prompt should force a screen refresh! You could insert this inside of your helper functions or add func $flush() { CATransation.flush() } to your import_helpers alias so that you can swift $flush() as needed — no more unpausing the debugger required!

Using CATransation.flush() is a common technique for me, so I want to be able to call it in the easiest way possible (i.e. I don’t want to reach for the $() keys all the time). Using a regular expression, we can insert code around the variable input to our alias. Optionally, replace the command alias swift ... we made earlier with the following line:

1. command regex swift 's#(.+)#expression -l Swift -O -- defer { CATransaction.flush() }; %1#'

This is a sneaky way to run CATransaction.flush() after every command, leveraging defer . We could of course simply add ; CATransaction.flush() after the %1 , however that would mean that the debugger would be printing the returned value ( Void ) of said function for our inspection rather than the code we entered. Hopefully this work-around remains valid in the future!

Act III — Finale

Calling import_helpers at the start of every debug session is not difficult, however we should automate repetitive tasks as much as possible. Another issue is our import_helpers contains import MyApp_tvOS — what if we are working on other targets or projects? If one statement in the alias fails, all subsequent statements will be skipped. Instead, let’s use a breakpoint.

Xcode’s UI provides us a way to create a “symbolic breakpoint” that runs commands — however these little windows can be a bit finicky. So let’s append our .lldbinit again:

1. breakpoint set -n AppDelegate.application --one-shot true --auto-continue true

2. breakpoint command add

3. swift import UIKit

4. swift import MyApp_tvOS

5. swift func $highlightFocusedView(in target: UIView) { target.subviews.forEach { if $0.isFocused { $0.backgroundColor = .red } else { $highlightFocusedView(in: $0) } } } ;

6. DONE

How does it work?

-n AppDelegate.application makes the breakpoint stop on any functions that match that name. In my case, this resolves to six separate functions. Ideally we would be able to use AppDelegate.application(_:didFinishLaunchingWithOption:) as the value of this parameter, however I was unable to to get it (or its variants) to work. Basically, for this parameter we want to choose a function that is called as close to the beginning of the app’s lifecycle as possible so that our helpers get created early on.

makes the breakpoint stop on any functions that match that name. In my case, this resolves to six separate functions. Ideally we would be able to use as the value of this parameter, however I was unable to to get it (or its variants) to work. Basically, for this parameter we want to choose a function that is called as close to the beginning of the app’s lifecycle as possible so that our helpers get created early on. --one-shot true means the breakpoint should be deleted after the first time it is encountered (working around the previous point’s issue).

means the breakpoint should be deleted after the first time it is encountered (working around the previous point’s issue). --auto-continue true means that we don’t actually want to pause execution upon reaching this breakpoint: just evaluate it’s commands and continue.

means that we don’t actually want to pause execution upon reaching this breakpoint: just evaluate it’s commands and continue. breakpoint command add means that each following line should append the previously created breakpoint’s list of commands that must be executed when the breakpoint is reached.

means that each following line should append the previously created breakpoint’s list of commands that must be executed when the breakpoint is reached. DONE marks the end of the list of commands expected by breakpoint command add

We now have the ability to create an infinitely expandable (and reasonably maintainable) list of Swift-language helper functions that are available for use in the debugger!

Epilogue — My helper functions

$findViews(_:in:)

I don’t actually have a $highlightFocusedView(...) helper as it’s easy enough to have a generic function that finds all views of a certain class. For example, the following finds all the UIButtons on the screen and changes their background color to red:

(lldb) swift $findViews(UIButton.self).filter({ $0.isFocused }).forEach({ $0.backgroundColor = .red })

Another common use for me is changing a label’s text:

(lldb) swift $findViews(UILabel.self).first(where: { $0.text == "Interstellar" })!.text = "This is a really long string to test how the layout reacts to it"

$printSubviews(of:)

This helper simply sends the private message recursiveDescription to a view so that it prints it’s heirachy:

(lldb) swift $printSubviews(of: self.view.headerContainer)

NSObject.$from(_:)

My favourite: this extension helps get a concrete type from an address that you might find in Xcode’s View Debugger (or when printing an array, perhaps from the output of swift $findViews(UIView.self) ?). For example, if I see a view in the View Debugger and I wanted to hide it I can copy it’s address from the Object Inspector and run:

(lldb) swift UIView.$from(0x7f921c5144a0).isHidden = true

The possibilities are endless!