Debugging techniques in SwiftUI

abdul ahad
4 min readFeb 26, 2024

--

Photo by Markus Spiske on Unsplash

If you’re trying to debug a SwiftUI view, you need to adjust your approach. Because SwiftUI is a declarative framework, you can’t add an imperative print call inside your view declaration. This is because the view body property expects to return a View value. To resolve this, you can use one of several approaches.

1- Add normal print statements

Image(systemName: "globe")
.onAppear {
print("on appear called.")
}

if you want to add in the normal print statements then you have to do it in on appear as directly calling it inside the body will give you an error like below.

You could also add an extension on View that returns itself and calls the print method:

extension View {
func printOutput(_ value: Any) -> Self {
print(value)
return self
}
}

You can then use printOutput as a view modifier to print any debug information you provide to the console when the view is built:

you can even go a step further and provide a debug action closure that only runs in debug mode only

 func debugAction(_ closure: () -> Void) -> Self {
#if DEBUG
closure()
#endif

return self
}

here we call assert to validate our assumption otherwise I crash my app which can come in handy sometimes as keeping track of the properties can get out of hand.

2- Self._printChanges()

you can also debug when your views change and perform necessary actions. This method is mostly used for debugging SwiftUI unnecessary draws so let’s take a look at Self._printChanges()

again you can’t just add the Self._printChanges() inside the body but you need to add let _ to satisfy the compiler.

struct DebuggingSwiftUIViewsWithPrintChanges: View {
@State var isTrue = true
var body: some View {

//add this on the view body
#if DEBUG
let _ = Self._printChanges()
#endif

VStack {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
Button(action: {
isTrue.toggle()
}) {
Text("Toggle")
.foregroundColor(.white)
.padding()
.padding(.horizontal)
.background(Capsule().fill(Color.blue))
}
if isTrue {
Text("istrue")
}else{
EmptyView()
}
}


}
}

if you run the code above you will get the following.

and your console will print the following so let's understand what it means.

these are the changes that have occurred in our SwiftUI view so let's see what they are.

@self means that the struct itself was changed which means that DebuggingSwiftUIViewsWithPrintChanges was created

@identity implies the creation of a new identity in the attributed graph.

_isTrue is the property that is changed

@self and @identity are interlinked in the creation of a view and identity is assigned on the attributed graph for that particular view, i will talk about this in detail in some other article.

if you tap on the toggle button you will notice only the property _isTrue changes so SwiftUI keeps track of the dynamic properties that changes and redraws only those views that uses them. This way we can track properties that causes unnecessary redraws and refactor them and improve our performance. I will talk more about it in another article.

3- Using BreakPoint

  • you can use Self._printChanges() inside the breakpoint to see what changed in that particular iteration
  • you can also debug the properties used inside the view even their projected and wrapped values.

4- Using View Hierarchy

you can click on individual views to inspect their properties and layout constraints.

5- using Mirror API

you can use the Mirror API in Swift to inspect the type of an object, you can access the subjectType property of a Mirror instance. This property provides the type of the object being reflected.

I use it to inspect the type of my SwiftUI views

 func debugType() -> Self {
let type = Mirror(reflecting: self).subjectType
print(type)
return self
}

here you can see the vstack has some complex type underneath.

References:

--

--

abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning