swiftuichartsdonut-chart

Border the Stroke a Circle in SwiftUI when using Trim?


My UI designer asked me to build this donut pie chart and I have been banging my head against the wall for days! It seems impossible. Is there a way to do this in SwiftUI or should I ask the designer for a design that's actually implementable?

enter image description here

I'm able to make the below, but then I'm stuck. I can't get the separation between each segment and I can't get the rounded corners. I thought about a border but that doesn't work. Any thoughts?: enter image description here

import SwiftUI

struct ContentView: View {
    @ObservedObject var charDataObj = ChartDataContainer()
    @State var indexOfTappedSlice = -1
    
    var body: some View {
        VStack {
            Spacer()
            
            ZStack {
                Circle()
                    .stroke(Color(hex: 0x2A3950), style: StrokeStyle(lineWidth: 100, lineCap: .round, lineJoin: .round))
                    .frame(width: 220, height: 270)
                
                ForEach(0..<charDataObj.chartData.count) { index in
                    Circle()
                        .trim(from: index == 0 ? 0.0 : charDataObj.chartData[index-1].value / 100,
                              to: (charDataObj.chartData[index].value / 100))
                        .stroke(charDataObj.chartData[index].color, style: StrokeStyle(lineWidth: 50.0, lineCap: .butt, lineJoin: .round))
                        .rotationEffect(Angle(degrees: 270.0))
                        .onTapGesture {
                            indexOfTappedSlice = indexOfTappedSlice == index ? -1 : index
                        }
                        .scaleEffect(index == indexOfTappedSlice ? 1.1 : 1.0)
                        .animation(.spring())
                }
            }.frame(width: 150, height: 150)
            
            Spacer()
        }
        .frame(maxWidth: .infinity)
        .background(Color(hex: 0x42506B))
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//MARK:- Chart Data
struct ChartData {
    var id = UUID()
    var color : Color
    var percent : CGFloat
    var value : CGFloat
}

@available(iOS 15.0, *)
class ChartDataContainer : ObservableObject {
    @Published var chartData =
    [ChartData(color: .green, percent: 26, value: 0),
     ChartData(color: .blue, percent: 28, value: 0),
     ChartData(color: .white, percent: 23, value: 0),
     ChartData(color: .yellow, percent: 12, value: 0),
     ChartData(color: .red, percent: 11, value: 0),]
    
    init() {
        calc()
    }
    func calc(){
        var value : CGFloat = 0
        
        for i in 0..<chartData.count {
            value += chartData[i].percent
            chartData[i].value = value
        }
    }
}

extension Color {
    init(hex: Int, opacity: Double = 1.0) {
        let red = Double((hex & 0xff0000) >> 16) / 255.0
        let green = Double((hex & 0xff00) >> 8) / 255.0
        let blue = Double((hex & 0xff) >> 0) / 255.0
        self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
    }
}

I can get some separation with an adjustment to the trim, but it looks awful:

.trim(from: index == 0 ? 0.003: (charDataObj.chartData[index-1].value / 100) + 0.003,
                              to: (charDataObj.chartData[index].value / 100) - 0.003)

enter image description here


Solution

  • This is now possible in iOS 17, and it's very easy:

    Chart(dailySales, id:\.day) { element in
        SectorMark(
            angle: .value("Sales", element.sales),
            innerRadius: .ratio(0.55),
            angularInset: 2
        )
        .cornerRadius(10)
        .foregroundStyle(element.color)
    }
    .rotationEffect(Angle(degrees: -35))