[SOLVED] PieChart in swiftui is not rendered when a member has @State modifier

Issue

I am creating an app and learning swift/swiftui together. My App displays piechart using Path object with ForEach loop.
My PieChart based on ForEach loop with Paths doesn’t appears when I change _percentages member from normal member to @State member.

class members:

    @State var data: [Float]
    @State var colors: [Color]
    @Binding var slicePressedNo: Int
    var angles: [Float] = []
    var offsets: [Float] = []
    var total: Float = 0
    private var _sortedData: [Float]
    @State private var _percentages: [Float] = [] // Removing @State fixes the issue and everything works as expected

init function:

init (data: [Float], colors: [Color], slicePressedNo: Binding<Int>) {
        self._data = State(initialValue: data)
        self._colors = State(initialValue: colors)
        self._slicePressedNo = slicePressedNo
        self._sortedData = data

        var sum: Float = 0.0
        for value in self.data {
            sum += value
        }

        self._percentages.removeAll()
        for value in self.data {
            self._percentages.append((value / sum))
            print("sum = \(sum)")
            print("value = \(value)")
        }

        angles.removeAll()
        offsets.removeAll()
        offsets.append(0)
        for value in self._percentages {
            let angle = value * 360
            angles.append(angle)
            offsets.append(angle + offsets.last!)
        }

        for i in self._percentages.indices {
            self._percentages[i] = self._percentages[i] * 100.0
            print("% = \(self._percentages[i])")
        }

        total = sum
    }

Code responsible to build piechart slices:

GeometryReader { geometry in
                var offset: Float = 0.0

                let width: CGFloat = min(geometry.size.width, geometry.size.height)
                let height = width
                let center = CGPoint(x: width * 0.5, y: height * 0.5)


                ForEach (angles.indices, id: \.self) { i in
                    withAnimation(.easeIn) {
                        Path { path in
                            path.move(to: center)
                            path.addArc(
                                center: center,
                                radius: width * 0.4,
                                startAngle: Angle(degrees:  Double(offsets[i])),
                                endAngle: Angle(degrees: Double(offsets[i] + angles[i])),
                                clockwise: false)
//                            print("i = \(angles[i])")
                            offset += angles[i]
                        }
                    }
                    .fill(slicePressedNo != i ? colors[i] : .yellow)
                }
...

_percantages member is used in the class but below PieChart building part:

ForEach (angles.indices, id: \.self) { i in
                    if (_percentages[i] >= 2) {
                        PieChartSliceValueView(label: /*String(format: "%.2f",*/ $_percentages[i], x: center.x, y: center.y, angle: Double(offsets[i] + (angles[i] / 2.0)), radius: width * 0.32)
                    }
                }

Everything works until I change _percentages (or other members) from regular member to @State members. If I change it, ForEach loops doesn’t work – nothing triggers to render it? or the change makes angles table empty? Probably I missed something in @State variable understanding.

Thank you in advance
Ɓukasz

Solution

First of all don’t use _ in var names. You only need them to access underlying values e.g. in the init.

Second I’m confused about your mention of class as I don’t see any in your code.

I would put the PieView in an extra view that only does that: displaying a pie based on data handed over.
This data and the colors don’t have to be state inside the view. Only things that can change inside the view have to be @State. Your slicePressedNo might have to be @Binding as you would detect this inside the view on tap. All other can be simple vars or even lets.

So the PieView could look like this:

struct PieView: View {
    
    var data: [Float]
    var colors: [Color]
    @Binding var slicePressedNo: Int
    
    private var angles: [Float] = []
    private var offsets: [Float] = []
    var total: Float = 0
    private var sortedData: [Float]
    private var percentages: [Float] = []
    
    init (data: [Float], colors: [Color], slicePressedNo: Binding<Int>) {
        self.data = data
        self.colors = colors
        self._slicePressedNo = slicePressedNo // only _ here
        self.sortedData = data
        
        // ... your code following

Then the calling view can use @State for changing values, which it passes down to PieView. Once these values change, PieView will be redrawn.

Here is a simple example which adds new values on button press:

struct ContentView: View {
    
    @State private var pressed = 1
    @State private var data: [Float] = [1, 5, 3, 8]
    @State private var colors: [Color] = [.blue, .red, .green, .orange, .teal]
    
    var body: some View {
        VStack {
            PieView(data: data,
                    colors: colors,
                    slicePressedNo: $pressed)
            
            Button("Add slice") {
                colors.append(colors.randomElement() ?? .red)
                data.append(Float.random(in: 0..<15))
            }
        }
    }
}

Here is the result:

enter image description here

Answered By – ChrisR

Answer Checked By – Pedro (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.