In the following SwiftUI code I have 3 main components:
TestView
- Holds a TestClass
instance, displays MyShape
, and updates the count of the TestClass
instance on tap.
TestClass
- A simple class to store a count
, which gets published when it's changed.
MyShape
- A simple shape whose position (should) change as the count of the TestClass
instance in TestView
changes.
The issue is that the position of MyShape
does not change when the count updates.
I know that the count is being updated because if I display the count in a Text view it changes on tap, so while the count is changing, the shape is not updating when the count changes. Why is the shape not updating / why is the publisher not reaching the shape?
struct TestView: View {
@ObservedObject var counter: TestClass = TestClass()
var body: some View {
ZStack {
MyShape(counter: counter)
.stroke(Color.red, lineWidth: 50)
.onTapGesture {
counter.count = 1
}
}
}
}
class TestClass: ObservableObject {
@Published var count = 0
}
struct MyShape: Shape {
var counter: TestClass
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: counter.count, y: counter.count))
return path
}
}
CodePudding user response:
SwiftUI only updates a View
or Shape
when its properties change.
Right now, MyShape
never changes its properties after the first render since TestClass
is a reference type (ie class
in this case) -- SwiftUI just sees that the properties are identical and doesn't bother to render a new version of MyShape
, even though one of the internal properties on TestClass
has changed.
Within a View
, we can use the @ObservedObject
property wrapper so that the View
knows to update when an ObservableObject
's @Published
properties change (like you do in TestView
), but that wrapper isn't available in a Shape
.
The easiest solution here is to pass count
instead of counter
. count
is an Int
(ie a value type), meaning whenever it changes, SwiftUI will know to update the Shape
:
struct TestView: View {
@ObservedObject var counter: TestClass = TestClass()
var body: some View {
ZStack {
MyShape(count: counter.count)
.stroke(Color.red, lineWidth: 50)
.onTapGesture {
counter.count = 1
}
}
}
}
class TestClass: ObservableObject {
@Published var count = 10
}
struct MyShape: Shape {
var count: Int
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: count, y: count))
return path
}
}
An alternate solution that may be fit the principals of SwiftUI a little better is to change your model into a struct
. Then, the model can be passed to the Shape
and since it's a value type, the Shape
will update. This is, of course, assuming that your real-world model is more complex and has more than one property that the Shape
will need -- otherwise, keeping the original solution of just exposing what it needs (ie the Int
) is probably a better one.
struct TestView: View {
@State var counter: TestStruct = TestStruct()
var body: some View {
ZStack {
MyShape(counter: counter)
.stroke(Color.red, lineWidth: 50)
.onTapGesture {
counter.count = 1
}
}
}
}
struct TestStruct {
var count = 10
}
struct MyShape: Shape {
var counter: TestStruct
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: counter.count, y: counter.count))
return path
}
}
Note: I updated the initial count
so that it was easier to tap the view at the beginning