Search code examples
iosswiftuilinear-gradientsswiftui-charts

Cannot convert value of type 'LinearGradient' to expected argument type 'GraphicsContext.Shading' in SwiftUI


I am trying to create a donut pie chart which has gradient colors.something like this

This is how I am trying to achieve this

struct Pie: View {
@State private var slices: [(Double, LinearGradient)]
var goldValue: Double
var silverValue: Double
var platinumValue: Double
var palladiumValue: Double

init(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) {
    self._slices = State(initialValue: Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue))
    self.goldValue = goldValue
    self.silverValue = silverValue
    self.platinumValue = platinumValue
    self.palladiumValue = palladiumValue
}

private static func calculateSlices(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) -> [(Double, LinearGradient)] {
    let sum = goldValue + silverValue + platinumValue + palladiumValue
    
    let goldGradient = LinearGradient(gradient: Gradient(colors: [.yellow, .orange]), startPoint: .topLeading, endPoint: .bottomTrailing)
    let silverGradient = LinearGradient(gradient: Gradient(colors: [.gray, .blue]), startPoint: .topLeading, endPoint: .bottomTrailing)
    let platinumGradient = LinearGradient(gradient: Gradient(colors: [.green, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)
    let palladiumGradient = LinearGradient(gradient: Gradient(colors: [.white, .pink]), startPoint: .topLeading, endPoint: .bottomTrailing)
    
    return [
        (sum != 0 ? goldValue / sum : 0, goldGradient),
        (sum != 0 ? silverValue / sum : 0, silverGradient),
        (sum != 0 ? platinumValue / sum : 0, platinumGradient),
        (sum != 0 ? palladiumValue / sum : 0, palladiumGradient)
    ]
}

var body: some View {
    Canvas { context, size in
        // Start Donut
        let donut = Path { p in
            let thickness: CGFloat = 20

            p.addEllipse(in: CGRect(origin: .zero, size: size))
            p.addEllipse(in: CGRect(
                x: thickness,
                y: thickness,
                width: size.width - 2 * thickness,
                height: size.height - 2 * thickness
            ))
        }
        context.clip(to: donut, style: .init(eoFill: true))
        // End Donut

        context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
        var pieContext = context
        pieContext.rotate(by: .degrees(-90))
        let radius = min(size.width, size.height) * 0.48

        var startAngle = Angle.zero
        for (value, gradient) in slices {
            let angle = Angle(degrees: 360 * value)
            let endAngle = startAngle + angle
            let path = Path { p in
                p.move(to: .zero)
                p.addArc(center: .zero, radius: radius, startAngle: startAngle + Angle(degrees: 0) / 2, endAngle: endAngle, clockwise: false)
                p.closeSubpath()
            }
            pieContext.fill(path, with: gradient)
            startAngle = endAngle
        }
    }
    .aspectRatio(contentMode: .fit)
    .onAppear {
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: goldValue) { newValue in
        slices = Pie.calculateSlices(goldValue: newValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: silverValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: newValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: platinumValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: newValue, palladiumValue: palladiumValue)
    }
    .onChange(of: palladiumValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: newValue)
    }
}

}

In the line

pieContext.fill(path, with: gradient)

I get this error

Cannot convert value of type 'LinearGradient' to expected argument type 'GraphicsContext.Shading'

I originally had this code of the donut pie chart which works perfectly fine but it doesn't use gradient colors.

struct Pie: View {
@State private var slices: [(Double, Color)]
var goldValue: Double
var silverValue: Double
var platinumValue: Double
var palladiumValue: Double

init(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) {
    self._slices = State(initialValue: Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue))
    self.goldValue = goldValue
    self.silverValue = silverValue
    self.platinumValue = platinumValue
    self.palladiumValue = palladiumValue
}

private static func calculateSlices(goldValue: Double, silverValue: Double, platinumValue: Double, palladiumValue: Double) -> [(Double, Color)] {
    let sum = goldValue + silverValue + platinumValue + palladiumValue
    return [
        (sum != 0 ? goldValue / sum : 0, Color.yellow),
        (sum != 0 ? silverValue / sum : 0, Color.gray),
        (sum != 0 ? platinumValue / sum : 0, Color.green),
        (sum != 0 ? palladiumValue / sum : 0, Color.white)
    ]
}

var body: some View {
    Canvas { context, size in
        // Start Donut
        let donut = Path { p in
            let thickness: CGFloat = 20

            p.addEllipse(in: CGRect(origin: .zero, size: size))
            p.addEllipse(in: CGRect(
                x: thickness,
                y: thickness,
                width: size.width - 2 * thickness,
                height: size.height - 2 * thickness
            ))
        }
        context.clip(to: donut, style: .init(eoFill: true))
        // End Donut

        context.translateBy(x: size.width * 0.5, y: size.height * 0.5)
        var pieContext = context
        pieContext.rotate(by: .degrees(-90))
        let radius = min(size.width, size.height) * 0.48

        var startAngle = Angle.zero
        for (value, color) in slices {
            let angle = Angle(degrees: 360 * value)
            let endAngle = startAngle + angle
            let path = Path { p in
                p.move(to: .zero)
                p.addArc(center: .zero, radius: radius, startAngle: startAngle + Angle(degrees: 0) / 2, endAngle: endAngle, clockwise: false)
                p.closeSubpath()
            }
            pieContext.fill(path, with: .color(color))
            startAngle = endAngle
        }
    }
    .aspectRatio(contentMode: .fit)
    .onAppear {
                slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: goldValue) { newValue in
        slices = Pie.calculateSlices(goldValue: newValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: silverValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: newValue, platinumValue: platinumValue, palladiumValue: palladiumValue)
    }
    .onChange(of: platinumValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: newValue, palladiumValue: palladiumValue)
    }
    .onChange(of: palladiumValue) { newValue in
        slices = Pie.calculateSlices(goldValue: goldValue, silverValue: silverValue, platinumValue: platinumValue, palladiumValue: newValue)
    }
    
}

}

I would just like the similar functionality with gradient colours.


Solution

  • GraphicsContext doesn't take ShapeStyles like a lot of the view modifiers do. It takes a GraphicsContext.Shading, which can be created by using one of the factory methods. For example, in your last code snippet, you've used .color(color) to create a solid color shading. You can't directly pass color to it.

    There is a factory method for creating linear gradients too: linearGradient. Unlike the ShapeStyle, this method takes the start and end points in the coordinate space of the canvas, instead of UnitPoints.

    I would first change calculateSlices to return a [(Double, Gradient)], and change the type of the slices state accordingly. So you'd do:

    let goldGradient = Gradient(colors: [.yellow, .orange])
    let silverGradient = Gradient(colors: [.gray, .blue])
    let platinumGradient = Gradient(colors: [.green, .purple])
    let palladiumGradient = Gradient(colors: [.white, .pink])
    

    You would calculate the start and end points in the Canvas closure.

    Depending on what you want, you can either do:

    // use the top leading and bottom trailing points of the path's bound rect
    let start = CGPoint(x: arc.boundingRect.maxX, y: arc.boundingRect.minY)
    let end = CGPoint(x: arc.boundingRect.minX, y: arc.boundingRect.maxY)
    pieContext.fill(path, with: .linearGradient(gradient, startPoint: start, endPoint: end))
    

    Or

    // use the top leading and bottom trailing points of the canvas
    let start = CGPoint(x: -size.width / 2, y: -size.height / 2)
        // also need to rotate these points since the context is rotated
        .applying(.init(rotationAngle: -.pi / 2))
    let end = CGPoint(x: size.width / 2, y: size.height / 2)
        .applying(.init(rotationAngle: -.pi / 2))
    pieContext.fill(path, with: .linearGradient(gradient, startPoint: start, endPoint: end))