Draw a circular transparent hole into UIView for given rect - swift

I am trying to create a circular transparent hole into my UIview with the following code, and this code makes the correct hole based on given rect. In my situation I have few rects which are individual and few of them are overlapped,for all those rect which are getting overlapped and not getting transparent holes over intersection rect area because of fill rule kCAFillRuleEvenOdd.How can I modify the same code so it can work for individual rects and all other overlapped rect and create transparent holes into view?
let overlay = UIView(frame: CGRect(x: 0, y: 0, width: width,height: height))
overlay.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.4)
// Create the path.
let path = UIBezierPath(rect: view.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = view.frame
for rectFrame in rects {
// Create the frame for the circle.
let rect = CGRect(x: rectFrame.origin.x, y: rectFrame.origin.y, width: 38, height: 38)
// Append the circle to the path so that it is subtracted.
path.append(UIBezierPath(ovalIn: rect))
}
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = path.cgPath
// Set the mask of the view.
view.layer.mask = maskLayer
self.overlayLayer.addSubview(overlay)

Related

How to centre rectangle in view

I want to centre a rectangle in a view, i try using midX or midY but the view still not centre
this is how i setup the rectangle
the overlayView have width = 414 and height = 688
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let midX = overlayView.bounds.midX
let midY = overlayView.bounds.midY
let center = CGPoint(x: midX, y: midY)
let size: CGFloat = 312
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = overlayView.bounds
// Create the path.
let rect = CGRect(x: center.x, y: center.y, width: size, height: size)
let path = UIBezierPath(rect: overlayView.bounds)
maskLayer.fillRule = .evenOdd
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(roundedRect: rect, cornerRadius: 20))
maskLayer.path = path.cgPath
// Set the mask of the view.
overlayView.layer.mask = maskLayer
}
I already try to calculate the center but still can't get the rect in the centre.
You have to subtract half-height and width from the center, as you have set start x and y position from the center point
so change your rect code.
let rect = CGRect(x: center.x - size/2, y: center.y - size/2, width: size, height: size)

How to center the CAShapeLayer path in the shape layer

I created a DrawView where it's possible to draw. I created an instance of UIBezierPath for drawing.
// This function is called when the user has finished drawing.
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let shapeLayer = CAShapeLayer()
//The CAShapeLayer has the same path of the drawing (currentPath is the instance of UIBezierPath).
shapeLayer.path = currentPath?.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 10.0
//Here I set the shapeLayer on the drawingView (the blue one that you can see in the image)
drawingView.layer.addSublayer(shapeLayer)
shapeLayer.position = CGPoint(x: drawingView.layer.bounds.midX, y: drawingView.layer.bounds.midY)
shapeLayer.backgroundColor = UIColor.blue.cgColor
shapeLayer.frame = drawingView.layer.bounds
}
The problem is that the path (e.g number 3 in the image) is not centered on its shapeLayer.
shapeLayer.path?.boundingBoxOfPath = (289.5, 349.5, 525.0, 129.0)
shapeLayer.frame = (0.0, 0.0, 200.0, 200.0)
shapeLayer.position = (100.0, 100.0)
drawingView.frame = (0.0, 912.0, 200.0, 200.0)
Any hint? thank you
Forget the drawing view and just think about how to center a path in a shape layer of known size. Here's a deliberate failure:
let path = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 50, height: 50))
let lay = CAShapeLayer()
lay.frame = CGRect(x: 40, y: 40, width: 200, height: 200)
lay.backgroundColor = UIColor.red.cgColor
lay.path = path.cgPath
self.view.layer.addSublayer(lay)
We get this:
The filled circle is not centered in the red shape layer. Okay, we know why, because we know the bezier path that created the filled circle. But let's say we don't know that. We can still center the shape in the shape layer, like this:
let path = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 50, height: 50))
let lay = CAShapeLayer()
lay.frame = CGRect(x: 40, y: 40, width: 200, height: 200)
lay.backgroundColor = UIColor.red.cgColor
let cgpath = path.cgPath
let box = cgpath.boundingBoxOfPath
let xtarget = (lay.bounds.width - box.width)/2
let ytarget = (lay.bounds.height - box.height)/2
let xoffset = xtarget - box.minX
let yoffset = ytarget - box.minY
var transform = CGAffineTransform(translationX: xoffset, y: yoffset)
let cgpath2 = cgpath.copy(using: &transform)
lay.path = cgpath2
self.view.layer.addSublayer(lay)
Result:
So, given a CGPath, and given a shape layer whose final bounds are known, you can use that technique to center the path in the shape layer. And those are circumstances that apply perfectly to your use case.

shadow on bezierpath view adds weird strokes to corners

I wanted to recreate the AppStore's "today" cards with rounded corners and a light drop shadow.
I created a path, a maskLayer and a separate shadowLayer, which – according to several sources – is the way of doing it.
The problem, however, is that my lovely rounded rectangle with a shadow has got some gray strokes at it corners. How can I solve this? I tried different shadow opacities and different radii. It didn't solve my problem.
Here you can see my screenshots and my code below.
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// create sample view and add to view hierarchy
let bigTeaser = UIView(frame: CGRect(x: 16, y: 200, width: 343, height: 267))
bigTeaser.backgroundColor = UIColor.white
view.addSubview(bigTeaser)
// create the path for the rounded corners and the shadow
let roundPath = UIBezierPath(roundedRect: bigTeaser.bounds, cornerRadius: 20)
// create maskLayer
let maskLayer = CAShapeLayer()
maskLayer.frame = bigTeaser.bounds
maskLayer.path = roundPath.cgPath
bigTeaser.layer.mask = maskLayer
// create shadowLayer
let shadowLayer = CAShapeLayer()
shadowLayer.path = roundPath.cgPath
shadowLayer.frame = bigTeaser.frame
shadowLayer.shadowOpacity = 0.3
shadowLayer.shadowRadius = 24
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0, height: 2)
// insert layers
bigTeaser.superview!.layer.insertSublayer(shadowLayer, below: bigTeaser.layer)
}
If you replace:
shadowLayer.path = roundPath.cgPath
to
shadowLayer.shadowPath = roundPath.cgPath
The ugly borders will magically disappear.

Can CAShapeLayer shapes center itself in a subView?

I created an arbitrary view
let middleView = UIView(
frame: CGRect(x: 0.0,
y: view.frame.height/4,
width: view.frame.width,
height: view.frame.height/4))
middleView.backgroundColor = UIColor.blue
view.addSubview(middleView)
Then I created a circle using UIBezierPath; however when I set the position to middleView.center, the circle is far off to the bottom of the view. Can you set the position in the center of a subview?
let shapeLayer = CAShapeLayer()
let circlePath = UIBezierPath(
arcCenter: .zero,
radius: 100,
startAngle: CGFloat(0).toRadians(),
endAngle: CGFloat(360).toRadians(),
clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = UIColor.purple.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.position = middleView.center
middleView.layer.addSublayer(shapeLayer)
How do I center this circle in that view?
You have two problems.
First, you are setting shapeLayer.position = middleView.center. The center of a view is is the superview's geometry. In other words, middleView.center is relative to view, not to middleView. But then you're adding shapeLayer as a sublayer of middleView.layer, which means shapeLayer needs a position that is in middleView's geometry, not in view's geometry. You need to set shapeLayer.position to the center of middleView.bounds:
shapeLayer.position = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
Second, you didn't say where you're doing all this. My guess is you're doing it in viewDidLoad. But that is too early. In viewDidLoad, the views loaded from the storyboard still have the frames they were given in the storyboard, and haven't been laid out for the current device's screen size yet. So it's a bad idea to look at frame (or bounds or center) in viewDidLoad if you don't do something to make sure that things will be laid out correctly during the layout phase. Usually you do this by setting the autoresizingMask or creating constraints. Example:
let middleView = UIView(
frame: CGRect(x: 0.0,
y: view.frame.height/4,
width: view.frame.width,
height: view.frame.height/4))
middleView.backgroundColor = UIColor.blue
middleView.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleTopMargin, .flexibleBottomMargin]
view.addSubview(middleView)
However, shapeLayer doesn't belong to a view, so it doesn't have an autoresizingMask and can't be constrained. You have to lay it out in code. You could do that, but it's better to just use a view to manage the shape layer. That way, you can use autoresizingMask or constraints to control the layout of the shape, and you can set it up in viewDidLoad.
let circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
circleView.center = CGPoint(x: middleView.bounds.midX, y: middleView.bounds.midY)
circleView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin]
circleView.shapeLayer.strokeColor = UIColor.purple.cgColor
circleView.shapeLayer.fillColor = nil
middleView.addSubview(circleView)
...
class CircleView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer }
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
Result:
And after rotating to landscape:

Fill circle border with gradient color using UIBezierPath

I want to fill half of a circle's border using UIBazierpath with gradient color.
Initially I tried with the full circle but it's not working, the gradient always fills the circle but not the border. Is there any way to do this?
Here's what I have so far:
let path = UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2)
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(shape)
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
shapeMask.lineWidth = 2
self.layer.addSublayer(gradient)
Edit: Added the image. I want to achieve something like this.
Core graphics doesn't support an axial gradient so you need to draw this out in a more manual way.
Here's a custom view class that draws a circle using the range of HSV colors around the circumference.
class RadialCircleView: UIView {
override func draw(_ rect: CGRect) {
let thickness: CGFloat = 20
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - thickness / 2
var last: CGFloat = 0
for a in 1...360 {
let ang = CGFloat(a) / 180 * .pi
let arc = UIBezierPath(arcCenter: center, radius: radius, startAngle: last, endAngle: ang, clockwise: true)
arc.lineWidth = thickness
last = ang
UIColor(hue: CGFloat(a) / 360, saturation: 1, brightness: 1, alpha: 1).set()
arc.stroke()
}
}
}
let radial = RadialCircleView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
radial.backgroundColor = UIColor(red: 0.98, green: 0.92, blue: 0.84, alpha: 1) // "antique white"
Copy this into a playground to experiment with the results. The colors don't exactly match your image but it may meet your needs.