Debugging SwiftUI views: what caused that change? – onlinecode
In this post we will give you information about Debugging SwiftUI views: what caused that change? – onlinecode. Hear we will give you detail about Debugging SwiftUI views: what caused that change? – onlinecodeAnd how to use it also give you demo for it if it is necessary.
Debugging SwiftUI views is an essential skill to own when writing dynamic views with several redrawing triggers. Property wrappers like @State
and @ObservedObject
will redraw your view based on a changed value. In many cases, this is expected behavior, and things look like they should. However, in so-called Massive SwiftUI Views (MSV), there could be many different triggers causing your views to redraw unexpectedly.
Alright, I made up the MSV, but you probably get my point. In UIKit, we used to have so-called Massive View Controllers, which had too many responsibilities. You’ll learn through this article why I think it’s essential to prevent the same from happening when writing dynamic SwiftUI views. Let’s dive in!
What is a dynamic SwiftUI View?
A dynamic SwiftUI view redraws as a result of a changed observed property. An example could be a timer count view that updates a label with an updated count integer:
struct TimerCountView: View {
@State var count = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("Count is now: (count)!")
.onReceive(timer) { input in
count += 1
}
}
}
Every time the timer fires, the count will go up. Our view redraws due to the @State
attribute attached to the count property. The TimerCountView
is dynamic since its contents can change.
It’s good to look at a static view example to understand what I mean by dynamic fully. In the following view, we have a static text label that uses the input title string:
struct ArticleView: View {
let title: String
var body: some View {
Text(title)
}
}
You could argue whether it’s worth creating a custom view for this example, but it does demonstrate a simple example of a static view. Since the title
property is a static let; without any attributes, we can assume that this view will not change. Therefore, we can call this a static view. Debugging SwiftUI views that are static is likely not often needed.
The problem of a Massive SwiftUI View
A SwiftUI view with many redraw triggers can be a pain to work with. Each individual @State
, @ObservedObject
, or other triggers can cause your view to redraw and influence dynamics like a running animation. Knowing how to debug a SwiftUI view becomes especially handy in these cases.
For example, we could introduce an animated button known from Looki into our timer view. The animation starts on appear and rotates the button back and forth:
struct TimerCountView: View {
@State var count = 0
@State var animateButton = true
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("Count is now: (count)!")
.onReceive(timer) { input in
count += 1
}
Button {
} label: {
Text("SAVE")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(.white)
.padding(.vertical, 6)
.padding(.horizontal, 80)
.background(.red)
.cornerRadius(50)
.shadow(color: .secondary, radius: 1, x: 0, y: 5)
}.rotationEffect(Angle(degrees: animateButton ? Double.random(in: -8.0...1.5) : Double.random(in: 0.5...16)))
}.onAppear {
withAnimation(.easeInOut(duration: 1).delay(0.5).repeatForever(autoreverses: true)) {
animateButton.toggle()
}
}
}
}
Since both the timer and the animation are triggering a redraw of the same TimerCountView
, our resulting animation is not what we expected:
The random value for our rotation effect is changed on every view redraw. The timer and our boolean toggle trigger a redraw, causing the button to jump instead of animating smoothly.
The above example shows what a view with multiple states can cause, while our example was still relatively small. A view with more triggers can cause several of these side effects, which will make it hard to debug which trigger caused an issue.
Before I explain how to solve this, I’ll demonstrate a few techniques you can apply to find out the cause of a SwiftUI View redraw.
Using LLDB to debug a change
LLDB is our debugging tool available inside the Xcode console. It allows us to print objects using po object
and find out state while our application is paused by, for example, a breakpoint.
Swift provides us with a private static method Self._printChanges()
that prints out the trigger of a redraw. We can use it by setting a breakpoint in our SwiftUI body and typing po Self._printChanges()
inside the console:
As you can see, the console tells us the _count
property changed. Our SwiftUI view redraws since we observed our count property as a state value.
To thoroughly verify our count property is causing animation issues, we could temporarily disable the timer and rerun our app. You’ll see a smooth animation not causing any problems anymore.
This was just a simple debugging example. Using Self._printChanges()
can be helpful in cases you want to find out which state property triggered a redraw.
Solving redraw issues in SwiftUI
Before diving into other debugging techniques, I think it’s good to explain how to solve the above issue in SwiftUI. The LLDB debugging technique gave us enough to work with for now, and we should be able to extract the timer away from the button animation.
We can solve our issue by isolating redraw triggers into single responsible views. By isolating triggers, we will only redraw relevant views. In our case, we want to separate the button animation only to redraw the button when our animateButton
boolean toggles:
struct TimerCountFixedView: View {
@State var count = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("Count is now: (count)!")
.onReceive(timer) { input in
count += 1
}
AnimatedButton()
}
}
}
struct AnimatedButton: View {
@State var animateButton = true
var body: some View {
Button {
} label: {
Text("SAVE")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(.white)
.padding(.vertical, 6)
.padding(.horizontal, 80)
.background(.red)
.cornerRadius(50)
.shadow(color: .secondary, radius: 1, x: 0, y: 5)
}.rotationEffect(Angle(degrees: animateButton ? Double.random(in: -8.0...1.5) : Double.random(in: 0.5...16))).onAppear {
withAnimation(.easeInOut(duration: 1).delay(0.5).repeatForever(autoreverses: true)) {
animateButton.toggle()
}
}
}
}
Running the app with the above code will show a perfectly smooth animation while the count is still updating:
The timer no longer changes the rotation effect random value since SwiftUI is smart enough not to redraw our button for a count change. Another benefit of isolating our code into a separate AnimatedButton
view is that we can reuse this button in any other place in our app.
The view examples in this article are still relatively small. When working on an actual project, you can quickly end up with a view having lots of responsibilities and triggers. What works for me is to be aware of situations in which a custom view makes more sense. Whenever I’m creating a view builder property like:
var animatedButton: some View {
// .. define button
}
I ask myself the question of whether it makes more sense to instead create a:
struct AnimatedButton: View {
// .. define button
}
By applying this mindset, you’ll lower the chances of running into animation issues in the first place.
Debugging changes using code
Now that we know how debugging SwiftUI views works using the Self._printChanges()
method, we can look into other valuable ways of using this method. Setting a breakpoint like in the previous example only works when you know which view is causing problems. There could be cases that you have multiple affected views since they all monitor the same observed object.
Using code could be a solution since it does not require entering lldb commands after a breakpoint hits manually. The code change speeds up debug processes since it will constantly run while your views redraw. We can use this technique as follows:
var body: some View {
Self._printChanges()
return VStack {
// .. other changes
}
}
The above code changes will print out any changes to our view inside the console:
TimerCountView: @self, @identity, _count, _animateButton changed.
TimerCountView: _animateButton changed.
TimerCountView: _count changed.
TimerCountView: _count changed.
You might notice that we’re getting a few new statements logged inside the console. The @self
and @identity
are new, and you might wonder what they mean. Looking at the documentation of the _printChanges method we’ll get an explanation:
When called within an invocation of
body
of a view of this type, prints the names of the changed dynamic properties that caused the result ofbody
to need to be refreshed. As well as the physical property names, “@self” is used to mark that the view value itself has changed, and “@identity” to mark that the identity of the view has changed (i.e. that the persistent data associated with the view has been recycled for a new instance of the same type).
Conclusion
Debugging SwiftUI views is an essential skill to own when working with dynamic properties on a view. By using the _printChanges
static method, we allow ourselves to find the root cause of a redraw. We can often solve frustrating animation issues by isolating views into single responsible views.
If you like to improve your SwiftUI knowledge even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.
Thanks!
<!– Disable cross link to Medium
Also published on Medium.
–>
Hope this code and post will helped you for implement Debugging SwiftUI views: what caused that change? – onlinecode. if you need any help or any feedback give it in comment section or you have good idea about this post you can give it comment section. Your comment will help us for help you more and improve us. we will give you this type of more interesting post in featured also so, For more interesting post and code Keep reading our blogs