1

I want to replicate this in Swift.. reading further: How to draw a linear gradient arc with Qt QPainter?

I've figured out how to draw a gradient inside of a rectangular view:

override func drawRect(rect: CGRect) {
        let currentContext = UIGraphicsGetCurrentContext()
        CGContextSaveGState(currentContext)
        let colorSpace = CGColorSpaceCreateDeviceRGB()

        let startColor = MyDisplay.displayColor(myMetric.currentValue)
        let startColorComponents = CGColorGetComponents(startColor.CGColor)
        let endColor = MyDisplay.gradientEndDisplayColor(myMetric.currentValue)
        let endColorComponents = CGColorGetComponents(endColor.CGColor)

        var colorComponents
            = [startColorComponents[0], startColorComponents[1], startColorComponents[2], startColorComponents[3], endColorComponents[0], endColorComponents[1], endColorComponents[2], endColorComponents[3]]

        var locations: [CGFloat] = [0.0, 1.0]
        let gradient = CGGradientCreateWithColorComponents(colorSpace, &colorComponents, &locations, 2)

        let startPoint = CGPointMake(0, 0)
        let endPoint = CGPointMake(1, 8)

        CGContextDrawLinearGradient(currentContext, gradient, startPoint, endPoint, CGGradientDrawingOptions.DrawsAfterEndLocation)
        CGContextRestoreGState(currentContext)
    }

and how to draw an arc

func drawArc(color: UIColor, x: CGFloat, y: CGFloat, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, lineWidth: CGFloat, clockwise: Int32) {
        let context = UIGraphicsGetCurrentContext()
        CGContextSetLineWidth(context, lineWidth)
        CGContextSetStrokeColorWithColor(context,
                                         color.CGColor)
        CGContextAddArc(context, x, y, radius, startAngle, endAngle, clockwise)
        CGContextStrokePath(context)
    }

With the gradient, that took up the entire view. I was unsure how to limit the gradient to a particular area so I made a subclass of UIView, modified the frame and used the gradient code (first snippet).

For arcs, I've used the second snippet. I'm thinking I can draw an arc and then draw a circle at the end of the arc to smooth out the arc with a rounded finish. How can I do this and draw a gradient over the arc shape?

Beginning of gradient arc End of gradient arc

Please ignore the purple mark. The first picture is the beginning of the gradient arc from my specification, the second is the end of the gradient arc, below and to the left of the beginning arc.

How can I draw this in Swift with Core Graphics?

Community
  • 1
  • 1
quantumpotato
  • 9,637
  • 14
  • 70
  • 146

2 Answers2

4

Exact code for 2019 with true circular gradient and totally flexible end positions:

These days basically you simply have a gradient layer and mask for that layer.

So step one, you need a "mask".

What the heck is a "mask" in iOS?

In iOS, you actually use a CAShapeLayer to make a mask.

This is confusing. It should be called something like 'CAShapeToUseAsAMaskLayer'

But that's how you do it, a CAShapeLayer.

Step two. In layout, you must resize the shape layer. And that's it.

So basically it's just this:

class SweetArc: UIView {
    
    open override func layoutSubviews() {
        super.layoutSubviews()
        arcLayer.frame = bounds
        gradientLayer.frame = bounds
    }
    
    private lazy var gradientLayer: CAGradientLayer = {
        let l = CAGradientLayer()
        ...
        l.mask = arcLayer
        layer.addSublayer(l)
        return l
    }()
    
    private lazy var arcLayer: CAShapeLayer = {
        let l = CAShapeLayer()
        ...
        return l
    }()
}

That's the whole thing.

Note that the mask of the gradient layer is indeed set as the shape layer, which we have named "arcLayer".

A shape layer is used as a mask.

Here's the exact code for the two layers:

private lazy var gradientLayer: CAGradientLayer = {
    // recall that .conic is finally available in 12 !
    let l = CAGradientLayer()
    l.type = .conic
    l.colors = [
        UIColor.yellow,
        UIColor.red
        ].map{$0.cgColor}
    l.locations = [0,  1]
    l.startPoint = CGPoint(x: 0.5, y: 0.5)
    l.endPoint = CGPoint(x: 0.5, y: 0)
    l.mask = arcLayer
    layer.addSublayer(l)
    return l
}()

private lazy var arcLayer: CAShapeLayer = {
    let l = CAShapeLayer()
    l.path = arcPath
    
    // to use a shape layer as a mask, you mask with white-on-clear:
    l.fillColor = UIColor.clear.cgColor
    l.strokeColor = UIColor.white.cgColor
    
    l.lineWidth = lineThick
    l.lineCap = CAShapeLayerLineCap.round
    l.strokeStart = 0.4 // try values 0-1
    l.strokeEnd = 0.5 // try values 0-1
    return l
}()

Again, notice arcLayer is a shape layer you are using as a mask - you do NOT actually add it as a subview.

Remember that shape layers used as masks do not! get added as subviews.

That can be a source of confusion. It's not a "layer" at all.

Finally, notice arcLayer uses a path arcPath.

Construct the path perfectly so that strokeStart and strokeEnd work correctly.

You want to build the path perfectly.

So that it is very easy to set the stroke start and stroke end values.

And they make sense by perfectly and correctly running clockwise, from the top, from 0 to 1.

Here's the exact code for that:

private let lineThick: CGFloat = 10.0
private let outerGap: CGFloat = 10.0
private let beginFraction: CGFloat = 0  // 0 means from top; runs clockwise

private lazy var arcPath: CGPath = {
    let b = beginFraction * .pi * 2.0
    return UIBezierPath(
        arcCenter: bounds.centerOfCGRect(),
        radius: bounds.width / 2.0 - lineThick / 2.0 - outerGap,
        startAngle: .pi * -0.5 + b,
        endAngle: .pi * 1.5 + b,
        clockwise: true
    ).cgPath
}()

You're done!

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
3
override func draw(_ rect: CGRect) {

    //Add gradient layer
    let gl = CAGradientLayer()
    gl.frame = rect
    gl.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
    layer.addSublayer(gl)

    //create mask in the shape of arc
    let sl = CAShapeLayer()
    sl.frame = rect
    sl.lineWidth = 15.0
    sl.strokeColor = UIColor.red.cgColor
    let path = UIBezierPath()
    path.addArc(withCenter: .zero, radius: 250.0, startAngle: 10.0.radians, endAngle: 60.0.radians, clockwise: true)
    sl.fillColor = UIColor.clear.cgColor
    sl.lineCap = kCALineCapRound
    sl.path = path.cgPath

    //Add mask to gradient layer
    gl.mask = sl

}

An arc drawn with gradient

Hope this helps!!

Alfred
  • 136
  • 8