Drawing A Circle With Gradient Stroke - ios

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;
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!

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.

Related

Masking a UIEffectView with intersecting UIBezierPath

I wanted to apply a UIEffectView with blur over a tableView but have circular UIImageView objects in each cell show through. I used a mask and adapted the solution from this answer to create a method that would iteratively cut out circles above each cell:
func cutCircle(inView view: UIView, rect1: CGRect, rect2: CGRect?) {
// Create new path and mask
let newMask = CAShapeLayer()
// Create path to clip
let newClipPath = UIBezierPath(rect: view.bounds)
let path1 = UIBezierPath(ovalIn: rect1)
newClipPath.append(path1)
if let rect2 = rect2 {
let path2 = UIBezierPath(ovalIn: rect2)
//FIXME: Need a way to get a union of both paths!
//newClipPath.append(path2)
}
// If view already has a mask
if let originalMask = view.layer.mask, let originalShape = originalMask as? CAShapeLayer, let originalPath = originalShape.path {
// Create bezierpath from original mask's path
let originalBezierPath = UIBezierPath(cgPath: originalPath)
// Append view's bounds to "reset" the mask path before we re-apply the original
newClipPath.append(UIBezierPath(rect: view.bounds))
// Combine new and original paths
newClipPath.append(originalBezierPath)
}
// Apply new mask
newMask.path = newClipPath.cgPath
newMask.fillRule = .evenOdd
view.layer.mask = newMask
}
This function is called on the UIEffectView for each visible tableview cell using: for cell in tableView.visibleCells(). It appends a new circle to the mask.
However, some items have a smaller circle icon overlay, like this:
I added the second CGRect parameter to the method above to conditionally cut out this circle. However, the mask remains intact where the two circles overlap, like this:
I looked at a few answers here, as I needed to find a way to get the union of two UIBezierPath objects. However, this proved very difficult. I don’t think I can use a drawing context as this is a UIEffectView and the mask needs to be cut iteratively.
I tried changing the fill rules (.evenOdd, .nonZero) but this does not have the desired effect.
Are there any tips for combining two overlapping UIBezierPath into a single mask?
The overall aim is to achieve this effect with consecutive tableview cells, but some icons will have the extra circle.
Notice how the bottom icon has the extra circle but it is cropped, and my current technique to cut out this extra circle causes the problem noted above, where the overlap is not masked as expected.
You could use addArcWithCenter method to combine two arcs into desired shape.eg:
#define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)
- (UIBezierPath*)createPath
{
UIBezierPath* path = [[UIBezierPath alloc]init];
[path addArcWithCenter:CGPointMake(25, 25) radius:25 startAngle:DEGREES_TO_RADIANS(30) endAngle:DEGREES_TO_RADIANS(60) clockwise:false];
[path addArcWithCenter:CGPointMake(40, 40) radius:10 startAngle:DEGREES_TO_RADIANS(330) endAngle:DEGREES_TO_RADIANS(120) clockwise:true];
[path closePath];
return path;
}
I wasn't able to find out the exact points but if you could adjust the constants you could create the perfect shape required.
try the following code in your function
let newRect: CGRect
if let rect2 = rect2{
let raw = rect1.union(rect2)
let size = max(raw.width, raw.height)
newRect = CGRect(x: raw.minX, y: raw.minX, width: size, height: size)
}else{
newRect = rect1
}
let path1 = UIBezierPath(ovalIn: newRect)
newClipPath.append(path1)
Full function
func cutCircle(inView view: UIView, rect1: CGRect, rect2: CGRect?) {
// Create new path and mask
let newMask = CAShapeLayer()
// Create path to clip
let newClipPath = UIBezierPath(rect: view.bounds)
let newRect: CGRect
if let rect2 = rect2{
let raw = rect1.union(rect2)
let size = max(raw.width, raw.height) // getting the larger value in order to draw a proper circle
newRect = CGRect(x: raw.minX, y: raw.minX, width: size, height: size)
}else{
newRect = rect1
}
let path1 = UIBezierPath(ovalIn: newRect)
newClipPath.append(path1)
// If view already has a mask
if let originalMask = view.layer.mask, let originalShape = originalMask as? CAShapeLayer, let originalPath = originalShape.path {
// Create bezierpath from original mask's path
let originalBezierPath = UIBezierPath(cgPath: originalPath)
// Append view's bounds to "reset" the mask path before we re-apply the original
newClipPath.append(UIBezierPath(rect: view.bounds))
// Combine new and original paths
newClipPath.append(originalBezierPath)
}
// Apply new mask
newMask.path = newClipPath.cgPath
newMask.fillRule = .evenOdd
view.layer.mask = newMask
}
I used the built-in function union to create a raw CGRect and then I get the max value to draw a proper circle.
Thanks to the accepted answer (from user Tibin Thomas) I was able to adapt the use of arcs with UIBezierPath to obtain exactly what I needed. I have accepted his answer but posted my final code here for future reference.
Of note, before calling this method, I have to convert the CGRect coordinates from the UIImageViews superview to the coordinate space of my UIEffectView. I also apply an inset of -1 to obtain the 1pt border. Thus the radii used are 1 greater than the radii of my UIImageViews.
func cutCircle(inView view: UIView, rect1: CGRect, rect2: CGRect?) {
// Create new path and mask
let newMask = CAShapeLayer()
// Create path to clip
let newClipPath = UIBezierPath(rect: view.bounds)
// Center of rect1
let x1 = rect1.midX
let y1 = rect1.midY
let center1 = CGPoint(x: x1, y: y1)
// New path
let newPath: UIBezierPath
if let rect2 = rect2 {
// Need to get two arcs - main icon and padlock icon
// Center of rect2
let x2 = rect2.midX
let y2 = rect2.midY
let center2 = CGPoint(x: x2, y: y2)
// These values are hard-coded for 25pt radius main icon and bottom-right-aligned 10pt radius padlock icon with a 1pt additional border
newPath = UIBezierPath(arcCenter: center1, radius: 26, startAngle: 1.2, endAngle: 0.3, clockwise: true)
newPath.addArc(withCenter: center2, radius: 11, startAngle: 5.8, endAngle: 2.2, clockwise: true)
} else {
// Only single circle is needed
newPath = UIBezierPath(ovalIn: rect1)
}
newPath.close()
newClipPath.append(newPath)
// If view already has a mask
if let originalMask = view.layer.mask,
let originalShape = originalMask as? CAShapeLayer,
let originalPath = originalShape.path {
// Create bezierpath from original mask's path
let originalBezierPath = UIBezierPath(cgPath: originalPath)
// Append view's bounds to "reset" the mask path before we re-apply the original
newClipPath.append(UIBezierPath(rect: view.bounds))
// Combine new and original paths
newClipPath.append(originalBezierPath)
}
// Apply new mask
newMask.path = newClipPath.cgPath
newMask.fillRule = .evenOdd
view.layer.mask = newMask
}

How to apply a gradient to a UIImageView using a UIImage as a mask

I have a UIImage inside a UIImageView. I want to overlay a gradient from right to left in the shape of the UIImage.
I can render the UIImage as a template and apply a tint to the UIImageView, but obviously that only has one colour rather than a gradient. I saw some stuff on here about using the UIImage as a mask but I couldn't figure it out. Anyone know how to do this?
There might be easier ways but if I got you right this could work for you:
// create image and image view
let image = UIImage(named: "yourImage")
let imageView = UIImageView(image: image)
// create the gradient
let gradient = CAGradientLayer()
gradient.colors = [
UIColor.red.cgColor,
UIColor.green.cgColor
]
gradient.startPoint = CGPoint(x: 1, y: 0.5)
gradient.endPoint = CGPoint(x: 0, y: 0.5)
gradient.frame = imageView.bounds
// add a mask to the gradient
let mask = CALayer()
mask.contents = image?.cgImage
mask.frame = gradient.bounds
gradient.mask = mask
// add the gradient as a sublayer to the image view's layer
imageView.layer.addSublayer(gradient)
Result:

Unable to create circle using UIBezierPath and SCNShape

Anyone familiar with using curved UIBezierPaths to create a SCNShape in ARKit? I'm creating a closed circle path, but I get a diamond shape in my ARKit scene.
Here is the code I use to create the bezier path and SCNShape:
let radius : CGFloat = 1.0
let outerPath = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: 2* radius, height: 2* radius))
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue
material.isDoubleSided = true
material.ambient.contents = UIColor.black
material.lightingModel = .constant
material.emission.contents = UIColor.blue
let shape = SCNShape(path: outerPath, extrusionDepth: 0.01)
shape.materials = [material]
let shapeNode = SCNNode(geometry: shape)
positioningNode.addChildNode(shapeNode)
I've successfully tested a rectangular bezier path, but even had issues with a rounded rect bezier path (using UIBezierPath(roundedRect:). For the rounded rect bezier path, ARKit shows the curved corners with 45 degree lines.
Thanks in advance!
UPDATE:
On the left is the initial SCNShape with UIBezierPath flatness set to 0.6. On the right is the same SCNShape with flatness set to 0.001.
I was able to find a solution. Basically the UIBezierPath's flatness variable is used to control curvature. The default value of 0.6 was too large in my case. I ended up using a flatness of 0.001
https://developer.apple.com/documentation/scenekit/scnshape/1523432-init

Radial gradient uses unexpected gray color

TL;DR
Does anyone know how I can smooth out the gradient transition from red to white in a radial gradient? Because I'm getting an ugly gray color in my gradient.
Details:
I created a custom UIView with the following in draw rect
override func draw(_ rect: CGRect) {
let colors = [UIColor.red.cgColor, UIColor.clear.cgColor] as CFArray
let divisor: CGFloat = 3
let endRadius = sqrt(pow(frame.width/divisor, 2) + pow(frame.height/divisor, 2))
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
let context = UIGraphicsGetCurrentContext()
context?.saveGState()
context?.drawRadialGradient(gradient!,
startCenter: center,
startRadius: 0.0,
endCenter: center,
endRadius: endRadius,
options: .drawsBeforeStartLocation)
context?.restoreGState()
}
I set the custom view's color to red and added the view to the viewcontroller's view, which has a white background.
I wanted the gradient to go from red to white but it seems to go from red to gray. I tried changing the second color in the gradient array to white, and though it did look slightly better, the grayness still remains.
When using clear color on transparent gradients keep in mind that the clear color is generated by applying the black color an alpha and thats why you get the greyish color on your case.
To solve this in your code change UIColor.clear.cgColor, from colors array, with UIColor.white.withAlphaComponent(0.0).cgColor. This way you can get the gradient you expect.

drawing gradient over rectangle in swift

I try to draw a gradient in a rectangle.
Following picture shows, that gradient in general works (blue), but it fills the entire screen.
I like to do the gradient only in a specific area like the green rectangle.
How can I do this ?
My code for the gradient experiment :-)
let context = UIGraphicsGetCurrentContext()
//let locations: [CGFloat] = [ 0.0, 0.25, 0.5, 0.75 ]
let locations: [CGFloat] = [ 0.0, 0.99 ]
let colors = [UIColor.blueColor().CGColor,
UIColor.whiteColor().CGColor]
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorspace,
colors, locations)
var startPoint = CGPoint()
var endPoint = CGPoint()
startPoint.x = 0.0
startPoint.y = 10.0
endPoint.x = self.frame.width-100;
endPoint.y = 10
//let rectangle_main = CGRectMake(CGFloat(15), CGFloat(0), CGFloat(1000), CGFloat(30));
//CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
You need to specify a clipping path right before drawing the gradient.
In your case, create a rectangle path and call CGContextClip(context)
CGContextAddRect(context, CGRect(...))
CGContextClip(context)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
The gradient will be clipped to the rectangle.

Resources