[SOLVED] SwiftUI: Publish updates to custom shape

Issue

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
    }
}

Solution

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

Answered By – jnpdx

Answer Checked By – Senaida (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.