Swift: Gradient along a bezier path (using CALayers)
Asked Answered
H

1

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!

Haig answered 13/1, 2015 at 20:37 Comment(8)
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?Raskin
I am looking to have the gradient bend to the path. Using a mask is one of the ways I accomplished the wrong effect.Haig
Could you draw a picture of what you want?Portugal
@Portugal Updated the description with a sample screenshot of the effect I want.Haig
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.Portugal
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.Raskin
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: s3.amazonaws.com/uploads.uservoice.com/assets/072/355/377/…Raskin
See this question for another graphic, with a slightly more general case than simply circular. #37795327Capitalism
P
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.

Portugal answered 13/1, 2015 at 23:4 Comment(4)
This doesn't quite do what the OP wants. As the progress fills up closer to 100%, your view would show it getting more purple again, where it should continue to be more blue at the "end" of the arc.Raskin
@IanMacDonald Added a paragraph about that to the answer.Portugal
Is there some way to add animation to the circle? I mean to draw the arc animatedAgalloch
@Portugal : How can i change background color from black to white ?Silly

© 2022 - 2024 — McMap. All rights reserved.