Not really, but you can just stroke a series of arcs in different colors:
import Cocoa
/// This draws an arc, of length `maxAngle`, ending at `endAngle. This is `@IBDesignable`, so if you
/// put this in a separate framework target, you can use this class in Interface Builder. The only
/// property that is not `@IBInspectable` is the `lineCapStyle` (as IB doesn't know how to show that).
///
/// If you want to make this animated, just use a `CADisplayLink` update the `endAngle` property (and
/// this will automatically re-render itself whenever you change that property).
@IBDesignable class GradientArcView: NSView {
/// Width of the stroke.
@IBInspectable var lineWidth: CGFloat = CGFloat(3) { didSet { setNeedsDisplay(bounds) } }
/// Color of the stroke (at full alpha, at the end).
@IBInspectable var strokeColor: NSColor = NSColor.blue { didSet { setNeedsDisplay(bounds) } }
/// Where the arc should end, measured in degrees, where 0 = "3 o'clock".
@IBInspectable var endAngle: CGFloat = 0 { didSet { setNeedsDisplay(bounds) } }
/// What is the full angle of the arc, measured in degrees, e.g. 180 = half way around, 360 = all the way around, etc.
@IBInspectable var maxAngle: CGFloat = 360 { didSet { setNeedsDisplay(bounds) } }
/// What is the shape at the end of the arc.
var lineCapStyle: NSLineCapStyle = .squareLineCapStyle { didSet { setNeedsDisplay(bounds) } }
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let gradations = 255
let startAngle = -endAngle + maxAngle
let center = NSPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
let radius = (min(bounds.size.width, bounds.size.height) - lineWidth) / 2
var angle = startAngle
for i in 1 ... gradations {
let percent = CGFloat(i) / CGFloat(gradations)
let endAngle = startAngle - percent * maxAngle
let path = NSBezierPath()
path.lineWidth = lineWidth
path.lineCapStyle = lineCapStyle
path.appendArc(withCenter: center, radius: radius, startAngle: angle, endAngle: endAngle, clockwise: true)
strokeColor.withAlphaComponent(percent).setStroke()
path.stroke()
angle = endAngle
}
}
}
Since your path is a circle, what you are asking for amounts to an angular gradient, that is, a sort of pie that changes color as we sweep a radius round the pie. There is no built-in way to do that, but there's a great library that does it for you:
The trick is that you draw your angular gradient with its center at the center of your circle, and then put a mask over it so that it appears only where your circle stroke is supposed to be.
Unfortunately not. There are only linear and radial gradients; no angular gradient support is available at the CG layer (or above).
This is quite annoying as Apple has started using these gradients quite heavily, for example in the Watch Activities circles, or in the Bedtime area of the timer app.
There are hacks where you draw thousands of lines from the outer edge of your circle to the center, and then mask them out to your shape. This works but is quite slow (frame drops when scrolling on-screen, when I tried it).
SpriteKit looks most promising: SKShader has some useful uniforms (path length & current distance along path) - dropping an SKView into your app is easy. (My designer dropped the gradient request before I had a chance to try this!)
Alternatively, you could use Metal (MTKView) or Core Image (CIFilter) and write your own shader. Unfortunately using Metal will mean no more working in the iOS Simulator, so CI would be the more realistic solution of the two.
The Donut view starts off by creating a base track, clipView, rotateView and a radialGradient imageView. It then calculates 360 colours between the two colours you want to use in the donut. It does so by incrementing the rgb values between the colours. Then a radial gradient image is created using those colours and added to the imageView. Because I wanted to use kCALineCapRound, I added a dot to cover up where the two colours meet. The whole thing needs to be rotated by -90 degrees to put it in the 12 O'Clock position. Then a mask is applied to the view, giving it the donut shape.
As the strokeEnd of the mask is changed, the view beneath it 'rotateView' is rotated as well. This gives the impression that the line is growing / shrinking as long as they in are in sync.
Not really, but you can just stroke a series of arcs in different colors:
Since your path is a circle, what you are asking for amounts to an angular gradient, that is, a sort of pie that changes color as we sweep a radius round the pie. There is no built-in way to do that, but there's a great library that does it for you:
https://github.com/paiv/AngleGradientLayer
The trick is that you draw your angular gradient with its center at the center of your circle, and then put a mask over it so that it appears only where your circle stroke is supposed to be.
Unfortunately not. There are only linear and radial gradients; no angular gradient support is available at the CG layer (or above).
This is quite annoying as Apple has started using these gradients quite heavily, for example in the Watch Activities circles, or in the Bedtime area of the timer app.
There are hacks where you draw thousands of lines from the outer edge of your circle to the center, and then mask them out to your shape. This works but is quite slow (frame drops when scrolling on-screen, when I tried it).
SpriteKit looks most promising: SKShader has some useful uniforms (path length & current distance along path) - dropping an SKView into your app is easy. (My designer dropped the gradient request before I had a chance to try this!)
Alternatively, you could use Metal (MTKView) or Core Image (CIFilter) and write your own shader. Unfortunately using Metal will mean no more working in the iOS Simulator, so CI would be the more realistic solution of the two.
Here is some code that worked for me. There's animations in it, but you can use the same principle to make a strokeEnd with a gradient.
A. Created a custom view 'Donut' and put this in the header:
B. Then did the basic view setup and wrote these two methods:
C. Create view:
D. Animate view:
E. Explanation:
The Donut view starts off by creating a base track, clipView, rotateView and a radialGradient imageView. It then calculates 360 colours between the two colours you want to use in the donut. It does so by incrementing the rgb values between the colours. Then a radial gradient image is created using those colours and added to the imageView. Because I wanted to use kCALineCapRound, I added a dot to cover up where the two colours meet. The whole thing needs to be rotated by -90 degrees to put it in the 12 O'Clock position. Then a mask is applied to the view, giving it the donut shape.
As the strokeEnd of the mask is changed, the view beneath it 'rotateView' is rotated as well. This gives the impression that the line is growing / shrinking as long as they in are in sync.
You might also need this: