Swift: UIBezierPath fill intersection of 2 paths - ios

I'm working with 2 UIBezierPath to create a rectangle and a semicircle shape. It works fine but I am not able to fill the intersection with color.
That's my drawCircle and my addPaths functions:
private func drawCircle(selectedPosition: SelectionRangePosition, currentMonth: Bool) {
circleView.layer.sublayers = nil
let radius = contentView.bounds.height/2
let arcCenter = CGPoint(x: ceil(contentView.bounds.width/2), y: ceil(contentView.bounds.height/2))
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: .pi/2, endAngle: .pi * 3/2, clockwise: selectedPosition == .left)
let semiCircleLayerPath = UIBezierPath()
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.fillColor = UIColor.tealish.cgColor
circleView.layer.addSublayer(semiCircleLayer)
switch (selectedPosition, currentMonth) {
case (.left, true):
let rectanglePath = UIBezierPath(rect: CGRect(x: contentView.bounds.width / 2, y: 0, width: ceil(contentView.bounds.width / 2), height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
case (.middle, true):
backgroundColor = .tealish
case (.right, true):
// Note: The + 10 is just to overlap the shapes and test if the filling works
let rectanglePath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: ceil(contentView.bounds.width / 2) + 10, height: contentView.bounds.height))
addPaths(semiCircleLayerPath, rectanglePath, circlePath, semiCircleLayer)
default:
backgroundColor = .clear
}
}
private func addPaths(_ semiCircleLayerPath: UIBezierPath, _ rectanglePath: UIBezierPath, _ circlePath: UIBezierPath, _ semiCircleLayer: CAShapeLayer) {
semiCircleLayerPath.append(circlePath)
semiCircleLayerPath.append(rectanglePath)
semiCircleLayer.path = semiCircleLayerPath.cgPath
}

The problem is that the semicircle path and the rectangle path are being added in opposite directions. This is a function of the way the Cocoa API adds them, unfortunately; in your case, it means that the intersection has a "wrap count" of 0.
Fortunately, there is an easy fix: create separate paths for the semicircle and the rectangle. When fill them both individually, you should get your desired result.
Unfortunately, this will mean that you can't use layer drawing only: you will want to use the drawRect method to draw the background.

If your dates are separate UILabels, you can skip this rendering & path filling problem entirely by dropping a UIView as a child of the calendar, under the labels holding the dates; give it a background color & rounded corners (using layer.cornerRadius as usual). That'll take care of your rounded rect for you.

Related

Why does CAShapeLayer exist?

I'm trying to understand CAShapeLayer and where it shines. It seems like the main advantage is the shape style properties that you can conveniently access:
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: .init(width: 200, height: 200))
shapeLayer.fillColor = .none
shapeLayer.lineWidth = 5
shapeLayer.strokeColor = UIColor.blue.cgColor
let path = UIBezierPath()
let withCenter = CGPoint(x: 100, y: 100)
let radius = CGFloat(100)
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(CGFloat.pi * 2)
path.addArc(withCenter: withCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.path = path.cgPath
But, I think the advantage ends there. It rather limits your ability set the context to multiple states like colors for different paths and requires a new CAShapeLayer. I can override the drawing method or use the delegate, but that's be doing the same thing as CALayer in that case.
On the other hand, with CALayer:
let layer = CALayer()
layer.frame = CGRect(origin: CGPoint(x: 100, y: 300), size: .init(width: 200, height: 200))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
let image = renderer.image { (_) in
let path = UIBezierPath()
let withCenter = CGPoint(x: 100, y: 100)
let radius = CGFloat(100)
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(CGFloat.pi * 2)
path.lineWidth = 5
UIColor.blue.setStroke()
path.addArc(withCenter: withCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.stroke()
}
layer.contents = image.cgImage
you can set multiple stroke colors for different paths. Obviously, you can set the CAShapeLayer's contents in a similar manner, but I want to take advantage of CAShapeLayer's API's that CALayer doesn't offer.
Are the shape style properties the main reason for CAShapeLayer?
For me, the big advantage to CAShapeLayer is animation. As Matt says, you can animate strokeStart and/or strokeEnd to cause a shape to either appear like it's being drawn with a pen, or disappear. You can also use animate strokeStart and/or strokeEnd to create a variety of different "wipe" animations.
Here is a link to a post I wrote using animations to strokeEnd to create a "clock wipe" animation:
How do you achieve a "clock wipe"/ radial wipe effect in iOS?
You can also animate the path that's installed into the shape layer. As long as the starting and ending paths have the same number of control points, the system creates a smooth, elegant looking animation. There are some tricks to making this work correctly however. Arc animations don't work as expected if you change the arc angle during an animation, because internally arcs are composed of different numbers of cubic Bezier curves.
Check out my projects RandomBlobs and TrochoidDemo on Github for examples of animating a path's control points.

How I can create this circular shape in iOS Swift4?

I have to create this shape like the below image in a UIView. But I am not get any idea how to draw this shape. I am trying to draw this shape using UIBezierPath path with CAShapeLayer the circular line draw successfully but I am unable to draw the circular points and circular fill colour. Can anyone please suggest me how I can achieve this type shape using any library or UIBezierPath.
This is my code which I am using try to draw this circular shape.
class ViewController: UIViewController {
var firstButton = UIButton()
var mylabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.creatingLayerWithInformation()
}
func creatingLayerWithInformation(){
let safeAreaHeight = self.view.safeAreaInsets.top
let navBarHeight = self.navigationController?.navigationBar.frame.height
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: 0, yPoint: navBarHeight!, layerColor: UIColor.green, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: self.view.frame.width, yPoint: self.view.frame.height - 150, layerColor: UIColor.blue, fillcolor: .clear)
let aa = self.view.frame.width * 0.72
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+aa, yPoint: navBarHeight!+5, layerColor: UIColor.blue, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+15, yPoint: navBarHeight!+aa, layerColor: UIColor.blue, fillcolor: .clear)
}
func addLayer(isClockWise: Bool, radius: CGFloat, xPoint: CGFloat, yPoint: CGFloat, layerColor: UIColor, fillcolor: UIColor) {
let pi = CGFloat(Float.pi)
let start:CGFloat = 0.0
let end :CGFloat = 20
// circlecurve
let path: UIBezierPath = UIBezierPath();
path.addArc(
withCenter: CGPoint(x:xPoint, y:yPoint),
radius: (radius),
startAngle: start,
endAngle: end,
clockwise: isClockWise
)
let layer = CAShapeLayer()
layer.lineWidth = 3
layer.fillColor = fillcolor.cgColor
layer.strokeColor = layerColor.cgColor
layer.path = path.cgPath
self.view.layer.addSublayer(layer)
}}
But I am getting the below result.
Please suggest me how I can achieve this shape. Correct me if I am doing anything wrong. If there is any library present then also please suggest. Please give me some solution.
Advance thanks to everyone.
There are many ways to achieve this effect, but a simple solution is to not draw the large circle as a single arc, but rather as a series of arcs that start and stop at the edges of the smaller circles. To do this, you need to know what the offset is from the inner circles. Doing a little trigonometry, you can calculate that as:
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
Thus:
let path = UIBezierPath()
path.move(to: point(from: arcCenter, radius: mainRadius, angle: startAngle))
let anglePerChoice = (endAngle - startAngle) / CGFloat(choices.count)
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
var from = startAngle
for index in 0 ..< choices.count {
var to = from + anglePerChoice / 2 - angleOffset
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
to = from + anglePerChoice
from += anglePerChoice / 2 + angleOffset
path.move(to: point(from: arcCenter, radius: mainRadius, angle: from))
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
from = to
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
layer.addSublayer(shapeLayer)
Where:
func point(from point: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: point.x + radius * cos(angle),
y: point.y + radius * sin(angle))
}
That yields:
So, when you then add the inner circles:
While the above is simple, it has limitations. Specifically if the lineWidth of the big arc was really wide in comparison to that of the small circles, the breaks in the separate large arcs won’t line up nicely with the edges of the small circles. E.g. imagine that the small circles had a radius 22 points, but that the big arc’s stroke was comparatively wide, e.g. 36 points.
If you have this scenario (not in your case, but for the sake of future readers), the other approach, as matt suggested, is to draw the big arc as a single stroke, but then mask it using the paths for the small circles.
So, imagine that you had:
a single CAShapeLayer, called mainArcLayer, for the big arc; and
an array of UIBezierPath, called smallCirclePaths, for all of the small circles.
You could then create a mask in layoutSubviews of your UIView subclass (or viewDidLayoutSubviews in your UIViewController subclass) like so:
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
smallCirclePaths.forEach { path.append($0) }
let mask = CAShapeLayer()
mask.fillColor = UIColor.white.cgColor
mask.strokeColor = UIColor.clear.cgColor
mask.lineWidth = 0
mask.path = path.cgPath
mask.fillRule = .evenOdd
mainArcLayer.mask = mask
}
That yields:
This is a slightly more generalized solution to this problem.
Think about the problem in the simplest possible form. Imagine a straight line with one small circle superimposed over the middle of it.
Let's divide our thinking into three layers:
The background that needs to show through
The layer that holds the straight line
The layer that holds the small circle
Calculate the location of the small circle layer relative to the straight line layer.
Place the small circle layer at that location. Okay, but the straight line shows through.
Go back to the straight line layer. Give it a mask. Construct that mask with a transparent circle at exactly the location of the small circle layer.
Now the mask "punches a hole" through the straight line — at exactly the place where the circle covers it. Thus we appear to see through the circle to the background, because the straight line is missing at exactly that place.
In real life there will be multiple circle layers and the mask will have multiple transparent circles and the straight line will be a curve, but that is a minor issue once you have the hole-punching worked out.

filling a circle gradually from bottom to top swift2

Basically I'am very new to Swift 2 and have created a circle with a stroke and white background using below code, then I got a circle something like this:
func getDynamicItemQty() -> UIImage {
let View = UIView(frame: CGRectMake(0,0,200,200))
let circlePath =
UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(90), startAngle: CGFloat(9.4), endAngle:CGFloat(0), clockwise: false)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
//shapeLayer.fillRule = kCAFillRuleEvenOdd
//change the fill color
shapeLayer.fillColor = UIColor.brownColor().CGColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.blueColor().CGColor
//you can change the line width
shapeLayer.lineWidth = 10
View.layer.addSublayer(shapeLayer)
return UIImage.renderUIViewToImage(View)
}
However, how can we draw circles that is partly filled horizontally in Swift 2? I mean circles which are filled, for example, from the bottom to the top according to the percentage specified in Swift code.
Here is a preview of what we need:
A view and a shape layer are definitely the wrang appraoch. You should take a look at UIGraphicsBeginImageContextWithOptions or for iOS 10 or newer UIGraphicsImageRenderer. For your problem: You should draw your circle twice. Something like that:
let size = CGSize(width: 200.0, height: 200.0)
UIGraphicsBeginImageContextWithOptions(size, true, 0)
let circlePath =
UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), radius: CGFloat(90), startAngle: CGFloat(9.4), endAngle:CGFloat(0), clockwise: false)
UIColor.white.fill()
UIRectFill(origin: CGPoint.zero, size: size)
// Drawing the background with a clipping
UIGraphicsPushContext(UIGraphicsGetCurrentContext())
UIColor(...).setFill()
UIRectClip(CGRect(x: 0.0, y:10.0 + 180.0 * (1.0 - percentage), width:size.width, height:size.height))
circlePath.fill()
// leave the subcontext to discard the clipping
UIGraphicsPopContext()
UIColor(...).setStroke()
circlePath.lineWidth = 10.0
circlePath.stroke()
// Keep the fruits of our labour
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Masking a CAShapeLayer creates this weird artifact?

I want to create a smooth progress bar for a to-do list. I used a circle (CGMutablePath) that masks the gray area and there's an obvious arc-like artifact. Not only that but there's also an artifact on the right side of the bar.
Note: I tried to rasterize the layers to no avail.
What causes iOS to do this and how can I smooth this out or get rid of it?
private var circleMask: CAShapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = GradientColor(.leftToRight, frame: self.bounds, colors: GradientFlatRed())
layer.cornerRadius = bounds.height / 2
plainProgressBar.layer.cornerRadius = layer.cornerRadius
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
plainProgressBar.layer.shouldRasterize = true
plainProgressBar.layer.rasterizationScale = UIScreen.main.scale
createMask()
}
private func createMask() {
let path = CGMutablePath()
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: 0, endAngle: CGFloat(Double.pi*2), clockwise: false)
path.addRect(bounds)
circleMask.path = path
circleMask.backgroundColor = plainMeterColor.cgColor
circleMask.fillRule = kCAFillRuleEvenOdd
plainProgressBar.layer.mask = circleMask
}
The issue is clearly an error in composition of the antialiased edges.
Knee-jerk question: do you have an atypical blend mode set?
Easiest solution: don't use path.addArc and path.addRect to generate two completely distinct shapes. Just use one of the init(roundedRect:... methods and set a corner radius of half your height, stretching the total path beyond the available space to the left to create a hard edge.
Or, if that doesn't appear, construct the path manually as a move, addLine(to:, addArc of only half a circle, then a final addLine(to: and a close. E.g. (thoroughly untested, especially re: start and end angles and clockwise versus anticlockwise versus iOS's upside-down coordinate system)
private func createMask() {
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y:0))
path.addLine(to: CGPoint(x: bounds.origin.x+25/2, y: 0))
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: CGFloat(Double.pi/2.0), endAngle: CGFloat(Double.pi*3.0/2.0), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: 25))
path.close()
...

Swift bezier curve fill gradient fill

Trying to create this curve in Swift using CG drawing on a view in my storyboard.
I've done the white curve but having trouble with filling it with a gradient as show in the image
There are three parts to this image.
The white curve.
The gradient.
The mask that blocks out the gradient.
Your best approach would be to create these as separate layers.
First create a CAGradientLayer with your desired colours.
Then create a path curve in white and add it above the gradient.
Now create a third layer with a closed path (similar to the curve but closed at the bottom. Use this as a maskLayer on the gradient layer.
Here is working code
class FillCurve: UIView {
override func draw(_ rect: CGRect) {
// Drawing ARC
let center = CGPoint(x: bounds.width / 2 , y: bounds.height / 2)
let curve = UIBezierPath()
curve.move(to:CGPoint(x: 10, y: bounds.height - 20))
curve.addQuadCurve(to: CGPoint(x: bounds.width - 20 , y: bounds.height - 20), controlPoint:center )
UIColor.white.setStroke()
curve.lineWidth = 2.0
curve.stroke()
let fillPath = curve.copy() as! UIBezierPath
fillPath.addLine(to: CGPoint(x: bounds.width , y: bounds.height - 2))
fillPath.addLine(to: CGPoint(x: 0, y: bounds.height - 2))
fillPath.close()
fillPath.fill()
fillPath.addClip()
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors = [UIColor.cyan.cgColor,UIColor.black.cgColor]
let location:[CGFloat] = [0.0,1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: location)!
UIGraphicsGetCurrentContext()?.drawLinearGradient(gradient, start: CGPoint(x: 0, y: fillPath.bounds.height), end: CGPoint(x: 0, y: bounds.height), options: [])
}
}
output

Resources