I was playing around with @resultBuilder
and made a simple result builder called SumBuilder
which takes a bunch of integers and adds them all up.
@resultBuilder
struct SumBuilder {
static func buildBlock(_ components: Int...) -> Int {
components.reduce(0, +)
}
}
Since I was also playing around with shapes the other day I thought about creating a custom shape which can use this new result builder in its implementation. The custom shape just draws a line from the origin of the container to the point specified.
struct CustomShape: Shape {
@SumBuilder let sum: () -> Int
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: .zero)
path.addLine(to: CGPoint(x: sum(), y: sum()))
}
}
}
When I hooked up sum
to a @State
variable in my ContentView
it works perfectly. However, when I change sum
using .onAppear()
, the value of sum
will change under the hood but the View
doesn’t update to reflect that:
struct ContentView: View {
@State private var distance = 0
var body: some View {
VStack {
Button("Increase distance") {
distance += 100
}
CustomShape { distance }
.stroke()
.onAppear { distance = 100 } // << Updates distance, but the View still doesn't show
// the line until I press the Button to make distance = 200
}
}
}
I tried moving the .onAppear()
to the Text
thinking the .onAppear()
wasn’t being called since the CustomShape
is initially a dot, but that didn’t change anything. What’s even stranger is that when I transform the CustomShape
to a regular View
, the .onAppear()
works as intended.
Does anyone know how to make a Shape
which utilises @resultBuilder
update even when a value is changed using .onAppear()
? If you can help in any shape or form I’d be very grateful.
Thanks in advance!
P.S. This is only the least amount of code required to recreate the problem. In my real app I’m initialising a variable called endPoint
to be the centre using a GeometryProxy
. That’s why I can’t initialise distance
directly when I declare it as it would be conceptually different than what I’m actually trying to achieve.
2
Answers
As i mentioned, i just tried your code but using a ViewModel, which worked as expected. In production you probably want to use input-ouput-pattern but this is the least amount of code to explain what i meant by using a ViewModel.
In SwiftUI, the
.onAppear()
modifier is used to perform some action when a view appears on the screen. However, this modifier does not trigger a re-render of the view, so any changes made within the .onAppear() closure will not be reflected in the view’s appearance.This applies to shapes that use @resultBuilder as well. If a shape is created using @resultBuilder and you make changes to it within the
.onAppear()
closure, the changes will not be visible in the view. To update a shape that uses @resultBuilder you need to use.onChange()
or.onReceive()
instead of.onAppear()
to update the shape which will trigger the view to re-render.