I do a lot of Apple platform development (iOS & macOS primarily) in Swift. For a long time now (at least since the introduction of async/await, but I think longer), debugging Swift code has been pretty bad. It was never great, even in other languages, but it has definitely gotten worse in the past couple of years. And I’m not the only one experiencing this. A few people chimed in on a recent post over on the Swift forums. I’ve reported these issues to Apple via Radar, but generally gotten “can’t reproduce” responses.
There are two major issues:
- Local variables are often not available for inspection
- Single-stepping through async code rarely works.
In general, the behaviors are as if the code were heavily optimized, but all of this is built with -Onone
(debug builds in Xcode, verified by looking at the build transcripts). All of these issues persist in the latest Xcode 14.2.
Local Variables
It’s frequently the case that some or all local variable values are not available to the debugger. The Xcode debugger pane will show all of the variables in the function, but it won’t display their values. So it knows they exist, but thinks they’re gone. Depending on how one attempts to inspect, either nothing is displayed (Xcode variables pane), or two different messages are reported. e.g.:
(lldb) v req
(Foundation.URLRequest) req = <no location, value may have been optimized out>
(lldb) po req
error: expression failed to parse:
error: <EXPR>:8:1: error: cannot find 'req' in scope
req
^~~
frame v
will also show all the same variables Xcode sees should exist, but won’t show values for some. e.g.:
(lldb) frame v
(String) inLogin = "rmann@test.com"
(String) inPassword = "testpass"
(<redacted>) self = 0x0000600001ebce80 {
session = 0x000000012fd07160 {…}
baseURL = "http://localhost:8080/api"
apiKey = "aldknca98s457qo24nfpo9u8a9hartap9a0s8ejl4kysdrt"
config = {
deviceID = 181CD0FD-D242-4794-BD48-8C4837C336E5
token = "a2999a27017bded26f11d18dc4647fae3970eccf0c25e6f7608028a5d0d09578"
selectedOrgID = nil
}
deviceName = "API Unit Test"
}
(Foundation.Data) json = 148 bytes
([String : Any]) obj = 4 key/value pairs {
[0] = {
key = "login"
value = "rmann@test.com"
}
[1] = {
key = "password"
value = "testpass"
}
[2] = {
key = "deviceName"
value = "API Unit Test"
}
[3] = {
key = "deviceID"
value = "181CD0FD-D242-4794-BD48-8C4837C336E5"
}
}
(<MyApp>.LoginRequest) loginRequest = <no location, value may have been optimized out>
(Foundation.URLRequest) req = <no location, value may have been optimized out>
(Foundation.URL) url = <no location, value may have been optimized out>
(Int) statusCode = <variable not available>
(URLResponse) resp = <variable not available>
(Foundation.Data) data = <variable not available>
(lldb)
Stepping Through Async Code
I don’t think I’ve ever been able to successfully step into or over an await
call. I can set a breakpoint inside that call, and it will hit it. I also can’t reliably step out; that usually results in the debugger stopping in some assembly code somewhere, if you then try to step out, you can’t resume execution, step, or anything other than kill the target.
Is it just that most people never experience these behaviors? I’ve had at least half-a-dozen people chime in that they also experience these issues, and are generally relegated to using print()
to debug.
Please know that I write this out of frustration but not with malintent toward anyone. It just seems like such a core functionality, and Apple should but a microscopic portion of their vast wealth toward making their tools best-in-class. I wish I knew enough about the Swift toolchain to actually get in there and fix this. But it would take me months to get anywhere, and I’ve got other things I want to build using those tools.