SwiftUI Pie Chart: glitches when rotating the device

Viewed 97

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.

enter image description here

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:

enter image description here

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
    }
}
0 Answers
Related