12

I have a relatively straight forward implementation of a progress view set up with CALayer objects. The progress view itself is a subview of UIView.

Here is the code that sets up the progress ring:

        self.progressRingLayer = CAShapeLayer()

        let innerRect = CGRectInset(bounds, CGFloat(self.lineWidth) / 2, CGFloat(self.lineWidth) / 2)
        let innerPath = UIBezierPath(ovalInRect: innerRect)

        self.progressRingLayer.path = innerPath.CGPath
        self.progressRingLayer.fillColor = UIColor.clearColor().CGColor
        self.progressRingLayer.strokeColor = kProgressColor.CGColor
        self.progressRingLayer.anchorPoint = CGPointMake(0.5, 0.5)
        self.progressRingLayer.transform = CATransform3DRotate(self.progressRingLayer.transform, (CGFloat(M_PI))*1, 0, 0, 1)
        self.progressRingLayer.lineCap = kCALineCapRound
        self.progressRingLayer.lineWidth = CGFloat(self.lineWidth)

        self.layer.addSublayer(self.progressRingLayer)

What I am trying to do now is add a gradient to the progressRingLayer that follows (or bends with) the path. I have been successful in adding a linear gradient to the fill, but not to just the path.

Here is an example of what effect I want:

enter image description here

So far everything I have found requires a bunch of additional steps with CoreGraphics and CGContext that don't quite fit with my implementation. Any help would be great, thanks!

Kyle Begeman
  • 7,169
  • 9
  • 40
  • 58
  • Would using your path as a mask for your gradient layer suffice as the answer, or do you also want your gradient to bend with your path instead of be displayed as a masked linear gradient? – Ian MacDonald Jan 13 '15 at 20:41
  • I am looking to have the gradient bend to the path. Using a mask is one of the ways I accomplished the wrong effect. – Kyle Begeman Jan 13 '15 at 20:48
  • Could you draw a picture of what you want? – matt Jan 13 '15 at 20:49
  • @matt Updated the description with a sample screenshot of the effect I want. – Kyle Begeman Jan 13 '15 at 20:53
  • Well, that does look like a mask that reveals a gradient behind the view, as @IanMacDonald suggested. You can prepare the gradient image beforehand or you can draw it in code. Then you just "erase" an arc of your black drawing to make the gradient show through. – matt Jan 13 '15 at 22:34
  • No, I understand what he's asking and it's not as easy as just masking. He wants the gradient to follow the arc. As the arc fills more towards the top again, it would remain blue, not change back to purple. – Ian MacDonald Jan 14 '15 at 00:16
  • He has already solved the mask and is requesting a programatic solution for generating an angular gradient. See this image for a visual description of *angular gradient*: https://s3.amazonaws.com/uploads.uservoice.com/assets/072/355/377/original/gradient-test-1.jpg?AWSAccessKeyId=14D6VH0N6B73PJ6VE382&Expires=1484366244&Signature=7GPzq17FmCk6PNRgJXACb9997%2FY%3D – Ian MacDonald Jan 14 '15 at 16:12
  • See this question for another graphic, with a slightly more general case than simply circular. http://stackoverflow.com/questions/37794827/stroke-a-cgpath-with-a-gradient-along-its-length#comment63067354_37794827 – Graham Perks Jun 30 '16 at 19:43

1 Answers1

7

What I would do is draw a gradient layer, then draw on top of that a layer that is black with the arc erased.

Here's my attempt at roughly the image you provided (I omitted the white label in the center, but that's trivial):

enter image description here

And here's the code that generated it:

let r = CGRectMake(100,100,130,100)

let g = CAGradientLayer()
g.frame = r
let c1 = UIColor(
    red: 151.0/255.0, green: 81.0/255.0, blue: 227.0/255.0, alpha: 1)
let c2 = UIColor(
    red: 36.0/255.0, green: 176.0/255.0, blue: 233.0/255.0, alpha: 1)
g.colors = [c1.CGColor as AnyObject, c2.CGColor as AnyObject];
self.view.layer.addSublayer(g)

let percent = CGFloat(0.64) // percentage of circle
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let con = UIGraphicsGetCurrentContext()
CGContextFillRect(con, CGRect(origin: CGPoint(), size: r.size))
CGContextSetLineWidth(con, 5)
CGContextSetLineCap(con, kCGLineCapRound)
CGContextSetBlendMode(con, kCGBlendModeClear)
let pi = CGFloat(M_PI)
CGContextAddArc(con, r.size.width/2.0, r.size.height/2.0, 30, 
    -pi/2.0, -pi/2.0 + percent*pi*2.0, 0)
CGContextStrokePath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let b = CALayer()
b.frame = r
b.contents = im.CGImage
self.view.layer.addSublayer(b)

The gradient layer (the first part of the code) is just a "serving suggestion". If that is not the gradient you want, you can design your own. You could draw it in Photoshop and use an image as the content of the gradient layer. Or you could make an "angular" layer in code, using third-party code such as https://github.com/paiv/AngleGradientLayer. The point of the example is merely to show how it is possible to "erase" an arc in a black layer so as to reveal the gradient concealed behind it, and thus appear to paint with a gradient.

matt
  • 515,959
  • 87
  • 875
  • 1,141