I'm trying to implement an animated pie chart in SwiftUI. I noticed that my implementation has some glitches on iPad, specifically when the device is rotated.
As you can see, the pieces are not connected anymore when the device is rotated. Cause: The pieces update their frame size immediately.
I noticed that everything will work fine when drawingGroup() is used with the chart. Another possible approach is to set the frame of the chart explicitly with a .frame modifier (using GeometryReader for the size) and to animate the frame change with .animation(). However, those options are creating problems in the app switcher. When the app is closed and after that, the device is rotated, the preview of the app in the app switcher looks weird. The chart is not centered and the size is off as well:
So that's not an option unfortunately.
Also: The pie chart should have different sizes in portrait vs landscape mode (I simulated that with a padding here). Moreover, I want to be able to animate all pieces of the pie chart individually (the start and end angle) just like in the current implementation.
Hope somebody has an idea how to fix this. Thanks so much for answering!
import SwiftUI
struct ContentView: View {
@ObservedObject var model = CellModel.sharedInstance
var body: some View {
PieChart(dataModel: $model.sample)
//.drawingGroup()
.aspectRatio(1, contentMode: .fit)
.padding()
.padding(.vertical, 200)
}
}
struct PieChart: View {
@Binding var dataModel: [ChartEntry]
var body: some View {
ZStack{
ForEach(dataModel) { elem in
PieChartWrapper(elem: elem)
}
}
}
}
struct PieChartWrapper: View {
@ObservedObject var elem: ChartEntry
var body: some View {
PieChartCell(start: elem.startAngle, end: elem.endAngle)
.foregroundColor(elem.color)
}
}
let chartCoverage: CGFloat = 0.7
struct PieChartCell: Shape {
var start: Double
var end: Double
var animatableData: AnimatablePair<Double, Double> {
get { .init(start, end) }
set {
start = newValue.first
end = newValue.second
}
}
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: rect.size.width/2, y: rect.size.width/2)
let radius = rect.size.width/2
let startAngle = Angle(degrees: start)
let endAngle = Angle(degrees: end)
let width = radius * chartCoverage
p.addArc(center: center, radius: abs(width - radius), startAngle: startAngle, endAngle: endAngle, clockwise: false)
p.addArc(center: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: true)
p.closeSubpath()
return p
}
}
class CellModel: ObservableObject {
static let sharedInstance = CellModel()
@Published var sample = [ChartEntry(color: .red), ChartEntry(color: .yellow), ChartEntry(color: .green)] {
didSet{
calcAngles()
}
}
private init() {
calcAngles()
}
private func calcAngles() {
for elem in sample {
withAnimation() {
elem.startAngle = getStartAngle(elem: elem, dataModel: sample)
elem.endAngle = getEndAngle(elem: elem, dataModel: sample)
}
}
}
private func getStartAngle(elem: ChartEntry, dataModel: [ChartEntry]) -> Double {
return 360 * (Double(dataModel.firstIndex(of: elem) ?? 0) / Double(dataModel.count))
}
private func getEndAngle(elem: ChartEntry, dataModel: [ChartEntry]) -> Double {
return 360 * (Double((dataModel.firstIndex(of: elem) ?? 0) + 1) / Double(dataModel.count))
}
}
class ChartEntry: Identifiable, Equatable, ObservableObject{
static func == (lhs: ChartEntry, rhs: ChartEntry) -> Bool {
lhs.id == rhs.id
}
var id = UUID()
var color: Color
@Published var startAngle: Double = 360
@Published var endAngle: Double = 360
init(color: Color) {
self.color = color
}
}

