스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Advanced Debugging with Xcode and LLDB
Discover advanced techniques, and tips and tricks for enhancing your Xcode debugging workflows. Learn how to take advantage of LLDB and custom breakpoints for more powerful debugging. Get the most out of Xcode's view debugging tools to solve UI issues in your app more efficiently.
리소스
관련 비디오
WWDC22
WWDC21
WWDC19
WWDC18
-
다운로드
Hello and welcome to advanced debugging with Xcode and LLDB.
I'm Chris Miles, one of the engineering managers on the Xcode team, and I'm really excited to be here today. Now, I'm well aware there's nothing between you and Beer Bash but our session, so thanks for coming. We'll get you out on time, but we've packed in lots of great content for you guys today. So let's get straight into it. I'd like to start by talking about Swift debugging reliability. The main point to make here is that there's good news. The team has landed a lot of reliability fixes in Xcode 10.
The -- thank you.
The compiler and debugger teams have been able to smooth over many of the rough edges that were causing Swift debugging headaches. I'd like to tell you about a couple of them. In some cases, usually with more complex projects or build configurations, attempts to po an object or evaluate an expression in the console may have failed with an error such as this. The AST context that this error refers to is an expression context that LLDB needs to reconstruct the state of the compiler from when it built your project. And, in some cases, such as if there are module conflicts, the expression context cannot be reliably reconstructed, and your expression would fail.
In Xcode 10, LLDB have implemented a fallback mechanism for when this problem case occurs. So if it can't reconstruct the context, it will fall back to creating a simpler context for the current frame and use that to evaluate your expression.
Another failure case that some developers may have had was due to the debugger being unable to materialize variable types while debugging.
This would manifest itself in Xcode looking something like this. Where on the left, you can see the variables' view shows all the names of the variables in the frame but there'd be no type information or values visible. And attempts to print out the value of a variable would fail with errors like these. Now, thanks again, in big part to your bug reports, the team has been able to track down and fix these many edge cases where debug information was not being reliably generated. So I want to thank you guys on behalf of the team again for filing issues when you've encountered any problems while debugging. And if you find any more problems with Xcode 10 while debugging a project, please continue to file bug reports for us. For those of you at the conference, if you find a problem with your project, please come along to the labs. We have an Xcode debugging and profiling lab tomorrow morning from nine till 12. Bring your project and ask for an Xcode debugger engineer or an LLDB engineer, as they would love to see your projects.
Now I'd like to move on to telling you about some of my favorite debugging tips and tricks that I like to use to enhance my debugging webflow.
In fact, I'm not just going to tell you about it, I'd like to show you with a demo.
Now the project we'll be using today is an iOS app called Solar System. And you may have seen Solar System appear in such keynotes as the Keynote and State of the Union. We're going to be debugging an area of the Solar System called Moon Jumper.
Moon Jumper allows the user to hold a phone in the hand and jump in the air.
The app measures the force of your jump and then translates that to moon gravity and shows you a visual representation of how high you would have jumped if you were standing on the moon.
You can set a bar to choose a limit, so you can try and challenge yourself to jump as high as the bar, in terms of moon gravity.
Now in making some enhancements to Moon Jumper, such as some visual enhancements and a GamePlay mode, while we're not ready to ship, we've done a test pass and come up with a list of bugs that we need to look at. So these are the bugs for Solar System. I'll be tackling the iOS bugs first, and later on, Sebastian will come out and solve the macOS bugs. Now, as it says, none of us are getting out of here to the Beer Bash until we fix all of these bugs, so there's nothing more exhilarating than peer programming with 2000 people.
Let's get straight into it and start with the first bug. The jump failure animation does not match the specification.
So what's that talking about? Well, switching to the simulator because we're using the simulator to speed up debugging and development. I've wired up a tap just to recognize them, so every time I tap the astronaut he performs a successful jump to the height of the bar.
Now this bug is talking about the case where the astronaut doesn't reach the height of the bar, so let's reproduce that.
Switching to the editor, I'm going to use the jump bar to navigate to the jump function and set a breakpoint at the start of that function.
Now I'll tap the astronaut, and we'll start a jump and now we are paused in the debugger.
The first thing to point out is actually up here in the tab bar.
We now have four tabs. This debug tab was just created for us by Xcode, and for those of you that like working in tabs like I do, this is an Xcode behavior that you can define.
So to do that you use the Xcode menu to edit behaviors and that takes you to the preferences in the behavior's tab. And here you can configure many behaviors. In this case, you would need to configure the pause's behavior in the running section. And this is the behavior that will be actioned when Xcode pauses in the debugger. So you can see here I've configured it to show a tab named debug, and Xcode will always switch to that the tab when pausing in the debugger. So that's great for users who like to work in tabs like I do.
Now switching back to the code, we can see that there's a condition based on this property did reach selected height, so I'd like to have a look at the value of this property. So switching to the debug console, I can use po to look at the value of that property and it's currently set to true. So the always sets this to true, and I'd like to change it to false so we can reproduce the bug. Now I could go to the code and modify it, tap just to recognize it to set it to false, but I don't like to make changes to my code just for debugging purposes if I can avoid it. So in this case, what I can do is use the debugger to do it for me. I can use the expression command. And here I can give it any Swift expression, such as did reach selected height equals false, and it will evaluate that and execute it. So now, we can see that this property has indeed changed to false.
And if I step over the line using the debugger, we can see that we've entered the false side of the branch.
So now, when we continue, we can see that the astronaut doesn't quite reach the height of the bar and falls down.
So that's the case we're trying to reproduce. I'd like this to happen every time I tap on the astronaut, and I don't want to have to pause and type this expression each time, so what I'm going to do is configure this breakpoint to do that for me.
If I right click on the breakpoint, I can select edit breakpoint and this gives me a popover window with some configuration to customize how the breakpoint behaves. I'm going to select the debugger command action and enter the expression command with the command that I used in the debug console and make this an auto-continuing breakpoint. So what we've configured here is a breakpoint that once execution enters this function, the breakpoint will trigger execute this command for us to change the value of the property and then automatically continue.
So now, every time I tap the astronaut, he'll perform this unsuccessful jump and fall back down.
Now what's the problem that we need to fix? The specification said that after falling down, the astronaut should stand back up again, so let's fix that now.
I'm going to navigate to this function update UI for jump failed, and we can see this function uses UIKit Dynamics to simulate failing the jump. So it starts by creating a UI Dynamic animator and then calls a function to add behaviors to create the physics effects. And then the astronaut is meant to be reoriented and recentered in the dynamic animator did pause delegate callback. Now we can scroll down and see that that delegate callback is implemented successfully, so that looks fine.
But I'm noticing that no delegate is set on this object, so if I add that code in here, I think this is the change I'll need to fix the problem. Now, at this point, I can recompile and rerun and try and verify my fix, but I'd like to shortcut that whole cycle if I can. So what I'm going to do is use a breakpoint to inject this change for me so I can see if this does fix the problem quickly and conveniently.
So I'll create a break point, double click to bring up the edit window, which is a shortcut for bringing up the edit window. And then, once again, use a debugger command action to inject an expression, and I can type in that line of code that I think will fix the problem and make this an continuing breakpoint.
So what I've configured here, even though I've made the change in the code, I haven't recompiled, of course. So I'm using the custom breakpoint to inject that change for me so I can test it using the current live application. So now, if I tap the astronaut, he performs this unsuccessful jump and falls over and stands back up again, so it looks like we fixed the problem. I like this effect, so I'm going to do it again.
Now let me just launch notes again.
And we can tick off the first bug as fixed.
Nothing better than checking off fixed bugs.
Now the next three bugs are to do with the new GamePlay mode I've been working on. So I'm going to press play in the simulator to show you that, and the challenge here is to try and jump higher than the bar 10 times. And the bar starts low and raises each time.
And you can also see some score labels have been added to the top. So if I tap the astronaut, you'll see he's still configured to perform an unsuccessful jump, and then the attempts label at the top should have incremented but it didn't. And that's the first bug we've got here, where these labels flash but do not change. We've also got an issue with the end-of-game state not being handled properly and a problem with the layout of those score and attempts labels, but we'll come back to those.
So returning to this bug, if you didn't notice it, let me tap the astronaut again and keep an eye up on the top on the attempts label.
You'll see that it does flash but it doesn't update. So what this tells me is that the label is getting a value set on it because we're seeing the transition animation but the value is incorrect. So I'd like to find the code that is modifying this label to have a look at what the logic is at that point.
So we note UI label is getting the text property changed, so what I'm going to do is switch to the breakpoint navigator and down at the bottom here select this plus button to create one of many specialized breakpoints. You can see we've got breakpoints for Swift errors and exceptions and even test values, but for this case, I'm going to use a symbolic breakpoint.
So that creates a new breakpoint and brings up the editor for that, and here we can enter any function or method name such as UI labels, set text, which we'll need in this case, and we enter it in Objective-C format because UIKit is in Objective-C framework.
The thing to point out, if I dismiss that, is that below the breakpoint, there is a added and this is the feedback from the debugger to tell us that it was able to resolve this breakpoint to one location in UIKit, Core.
Some symbols may result in multiple locations, and you would see them all here. And if you saw no entries, that would indicate that the debugger wasn't able to resolve your breakpoint, so it wouldn't hit.
So with that in place, I'm going to tap the astronaut again, and now we see that we've hit the breakpoint in UI label set text. Now we don't have the source code for UIKit, of course, so we're looking at assembly code, but there's no need to remain in the dark even if you're in assembly code of one of the system frameworks. We can inspect the arguments passed into a function. You just need to know the calling convention for the architecture, and you can inspect the registers to see the arguments. Well, I'll admit I never remember what those registers are but thankfully, I don't have to because the debugger provides pseudo-registers.
Dollar arg one is translated to the register that holds the first argument. So in this case, we can have a look at the receiver of that Objective-C message, which is a UI label instance.
Now we see that it has a value of 17 feet, and that indicates to me that it's this height label here, so it's not the label we're interested in, but while we're here, let's look at the other arguments.
If you're familiar with Objective-C message send, you may remember that the second argument should be the selector.
We don't see that here but that's because LLDB doesn't implicitly know the types of these arguments. So in some cases, we need to typecast it and now we see the selector for this message. Now the third argument is the first parameter passed into the method. In other words, it's the string passed into the set text. So this is a great convenience for inspecting the arguments, if you're in an assembly frame for one of the frameworks.
But like I said, this wasn't the object or the label we're interested in, so let's press continue, and now we've hit the breakpoint again. So we can inspect dollar arg one to see what the receiver is, and it looks like it's the same height label with zero feet now. And I can see the problem with my strategy.
While the astronaut is jumping, the code is updating the height label in real time, so the breakpoint is going to hit on this object quite frequently, and it's going to take us a long time. It'll be very difficult to hit a breakpoint on UI label set text for the attempts label. So what I think I should do is only set this symbolic breakpoint after the jump animation has completed, so let me show you a way to do that. I'm going to switch to the breakpoint navigator. And if I double-click on the indicator for the symbolic breakpoint, we can bring the editor back up and we can use this condition field. We can enter an expression here that returns true or false, and the breakpoint will only trigger if that expression evaluates to true. So if we had a property such as jump animation in progress, we could edit an expression to test if that was false and then the breakpoint would trigger. I don't have a property in this case, so I'm going to show you a different method. I'm going to delete that symbolic breakpoint and instead, scroll down to this function jump completed and set a breakpoint here.
Now jump completed is called after the animation has finished so that it can update UI and update game state. However, we don't want a break in this function. What I want to do is configure this breakpoint to actually set the symbolic breakpoint in UI label set text for me. And I can do that by adding a debugger command action, which is breakpoint set, and I'm going to use the option one shot true. And a one shot breakpoint is a temporary breakpoint that only exists until it's triggered and then it's automatically deleted.
And we can give it the symbolic name, UI label set text, and make this an order continuing breakpoint. So what we've created, in this case, is a breakpoint that when the execution enters the jump completed function, it sets a temporary breakpoint in the location we're interested in and then continues. So then we'll only hit that set text breakpoint after flowing through this function.
So let's press continue, and we see that the jump animation completes in the simulator, and now we hit the breakpoint on UI label set text. So now we can have a look at the receiver of that message by using po dollar arg one. And we see that is indeed a different UI label instance with a value of zero, so that's likely to be one of these at the top. So we think we've found the right object, so let's have a look at the code that's modifying this label's value.
We can do that in the debug navigator by selecting the next frame up in the stack. And now we've found the code that's modifying the label value.
It's currently passing in a string of zero using the label text variable, and looking up, we can see that label text is always set to the label's current value, so that's not going to change. It looks like value text is the variable that contains the new value, so probably just a typo; let's fix that.
Let's make it value text.
And then, what I'd like to do, rather than recompile and rerun to test this change, I'd like to actually test this change in the context of the current running application like before. So I'm going to add a breakpoint below the current line because remember, we've changed the code but we haven't recompiled. So while the label will still be set, we'll add our line below that to set it to the value we think it should be. So another custom breakpoint, and we can use expression once again to inject that code and make this auto-continuing.
So now, if I press continue, code execution will continue to flow through here, and we see that the attempts label is indeed updated. I'd like to make sure that also works. Well, thank you.
I'd like to make sure that also works for the score label after a successful jump, so let's navigate back to here. I can remove this jump-completed breakpoint that was creating the one shot breakpoint because we don't need that anymore. And I can disable the breakpoint in jump because we don't want to modify did reach selected height anymore. And now, when I tap the astronaut, he performs a successful jump, and we can see that all the labels update properly, so that bug looks great to me.
So let's return and check that one off.
All right. The next bug is a problem with the end-of-game state. So the game is meant to end after 10 attempts, so I could tap the astronaut, wait for the animations to play out, and progress through the game to get through that state to try and reproduce it. But the animations take some time, and I may need to do this numerous times while I test and verify my fixes, so I'd like to skip all the jump animations. So let me show you how I'll do that. I'm going to navigate to update UI for jump succeeded, and we can see this function modifies some colors and then calls jump astronaut animated true. So it looks like I just need to call jump astronaut animated false. I could change the code and recompile, but like I said before, I don't like to change my code for debugging purposes if I can avoid it, so let me show you the technique I'll use instead. I'm going to set a breakpoint on this line. Let's clear the debug console and initiate a new jump by tapping the astronaut, and now we're paused on this line.
So I need to ask the debugger to replace this line with a call to jump astronaut animated false. Well, the code's compiled in, we can't replace it, but what we can do is ask the debugger to skip over this line, to not execute it but skip over it. And then we can use expression to inject the change that we -- or the call that we'd like to make.
So how do we skip over a line? Well, let me draw your attention to this green annotation label thread one. We call this the instruction pointer. It points to the line containing the instructions that will be executed next. And this icon here, that kind of looks like a grab handle, in fact, is a grab handle. If I hold the mouse down, I can move this around, and I'm able to change the instruction pointer while paused, so I can move it down one line and let go and then we get a scary message from Xcode. And basically, what it's saying is, with great power comes great responsibility. And I'll be honest with you, this is the riskiest feature I'll tell you about today. But it's only risky because the debugger allows you to move the instruction point wherever you like. It does not care, but it cannot guarantee that the application's state will remain intact. So you could, for example, cause memory management issues if you end up referencing an object that has not yet been initialized or over-releasing an object, for example.
But we've all been to the advanced debugging session now, so we know what we're doing. So let's press the blue button.
All right. So we've skipped over that line, and now in the console, we can use expression and call jump astronaut animated false.
And now we press continue to see if all this worked, and indeed, the game state updated, and we skipped all the jump animations.
So I'd like that to happen every time I tap the astronaut, so I'm going to configure this breakpoint here to do that for me.
So first, we need a debugger action that skips over one line, and the command for that is thread jump.
And I give it the option by one, and this tells the debugger to jump over one line of code for the current thread. And then we just need to call our expression and we can do that by pressing the plus button and adding another command action.
Expression jump astronaut animated false. And we make this order continuing.
So what we've got here is a breakpoint then when execution reaches this line but before it executes this line, the breakpoint is triggered. It will perform the command to jump over that line and then use expression to call the function call that we'd like to make instead. So now, if we tap on the astronaut, we can rapidly progress through the game state and skip all the animations and easily reproduce our bug.
So, as I said, the game was meant to end after 10 attempts, and we've gone well beyond that, so let's have a look at the game state.
That is all stored in a property up here called GamePlay, so I'm going to set a breakpoint on that property and start a new jump. And now we've paused at the next reference to that property.
I'm going to use po to look at the current state of that object, and here we see the debugged description for this GamePlay object. And we have a custom debugged description here, so it's worth pointing out that po requests the programmatic debug description of an object and you can customize these. I'll show you how we did that for GamePlay. If I switch to the source code and scroll to the bottom, you can see that we've added an extension to conform GamePlay to custom debug string convertible.
And conformance requires that you implement a property called debug description and return a string. And you can return whatever string you like to represent this object for your debugging purposes.
You can do the same for Objective-C objects as well by implementing debug description.
Compare that with the command p GamePlay.
P is an alternate LLDB command, which uses LLDB's built-in formatters to represent the object. So here, we see two representations of the same object, and default formatter shows you the full type name, the memory address, and then a list of all the properties and their values. So we can see here that there is a max attempts property, and it's correctly set to 10, so it looks like there's perhaps a logic error where, after attempts is incremented, it's not successfully determining that it's past the maximum. So I'd like to find the code that's modifying attempts to see what the logic looks like. I'm going to open the variables view and expand the view controller to see all its properties and down at the bottom, I'm going to use the filter to enter GamePlay to find that property. Expand its properties and then I'm going to select the attempts property.
And down here, I'm going to open the contextual menu and select watch attempts.
Now what this does is creates what's called a watchpoint.
In the breakpoint navigator, below all the breakpoints, you'll see there's a new group called watchpoints, and we have one watchpoint for attempts. And a watchpoint is like a breakpoint, but it pauses the debugger the next time the value of that variable is changed, so we can remove this property breakpoint because we no longer need it and press continue. And now we've paused at this watchpoint, and we found the code that's modifying the attempts variable. I can disable this watchpoint because I no longer need it. And we can look at the code here while the game's playing, increment attempts, and if successful, increment score. So I'm not seeing any logic that will detect if attempts has exceeded maximum and transition to the end-of-game state.
So I think that's all that's needed, but in this case, I'd like to test my hypothesis before I make any actual code changes. So I'm going to create a breakpoint and configure it to inject that change to see if it fixes the problem before I make any code changes. So once again, I can add a debugger command action with an expression and I think what we need is if attempts is greater and equal to max attempts, then we change the game state to ended and make this order continuing.
So now it's easy just to test if that actually fixes the problem by pressing continue. Execution will continue through this breakpoint. Inject the code and we can see that it does look like it fixes the problem. I'd like to verify that from the start of the game and I can quickly and easily do that by clicking play again and rapidly progressing through 10 attempts. And at the tenth, we see that it does indeed detect end-of-game state, so it looks like that's the fix we need.
And now, don't forget to apply that to your code. So I'm just copy that out, drag the breakpoint to delete it. And then I can paste that in and that looks good.
So let's check that one off, and we've only got one more left for this section and that's the layout of the attempt and score labels. Now, the layout of this application has been left up to the engineers, and as good engineers do, we've found an efficient location stuffed right up in the top corners. But the team decided that wasn't very appropriate, so they've sent it back and asked us to try again. So I'd like to mock up a new layout for these score labels. Now I could get out my graphical application and start mocking it up, but I'm an engineer and I like to mock up using code. In fact, I'm a debugger engineer, so I like to mock up using the debugger with a live application and real data. So let me show you how to do that.
Let's navigate back and put breakpoint in the jump function. What we need to do is first find a reference to a view that we can play around with, so I'm going to clear everything and open that up and start a new jump. So we're paused in the debugger in this jump function within the view controller.
So if you have of course a property or an outlet for a view, then that's a good reference. But if you don't, then you need to get the memory address of a view. So let me show you some ways to find the memory address and how to manipulate a view only by memory address.
Well, like we said before, the debug description contains a custom description. So looking at the view controller's view, we can see the default debug description for a UI view has the class of the view and then the memory address. So one way is to just get the debug description for objects. So that's easy to get it for this one because there's a property for it. But how about all of the views below this view controller's view? Well, we need to look at the view hierarchy and one way to do that is to use this button here, which invokes Xcode's visual view debugger. It will snapshot the hierarchy and give you a 3D exploded view, and you can use that to inspect views that way.
Sebastian's going to talk more about that in a few minutes, so let me show you an alternative way, which is good for simpler hierarchies and keeps you in the debug console. And that's using a debug function on UI view called recursive description.
So we should be able to call po self.view recursive description.
However, that doesn't work. Why is that? Well, recursive description only exists for debugging purposes. It's not part of the public API and so isn't to Swift. And Swift is a strict language and doesn't allow you to call functions that haven't been strictly defined.
However, Objective-C code can run wild and free in Objective-C world and you can pretty much do whatever you like. I mean it's a dynamic language so you can call functions like this. So what we need to do is to tell the debugger to evaluate this expression in an Objective-C syntax. And the way to do that is to use expression with the option - l objc. That tells expression that you're about to give it Objective-C code even though you're in a Swift frame. And we'll give it -O, tell it that we also want the debug description the same as po would do and -- to indicate that there are no more options.
The rest of the line is just raw expression input. So we should be able to then give it the Objective-C format of this method call.
Unfortunately, that doesn't quite work and the reason for that is that expression will create a temporary expression context for the Objective-C compilation, and it doesn't inherit all the variables from the Swift frame. So there's a way around that, though.
If we just put view in back ticks. Back ticks is like a step that says first, evaluate the contents of this in the current frame and insert the result, and then we can evaluate the rest. And now we get the recursive description.
So using this, we can see all the debug descriptions for all the views. And I'm interested in the scoreboard views, which host these labels, so we can find the memory address for one of those. And now we can use po memory address, which you might be familiar with if you're an Objective-C developer. Well, that doesn't work and that's because Swift doesn't treat numbers as pointers and de-reference them for you. So once again, we need to do this from an Objective-C context. So we could do the same thing we did before, but I find this to be so convenient that I like to shortcut this down to just a simple short command. So I'm going to do that by using command alias, and I'm going to call that command poc.
So now that I've created an alias, I can simply poc that memory address and see the debug description for that object. I'd like to show you another way to look at the description of an object if you only have its memory address. And in Swift, you can use a function called unsafe bit cast.
Give it the memory address and then it's unsafe because it's up to you to provide the correct type, so I'll give it scoreboard view.self.
And now we see we can use unsafe bit cast to see the debug description for an object.
Now the great thing about unsafe bit cast is that it returns a typed result, so we can call our functions and property names on it such as .frame.
And in this case, I'd like to inspect a center point and then modify that center point. Let's change it to 300 we can see it has changed to 300, but the view in the simulator hasn't moved. Well, why not? Well, we're paused in the debugger, so cronomation isn't currently applying any view module changes to the screen's frame buffer. But we can ask cronomation to do that for us, just use the expression ca transaction.flush and that tells cronomation to update the screen's frame buffer.
So now, I can just use these two lines to fix the new positions and continue flashing and we can move around.
And in fact, I find this to be so convenient that I kind of wanted to wrap all this up in just a single command to nudge views around, and so that's what I did.
Let me show you that. I'm going to switch to terminal and open a Python file.
Why a Python file? Well, LLDB is scriptable using Python, where you get full access to the LLDB API.
So I've created an LLDB Python script to create a nudge command, which takes an x offset, a y offset and a view expression, and you can use that to nudge views around while paused in the debugger. Now, it might look like a sort of long script but most of that is argument pausing. The core of it, in the middle, is just calling out to the expressions we would call in manually.
We don't have time unfortunately to go into detail in this script. But we're going to make this available for you guys to download, so you can see how it works and use it as the basis for your custom debugging commands.
Let me show you how to enable a script like this. Just edit your .lldb in it file in your home directory and add a line command script import.
I'd also like to add some of the aliases that I find convenient, such as the poc alias I created before, and an alias for flushing the transaction. I think I'll remember this one.
I'm going to copy command script import so we can just paste it in to the debug session to save us restarting that session. And now we have a command called nudge. So I can, let's say, nudge zero horizontally, minus five vertically, give it the memory address of that view and start just nudging it around in the simulator.
The great thing about LLDB is if you just hit enter on a blank line, it repeats the previous line, so it's great for nudging.
And I can nudge it across to the right a bit, just to get it right. And then let's do the other view. We can give it any view expression, say the attempts view down to here. The other feature of nudge is once you've given it a view expression, you don't have to repeat that expression.
It remembers that and applies it to the same view that you've specified previously. So something like that looks fine. It's a better layout than we had before. And what I can do now is take the information provided by nudge, such as the total offset applied to that view relative to its original center point, and then your frame value. Back to my code and modify my layout code or my auto-layout constraints, and I've easily mocked up a new layout for my scene.
Now the last thing to do -- well, firstly, don't forget to check off debug, very important.
And then the last thing to do before restarting or recompiling and rerunning is to disable or remove any breakpoints that are injecting expressions because you don't want those lines to be executed twice.
Simply selecting them or a group of them and hitting delete is a quick way to delete those.
And so those are some of the debugging techniques that I like to use to enhance my debugging workflows.
Notice how we were able to diagnose and fix all four bugs without having to recompile or rerun.
This can be a huge timesaver, especially for complex projects and can be crucial when trying to solve hard to reproduce bugs. So thanks for I hope you enjoyed that and can use these techniques in your debugging sessions.
I'd just like to quickly recap all of the features and tricks that we went over during that debug session. So firstly, we looked at how we can use Xcode behaviors to dedicate attempt to debugging and how to use LLDB expressions to modify program state.
We can use auto-continuing breakpoints with debugger commands to inject code live, and we can create dependent breakpoint configurations using breakpoint set one shot as a debugger command action for another breakpoint.
Even when in assembly frames, we can easily inspect the function arguments using po dollar arg one, dollar arg two, et cetera, and we can skip lines of code by dragging the instruction pointer or using the command thread jump.
We can request that the debugger pause when a variable is modified using watchpoints, and we can even evaluate Objective-C code in Swift frames using expression -l objc. We can request that view changes are flashed directly to the screen, even while paused in the debugger, using the expression ca transition flush.
And you can add custom LLDB commands, either aliasing commonly used commands to correct shortcuts or by completely customizing and creating your own command using LLDB's Python scripting. And don't forget to check out our session website. We'll be posting that nudge script soon, so you can download it, check it out, and use it as the basis for your commands.
There's one more thing I wanted to cover with you guys and that's just the current LLDB print commands. So you might be familiar with po. We used it a lot during the demo, and we saw that po requests the debug description of an object, and you can customize that. And that's because po is simply an alias for expression -- object description or expression -O, compared with the p commands, which is simply an alias for expression. And it uses LLDB's built-in formatters to show a representation of that object.
The third command that's important to know is frame variable. It differs from the previous two in that it doesn't have to compile and evaluate an expression at all. It simply reads the value of the name variable directly from memory and then uses LLDB's built-in formatters.
So the choice of which command to use is more personal preference and the type of information you want to see while debugging.
But it's important to remember that if you ever end up in a situation where expressions are failing or so po and p may not be working for you, if you need to inspect a variable in the current frame, then frame variable should still work for you.
And with that, I'd like to hand over to Sebastian, who's going to tell you about some advanced view debugging techniques. Thank you.
Thank you, Chris.
I'm excited to show you tips and tricks how to get the most out of Xcode's view debugger. And we'll also take a look at the enhancements we made for Xcode 10 to provide a great debugging experience when you adopt a dark look in macOS . And we'll take a look at this in a demo.
So let me switch to the demo machine, and I'll be using the same project that Chris has been using and you already saw that there are two more bugs we have to solve. However, I'm not going to be using the iOS App, I'm going to be using the .
So we can see the Mac version of our Solar System app here, we can see that looks pretty good in dark mode but there are two bugs that we have to solve today.
First of all, the planet image is not centered horizontally correctly and that is a very obvious bug. You can see on the right-hand side this Earth image is shifted to the right-hand side, so we'll take a look at this problem. And the second bug is that the description in a popover is not readable in dark mode. Let me show you what that is referring to.
When I switch to this app, I can bring up the orbital details information in this popover here.
You can see that the labels at the top and nice and readable; however, the label at the bottom is so hard to read, I really have to select the text to read it.
So these are the two bugs we have to take a look at.
Let me hide the to do list and let's jump right in. So what I want to do is I want to capture the view hierarchy of this application with Xcode, then inspect it.
We'll find the problem and then hopefully find the fix, so we can all have a beer.
The problem is when I switch to Xcode to capture the view hierarchy, this popover will be dismissed since the application goes into the background, and we won't be able to capture its view hierarchy.
So what we have to do is we have to capture the app in its active state, and I will show you two ways how to do that.
And you can see when I now switch to Xcode how this popover is being dismissed.
First of all, we can use the touch bar, and I'll show you what that looks like by bringing up the touch bar simulator from Xcode's window menu.
I'll switch back to the Solar System app and bring up the popover again. And taking a look at the touch bar, you can see that there's this spray can icon, and when I tap that on the touch bar, you can see that there's a subset of the debug option that Xcode provides in its debug bar. So it's a very convenient way to access these from your touch bar. And as you can see, I can bring those up with Xcode being in the background, so you can access them even when you're, for example, developing your app in full screen mode.
And one of these options allows me to capture the view hierarchy. Now I'm not going to do that because I know that not everybody has a touch bar so I will show you an alternative.
I'm going to close the simulator, and I will make use of command click to perform the click on the button in Xcode's debug bar.
Command click is a system-wide gesture that allows you to perform mouse events without activating the application that the mouse event is performed on.
So this allowed us to invoke the capture of the view hierarchy. The debugger paused the application while it was still in its active state, and we can see that the UI still renders as if the app was front most and the popover hasn't been dismissed. If you're wondering why that spinning is coming up, that's because the application is paused in the debugger and doesn't respond to mouse events anymore.
Now, you may be wondering, if we take a look at the view debugger here, why the popover isn't visible.
Don't worry, the view hierarchy has been captured. I'll get to how we take a look at the popover once we get to that bug, but first I want to take a look at the layout issue of this image view here. So I'll select the image view here and I'll zoom in a little bit. When we take a look at the inspect on the right-hand side, we can see that this is an underscore NS image view simple image view. Now the fact that this is prefix with an underscore usually hints at the fact that this is an internal class from a system framework and not what we use when we set up image views in code or in interface builder. So let's take a look at this object in the view hierarchy. And I can do that by using the navigate menu, select reveal in debug navigator.
We can now, on the left-hand side, see it relative to its super and sub views.
Now, we can see that super view is in fact an NS image view, so that's what we're looking for.
We can also see that its super view is a planet globe view, and the super view of the planet globe view is an NS effect view.
So I will select the image view here and we can now also see other properties of the image view on the right-hand side.
So let's inspect the layout of this view.
I'm using auto-layout in this application, so I want to take a look at the auto-layout constraints.
I can show the constraints using this button here, and we can now see all the constraints that currently affect the layout of this view. You can see that there's, for example, an constraint here.
We can also see this vertical line here, which is an alignment constraint.
When I select this constraint, we can see all the properties in the inspect on the right-hand side. If you're wondering why the view debugger only shows you wireframes right now, that is because it only shows the content of the views that currently participate in the layout of the selected view. And since all those views themselves don't have content, we currently only show wireframes.
So with the constraint selected, let's take a look at the inspect on the right-hand side.
We can see that it aligns the horizontal center of the image view with the horizontal center of the planet globe view and it does so with a constant of zero. So it aligns it horizontally centered in its super view.
Now let's select the planet globe view from the debug navigator, and we can see that it's a little bit larger towards the left-hand side but it aligns on the right-hand side. So that's a little bit weird because we just saw the constraints aligning them correctly or exactly horizontally, but that's not really what we see in the view debugger. So let's take a look at the constraints of the planet globe view and see if we can understand what's going on. I'll select the leading constraint here, and taking a look at the inspect again, we can see that it aligns the leading edge of the planet globe view with the leading edge of the NS visual effect view, which we saw in super view. So it simply inserts it relative to its super view and it does so with a constant of 30, so that makes sense.
And then the trailing constraint here aligns the trailing edge of the planet globe view with the trailing edge of the super view, and it also does so with a constant of 30.
Now the fact that this constraint doesn't attach to anything on the right-hand side is a little bit suspicious and makes me wonder if we see the whole picture here. In such a situation, it's usually a good idea to see if there's any content that's currently being clipped that we don't see by default.
And you can do so using this button down here to show clip content. Now, when I enable this functionality, you can see that the planet globe view actually extends past the window bounds towards the right-hand side, and now the horizontal centering constraint makes sense again since it actually correctly centers it in the super view. However, the super view just extends past the window. This is a very common issue if you set up your constraints in code, where you accidentally swap first and second item and thereby get the layout direction wrong. Or you accidentally invert the constant, and in this case, we used 30 instead of using negative 30 to insert it towards the left-hand side. So what I want to do is I want to try out that fix to invert the constant. I will make use of the same technique that Chris introduced earlier by simply applying it through LLDB.
So, with this constraint selected, I'll select edit, copy.
And I will bring up the console area at the bottom, and what this copying gives me is the pointer to the selected object, and that works for all objects that you select in the view debugger as well as in the memory graph debugger. It makes it super convenient to use them in the console. So let's -- Thank you.
Let's print the debug description and we can confirm that the constant is in fact positive 30.
That's what we saw in the inspector as well. So let's set the constant to negative 30. I'll type e, which is the super short form of expression by casting the pointer and then say set constant to negative 30. We have the same problem that the path application doesn't update that Chris saw earlier, so what I'm going to do or what I have to do is I have to the ca transaction to application schedule update of its UI. I can use the handy command that Chris added earlier, so I can take the command from here.
And we can now see that the planet image is horizontally centered correctly, so inverting the constant was the correct fix. So we were able to confirm this, so let's apply this change in our code.
With the constraints selected, you can see that on the right-hand side I can see this back trace here, and that is the allocation back trace of the constraint and tells me exactly in which frame it was allocated. So it allows me to jump right to the code where I created that constraint.
Now to have this creation back traces show up, you have to enable mailx backlogging and let me show you how you enable that. You go to your scheme up here and edit scheme.
And in the diagnostics tab of the scheme options, under logging, you can enable mailx stack logging for all allocations in prehistory.
And that will provide you with these handy allocation backtraces for all selected objects in the view debugger as well as in Xcode's memory graph debugger.
Now, when I mouse over this stackframe here, we can see the full name of that frame.
And we can see that it is the set up planet globe view layout method in the scene view controller.
And I can jump to these points in code using this jump to arrow.
And I will do so by holding down shift control option, which brings us this navigational and allows me to open this file in a separate window.
Now you can see that the line of code where the constraint is allocated is highlighted.
We can see the constant of 30, and I can invert it to negative 30.
I'll save this file and close it and we're done with our first bug.
Perfect. So the second bug was that we wouldn't be able to read the description inside that popover, so let's take a look at that.
First of all, I want to disable constraint mode and clipping so we see the constant again. And I will also clear the console.
Now, I showed you at the beginning how to capture this in its active state so we would be able to get to the view hierarchy of the popover while it's open; however, we don't see it. That is because the view debugger only shows you a single window at a time.
Let me show you how to take a look at other windows.
When I scroll up in the view hierarchy and walk up the view hierarchy to eventually hit the window of the current window that we see, we can see that it's hosted by a window controller. And if I collapse this root level item here, we can see that there's actually another root level item, which is exactly what we're looking for.
Our popover.
So if your application has multiple windows, and that is true for macOS and iOS, they show up as multiple root level objects in the outline on the left-hand side.
So take a look there, if you think your application should have multiple windows that should be captured by the view debugger.
So we can take a look at this in 3D, and we can see that there's this large view that's obscuring clicks to the labels. I would like to take a look at the labels to inspect them. There's a trick to click through views in the view debugger. You can simply hold down command.
So I can click through this view in front and select this blue label.
So let's take a look at the text color for this label. I want to first look at the labels that look great in dark so that we can hopefully derive a solution for the problematic label at the bottom. So let's take a look at the text color and we can see that this is a RGB color resulting in this blue and we can also see that the inspector provides us with a name for this color. Now this indicates that this color is coming from an asset catalogue in our project and with Xcode 10, you can provide multiple variants of a color for a single color that you define. So, for example, you can provide one for light and one for dark.
And which variant is picked is determined by the appearance of a view, and you can get that information in the inspector as well. And I scroll down here to the view section in the inspector. You can see that there's appearance and effective appearance. Now appearance is not set explicitly on this view. And this is a very common scenario because most views inherit the appearance from either one of the super views, the window or the application.
But you can see the inherited effective appearance here. And that is vibrant dark, so that determines which color of the color that you define in your asset catalogue is selected.
Okay. Actually, while I'm down here in the inspector, I want to point out the description property, which is the debug description of that object. And Chris showed you earlier how to provide a custom debug description for your objects. So you don't only benefit from providing a great debug description in your console when you use po on an object but you also benefit from it in the view debugger, since it shows up right in the inspector.
Okay, so let's get back to the text color. And I want to select the second label since that also looks good in dark. And in this case, we can see there's also a name color. It's a label color, but it's prefixed with system. And that indicates that it's not coming from our own asset catalogue but from the system, and of course, system colors automatically adapt to appearance changes as well.
Now, taking a look at the problematic label here, we can see that it's this very dark gray, and it doesn't have a name. That means it's a custom color that doesn't adapt to appearance changes.
So what we want to do is we want to change its color, its text color, to a system color. So with this object selected, I'll hit command c.
I'll type e, paste in the pointer and say set text color to NS color text color.
Again, I have to to see a transaction.
And we can see that the popover now updates, and the font is nice and readable.
Now, I'm not going to apply this fix in my storybook file.
But one thing I want to point out is it's very important that you verify that your application still looks good across all appearances when you make a change to your now that there are multiple system appearances.
And I will show you how to do that. I will continue running, and instead of switching my entire system appearance to light to take a look if the label still looks good on a light background, Xcode 10 provides you with a way to override the appearance only for the target application. And you can use this button in the debug bar. And I can select light here, and you can see that the application now is presented in a light appearance.
And I can bring up the popover, and we can verify that the text is nice and readable.
So that confirms that we fixed our issue. Now, since this is a very common action to take a look at your application in different appearances, we actually made that option available in the touch bar. I will show you . I'll bring up the touch bar simulator again, and with the popover opened, I can select this option here.
And you can now see all the override options right in your touch bar, so even if your application is in full screen mode, for example, I can access these.
Let me switch to high contrast light, which enables the accessibility feature of high contrast as well as overrides the appearance to light, so you can make sure that your app looks good in that configuration as well.
And, of course, I can go back to the system appearance as well. So we confirmed that our issue is fixed. And that allows me to check off this on our bug list with this, we're at the end of the demo and I will go back to slides.
So let's recap what we just saw.
I showed you how to use reveal in debug navigator to orient your current selection in the hierarchical outline on the left-hand side.
I showed you how to show clip content, and we made use of the auto-layout debugging functionality to track down our constraint problem. I showed you how to use the object pointers that you can easily access by just copying the selected objects and use them in LLDB. We had a look at the creation backtraces, which are available if you have mailx stack logging enabled in your scheme options to jump right to the code and applied a change that we needed for our constraint.
We had a look at the debug description that's conveniently available in the inspector of the view debugger.
And we had a look at click through to select a view that was behind another view. And regarding debugging dark mode, we saw that you can easily override the appearance of the target application right from Xcode's debug bar or from the touch bar.
I showed you how to capture a Mac app in its active state. And we had a look at the name color information and its appearance information that is now available in Xcode's view debugger inspector. If you want to learn more about adapting the dark mode in your Mac application, please check out the videos for these two sessions.
That brings us to the end of this talk.
If you want more information about the talk, including the script that Chris showed you earlier, it will be posted on the session's website. And if you have any questions to any of the content of this session, or debugging in general, there's a profiling and debugging lab tomorrow morning at 9 am. Chris and I will be there. We'll be happy to answer any questions you may have. And there's also an iOS memory deep dive talk, in case you're interested, in memory debugging that is also tomorrow.
With that, I hope you have a fantastic time at the Beer Bash and enjoy the rest of the conference.
[ Applause ]
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.