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
CodePudding user response:
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: