Debugging techniques in SwiftUI
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
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: