7

I am working on a task in which I must create a circle with no fill, but a gradient stroke. For reference, here is the end result I am after;

Circle with gradient stroke

Given other occurrences with the app, I am drawing my circle like so;

let c = UIGraphicsGetCurrentContext()!
c.saveGState()
let clipPath: CGPath = UIBezierPath(roundedRect: converted_rect, cornerRadius: converted_rect.width / 2).cgPath
c.addPath(clipPath)
c.setLineWidth(9.0)
c.setStrokeColor(UIColor.blue.cgColor)
c.closePath()
c.strokePath()
c.restoreGState()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

This results in a circle with a blue stroke. Despite many searches around SO, I'm struggling to figure out how I'd replace that setStrokeColor with a gradient, rather than a blue color. My most success came from creating a CAGradientLayer, then masking it with a CAShapeLayer created from the path, but I was only able to create a filled circle, not a hollow circle.

Thank you!

Community
  • 1
  • 1
ZbadhabitZ
  • 2,753
  • 1
  • 25
  • 45
  • Would a gradient layer with an appropriate mask work in this instance? – solenoid Aug 13 '17 at 17:38
  • @solenoid I tried that with limited success. That got me to a filled circle with a gradient, but not a circle with a gradient stroke. – ZbadhabitZ Aug 13 '17 at 17:38
  • Have you reviewed [these search results](https://stackoverflow.com/search?q=%5Bswift%5D+stroke+gradient)? – rmaddy Aug 13 '17 at 17:40
  • Well, the mask would be kind of 2 paths - one inside one outside – solenoid Aug 13 '17 at 17:41
  • @rmaddy I have. Where I can't seem to convert the logic is how to draw any sort of circle/gradient in context, rather than adding to a view's layer, which is impractical in my case. – ZbadhabitZ Aug 13 '17 at 17:41
  • https://medium.com/swift-programming/how-to-create-an-angle-gradient-border-in-swift-f4856dde4c90 – solenoid Aug 13 '17 at 17:44

1 Answers1

8

The basic idea is to use your path as a clipping path, then draw the gradient.

let c = UIGraphicsGetCurrentContext()!
let clipPath: CGPath = UIBezierPath(ovalIn: converted_rect).cgPath

c.saveGState()
c.setLineWidth(9.0)
c.addPath(clipPath)
c.replacePathWithStrokedPath()
c.clip()

// Draw gradient
let colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
let offsets = [ CGFloat(0.0), CGFloat(1.0) ]
let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: offsets)
let start = converted_rect.origin
let end = CGPoint(x: converted_rect.maxX, y: converted_rect.maxY)
c.drawLinearGradient(grad!, start: start, end: end, options: [])

c.restoreGState()

let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Setup a CGGradient first with the desired colors. Then for a linear gradient you use drawLinearGradient. For a radial gradient, use drawRadialGradient.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • I have given this a try but it results in nothing actually being drawn. How does the linear gradient understand that it has any correlation to the clip path? – ZbadhabitZ Aug 13 '17 at 18:19
  • It's all in how you setup the `CGGradient` and draw it with `drawLinearGradient` or `drawRadialGradient`. – rmaddy Aug 13 '17 at 18:20
  • Bingo, @rmaddy! This worked perfectly. Thank you for the complete answer, that exactly accomplished what I was after. You rock! – ZbadhabitZ Aug 13 '17 at 19:22
  • Great answer! Helps me so much! – hamsternik Nov 29 '17 at 01:11