In the previous video, you learned what a breakpoint is and what types of breakpoints Xcode supports. In this video, we step through code using a breakpoint and the debug bar we explored earlier in this course.
Stepping through Code Execution
If we run the app and the breakpoint in LocationsViewModel.swift is hit, the line of the breakpoint turns green. It indicates the location of the instruction pointer. We can inspect the stack trace, the variables view, and the output in Xcode's console. This is helpful if you are debugging an issue in your project.
But it is often useful to understand what happens next. The step controls of the debug bar allow us to do that. Earlier in this course, you learned that we can continue execution by clicking the Continue/Pause button of the debug bar. That isn't what we want, though. I would like to zoom in on the step controls on the right of the Continue/Pause button.
- Step Over
- Step Into
- Step Out
Step Over
Stepping through code is a skill that is very useful to have. While it isn't as difficult as you might think, it is something that confuses many, many developers.
The app is currently paused in the start()
method of the LocationsViewModel
class. If we click the Step Over button, the button on the right of the Continue/Pause button, the execution of the app's process continues as normal and the debugger pauses execution at the next line of code. Let's try it out. Click the Step Over button and see what happens.
The debugger pauses the execution of the app on line 57. If we click the Step Over button again, the next executable line of code is executed, that is, line 65. Notice that it skips the closure that is passed to the map
operator. That closure is executed when the publisher emits a value. The next executable line of code is line 65. If we click Step Over a few more times, Xcode takes us to the closure that is passed to the onAppear
modifier of the LocationsView
struct. In this closure, the view model's start()
method is invoked.
Even though the name Step Over may sound odd or confusing, it simply means we advance to the next line of code as if the app is running under normal conditions.
Step Into
To illustrate how the Step Into button works, we need to set a breakpoint at a different location. We first disable the breakpoint on line 56 in LocationsViewModel.swift by clicking the breakpoint in the gutter once. The breakpoint turns gray, indicating that it is no longer enabled.
Open AddLocationViewModel.swift and add a breakpoint on line 70. On that line, the view model invokes the addLocation(_:)
method on its store
property. Run the app, tap the Add Location button, enter the name of a town or city, and tap the + button. The debugger pauses execution on line 70 of AddLocationViewModel.swift.
We can continue to the next line by clicking the Step Over button. That isn't what we want this time, though. Click the Step Into button instead, the button on the right of the Step Over button.
As the name suggests, the Step Into button follows the execution of the app's process into the addLocation(_:)
method. The debugger breaks the app's process on line 30 of UserDefaults+Helpers.swift, the line on which the addLocation(_:)
method is declared.
If there is no method to step into, then the debugger advances to the next executable line of code and the result is identical to clicking the Step Over button.
Step Out
Now that you know how stepping into a method or function works, it is easy to understand what it means to step out of a method or function. Click the step over button to continue the execution of the addLocation(_:)
method. We covered that before.
Click the Step Out button and see what happens. The debugger steps out of the addLocation(_:)
method, jumping back to location where the addLocation(_:)
method is invoked.
Knowing how to step through your code is important if you are working with breakpoints. The best way to learn the difference between step over, step into, and step out is by playing with breakpoints. After a while, it becomes second nature to step through your code.
Asynchronous Code
Remove the breakpoint on line 70 of AddLocationViewModel.swift by dragging it out of the gutter. That is a quick way to remove a breakpoint. Add a breakpoint on line 79 of the setupBindings()
method of the AddLocationViewModel
class.
Run the app and tap the Add Location button. The debugger pauses execution of the app's process on line 79. That is expected. Click the step over button a few times. Notice that the debugger skips line 83, the line on which the view model invokes the geocodeAddressString(_:)
method. Why is that?
You may have expected the debugger to enter the closure that is passed to the sink(receiveValue:)
method. Why does the debugger skip line 83?
The closure is executed every time the publisher the filter
operator returns emits an event. A publisher emits its events asynchronously and that is why the debugger doesn't enter the closure that is passed to the sink(receiveValue:)
method.
The debugger follows the flow of your app. Let's add a breakpoint to line 83 of AddLocationViewModel.swift, the line on which the view model invokes the geocodeAddressString(_:)
method.
Run the app and tap the Add Location button. The debugger breaks the execution of Thunderstorm on line 79 of AddLocationViewModel.swift. Thunderstorm's process continues when we click the Continue button.
Enter the name of a town or city in the text field. The debugger breaks the execution of Thunderstorm on line 83. Open the Debug Navigator and inspect the stack trace of the main thread. It shows us that the closure to which we added the breakpoint is invoked the moment the publisher the filter
operator returns emits an event.
Open the debugger at the bottom and inspect the variables view. The value of the addressString
constant is P
, the character I entered into the text field.
Debug Symbols
While I don't want to overcomplicate this video with technical details, it is important to understand that you can only debug code for which you have the debug symbols. The code you write is translated to assembly and the debugger can only translate assembly to Swift if it has the debug symbols for the code.
Let me illustrate this with an example. Open the Breakpoint Navigator on the left, right-click Thunderstorm, and choose Delete Breakpoints.
Open LocationsView.swift and add a breakpoint to the closure we pass to the onAppear
modifier on line 52. Run the app. The debugger pauses the app on line 52 of LocationsView.swift.
What happens if we decide to step out of the closure we pass to the onAppear
modifier of the LocationsView
struct? Click the Step Out button to find out.
By stepping out of the closure we pass to the onAppear
modifier of the LocationsView
struct, the debugger takes us to the scope in which the closure we pass to the onAppear
modifier is invoked. What we are seeing isn't very useful, though. The debugger shows us assembly.
The compiler translates the code you write to the assembly language. The assembly language is interpreted by the hardware of the device your app runs on. Because most of us can't read assembly, the debugger kindly translates assembly to Swift or Objective-C. That makes debugging Thunderstorm much easier.
The debugger can only translate assembly to Swift or Objective-C if it has the debug symbols for the code. Debug symbols are like a dictionary to translate assembly to Swift or Objective-C. While this is a simplified explanation of what happens under the hood, it should give you an idea why debug symbols are essential for debugging. When you build your app in development, the compiler automatically creates debug symbols for the build it creates.
This isn't true for Apple's frameworks or a third-party framework you are using in a project. If the debugger doesn't have the debug symbols for the code you are debugging, then it has no other option but to show you what you are seeing here, assembly. You can see some hints in the assembly code, but it is usually not helpful for debugging your app.
That is also why you always need to safely store the debug symbols of any build you ship to the App Store. Without debug symbols, you cannot symbolicate the crash reports you collect.
What's Next?
In this and the previous video, we used breakpoints to pause or suspend the app's process. Breakpoints have several other capabilities. We explore some of them in the next video.