Debugging Chronicles: A Journey into View Layer Debugging Mastery

İrem Tosun
adessoTurkey
Published in
8 min readMay 29, 2024

Today, we’re going to explore some really awesome strategies as we delve into the interesting world of software debugging! I can assist you with everything, from understanding the complexities of SwiftUI when testing the view layer to improving debugging skills, and finally, experimenting with network layer debugging. So, take a seat comfortably and get ready to learn some awesome tips that will help you become a better debugger. There’s something here for every developer, regardless of experience level. You can download the reference project to experience more. Let’s get started and enjoy debugging! 🚀

“Testing shows the presence, not the absence of bugs.” — Edsger W. Dijkstra

Since SwiftUI is a declarative framework, you can’t add an imperative print call inside your view declaration. The reason is that the view body property expects to return a View value. To overcome that limitations while debugging your view, you can use one of several approaches depending on your needs.

To delve deeper into the topic and demonstrate how to use different approaches, I will provide you with some real code examples and screenshots from my sample app. In the meantime, I will also share my observations.

Now, I’ve designed a view with a visual bug. Let’s imagine you don’t know what caused the bug and you’re trying to fix it. I’ll show you how to use some runtime debugging methods to identify and resolve the problem with the SwiftUI view.

The problematic movements of vane is undetected.

If you look very carefully to the clock, you will notice that vane has an unexpected behaviour to move backwards frequently. In order to explore what caused this, you can use some of the techniques that I will provide.

In my example, this issue has occured because both timer in the view model and the animation on the view are making changes on the rotation angle of the vane. You can find all the details in the source code. To spot the cause of the problem, you can debug the view.

1. Use LLDB Command

You can try to debug the issue with let _ = Self._printChanges() command. In order to do it, all you have to do is to add this command to the beginning of the body part of the view.

struct AnimatedClockView: View {
@State private var model: AnimationClockViewModel = .init()

var body: some View {
let _ = Self._printChanges()
VStack {
clock
}
.onAppear {
model.startTimers()
// Buggy field that moves the vane with a wrong angle
withAnimation(.easeInOut(duration: 1).delay(8).repeatForever()) {
model.rotate(type: .vane, angle: -10)
}
}
.onDisappear {
model.stopTimers()
}
}

...

As a result of doing this, you will notice the following output on the terminal.

As you see, it generated a line that explains why it is redrawn each time the model has changed.

Furthermore, If you are looking for a change in view after a specific action, you can also place a breakpoint to the start of the view and then type this lldb command to terminal: po Self._printChanges(). This will also give you the reason why view has been redrawn.

2. Use of OnChange, onReceive Modifiers

The previous approach was indeed fantastic. However, it didn’t quite meet my needs. I require more detailed information to detect unexpected behavior. Therefore, I’d like to add this line to log the changes.

.onChange(of: model.vaneRotationAngle, initial: false) { old, new in
print("New rotation angle of vane was \(old) -> now it is is \(new)")
}

As you see, I am using the onchange() modifier of the view in order to debug the rotation angles, which give me the following output. You can also use onReceive, depending on your structure.

After observing more carefully, I can say that the unexpected angle occurs in the very beginning. Timer triggers the changes and changes are recognized each time. However, animations behave differently. Although I run it in a repeatForever mode, it is triggered just once. So with the help of this debugging, I can conclude that the timer causes this visual bug and it needs to be removed in order to fix the issue.

3. Take Advantage of Using View Extensions

Let’s talk about another scenario where you want to verify whether you are using the correct color as defined by the design team in your app. It is simple enough that you can add a View extension to accomplish this.

extension View {
func printOutput(_ value: Any, saying description: String = "") -> Self {
let message = "\(description) \(value)".trimmingCharacters(in: .whitespaces)
print(message)
return self
}
}

And then you can add the following function to your view:

.printOutput(clockColor.hex, saying: "Color hex code of the clock is")
As a result you will be able to debug the hex code.

Not limited to just colors or hex codes, you can debug every component in your view using this technique.

4. Use of Preview Provider

As you are already aware, SwiftUI has added a Canvas that greatly simplifies the lives of developers. Even though many developers neglect to spend a lot of attention on that functionality, the preview provider offers fantastic debugging methods.

Design is crucial, and SwiftUI is highly capable of producing responsive screens. However, we should invest more time in debugging to test all possible cases before publishing the app on the store.

First, Select Editor-> Canvas from the menu bar. Then, select the Dynamic type variants from the third option.

Dynamic Type Variants

Selecting dynamic type variants

Please take a look at the unpolished design before debugging and adjusting my view using the preview provider. As you can see, there have been no adjustments made in my implementation for accessibility.

Views displayed before making adjustments
View gets unreadable when large appearence is preferred

For various accessibility options, font sizes are significantly increased to enhance readability for users who prefer larger fonts. Therefore, we may need to redesign components to accommodate all different type variants. Here’s an environment variable that can be a lifesaver for this purpose.

@Environment(\.sizeCategory) var sizeCategory

I used this variable to adjust my components and displayed the components differently based on their size category.

VStack {
if sizeCategory < ConstantSizeCategory.limit {
MediumCharacterDetailView(viewModel: viewModel)
} else {
ExtraLargeCharacterDetailView(viewModel: viewModel)
}
}
enum ConstantSizeCategory {
static let limit: ContentSizeCategory = .extraLarge
}

After making all the adjustments for a better design, I now feel more comfortable viewing the variants.

Result of the display that is smaller than extra large category size.
Improved design for large content sizes.

Apparently, the design changed completely for larger appearances. I made my own decisions while doing this. However, designers can offer much better designs for these kinds of improvements.

Display for smaller variants.
Display for larger variants.

Color Scheme Variants

Obviously, as the name suggests, you can also preview the dark mode and light mode appearances with the preview provider.

Preview of color scheme variants

You can also display it by clicking the run button, which is the first element of the button list. By selecting the dark appearance, you are good to go.

Displaying the dark appearence by using Canvas Device setting

Orientation Variants

Last but not least, orientation variants are also important since most of the apps support orientation of the device.

As you can see, with the help of this feature, we can notice a bug in the landscape orientation type.

Spotted buggy look of landscape mode by preview provider.

By making a little adjustment in the width of the header view, we can fix this.

Fixed version

5. Debug View Hierarchy

Let’s talk about the visual debugger. First and foremost, how do you access it? To access it, you need to set a breakpoint on the onAppear function in your SwiftUI View.

Setting breakpoint to the onAppear function.

As the execution pauses, if you look at the options available from this button, you will see an option that says “Debug View Hierarchy” when you hover over it.

Toolbar displays debug view hierarchy option.

You can go ahead and click on it and you get brought into this cool view.

You go into the visual debugger.

It actually pauses your app so you can’t interact with it right now. What you can do in here, on the right hand side, there is an “Object inspector” and “Size inspector” where you can observe all the constraints.

Displaying size inspector.

Additionally, you can observe a specific component by right clicking and seeing the selections on it.

Possible options when you right click on a component.

Debugging view hierarchy comes in handy when you are programmatically creating you views. The UI debugger provides the developer with runtime constraint errors highlighted with purple ⚠️ symbol.

Some runtime errors are catched.

As you can see, it has already given me a few unexpected runtime issues. I’ll now dive into the code and tackle them immediately. 😊

We’ve explored various great methods for debugging SwiftUI applications’ view layer. I have another article where you can read more about general debugging skills.

--

--