Erase all contents from CAShapeLayer - ios

I have a function that draws rectangles on top a AVCaptureVideoPreviewLayer. This rectangle needs to be refreshed upon a trigger, and a new rectangle drawn. Is it best to erase all contents from the CAShapeLayer or just remove the layer and add a new layer? I don't know how to do either but my attempt is below.
func showRectangle(recLayer: CAShapeLayer, vnRectangleObservation: VNRectangleObservation) -> Void{
// new CAShapeLayer
let newLayer = CAShapeLayer.init()
let rectangle = UIBezierPath.init()
rectangle.move(to: CGPoint.init(x: vnRectangleObservation.topLeft.x, y: vnRectangleObservation.topLeft.y))
rectangle.addLine(to: CGPoint.init(x: vnRectangleObservation.topRight.x, y: vnRectangleObservation.topRight.y))
rectangle.addLine(to: CGPoint.init(x: vnRectangleObservation.bottomRight.x, y: vnRectangleObservation.bottomRight.y))
rectangle.addLine(to: CGPoint.init(x: vnRectangleObservation.bottomLeft.x, y: vnRectangleObservation.bottomLeft.y))
rectangle.close()
newLayer.opacity = 0.4
newLayer.path = rectangle.cgPath
newLayer.fillColor = UIColor.orange.cgColor
// replace current layer containing old rectangle
recLayer.replaceSublayer(recLayer, with: newLayer)
}

You can iterate through all sublayers and remove them.
for sublayer in yourLayer.sublayers ?? [] {
sublayer.removeFromSuperlayer()
}
And just add new ones with addSublayer:
let newSublayer = CAShapeLayer()
yourLayer.addSublayer(newSublayer)

You can remove your old layer by making
oldLayer.removeFromSuperlayer()
And then add a new one
parentLayer.addSublayer(newLayer)
I think the best solution is to prepare both layers before your view appears on the screen and change them by trigger.

Related

dotted line is not coming till the end in uiview

I am trying to create a dotted line, I have tried with the code below:
private func createDottedView(for view:UIView) {
let width: CGFloat = 1
let color: CGColor = UIColor.black.cgColor
let caShapeLayer = CAShapeLayer()
caShapeLayer.strokeColor = color
caShapeLayer.lineWidth = width
caShapeLayer.lineDashPattern = [9,3]
let cgPath = CGMutablePath()
let cgPoint = [CGPoint(x: view.layer.bounds.minX, y: 0), CGPoint(x: view.layer.bounds.maxX, y: 0)]
cgPath.addLines(between: cgPoint)
caShapeLayer.path = cgPath
view.layer.addSublayer(caShapeLayer)
}
But this is not coming till end, how can i get this dotted line till end?
You can use an image misted of making a uiview a dotted line because of the CAShapeLayer()
It can cause this type of problem. So you can just download a similar image and place the image view inside the uiview and use the image to make it scale to fill it should work.
Or if you want to do it with coding only you can change your code with the given live of code the rest of the code will work fine.
let cgPoint = [CGPoint(x: view.layer.bounds.minX, y: 0), CGPoint(x:
view.layer.bounds.maxX+40, y: 0)]
We can add trailing and leading constraints on the UIView as shown below in the storyboard.
Added constrainsts to the lineView
Output of dotted UIView

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 multiple masks to UIView

I have a question about how to apply multiple masks to a UIView that already has a mask.
The situation:
I have a view with an active mask that creates a hole in its top left corner, this is a template UIView that is reused everywhere in the project. Later in the project I would like to be able to create a second hole but this time in the bottom right corner, this without the need to create a completely new UIView.
The problem:
When I apply the bottom mask, it of course replaces the first one thus removing the top hole ... Is there a way to combine them both? And for that matter to combine any existing mask with a new one?
Thank you in advance!
Based on #Sharad's answer, I realised that re-adding the view's rect would enable me to combine the original and new mask into one.
Here is my solution:
func cutCircle(inView view: UIView, withRect rect: CGRect) {
// Create new path and mask
let newMask = CAShapeLayer()
let newPath = UIBezierPath(ovalIn: rect)
// Create path to clip
let newClipPath = UIBezierPath(rect: view.bounds)
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 = kCAFillRuleEvenOdd
view.layer.mask = newMask
}
This is code I have used in my project to create one circle and one rectangle mask in UIView, you can replace the UIBezierPath line with same arc code :
func createCircleMask(view: UIView, x: CGFloat, y: CGFloat, radius: CGFloat, downloadRect: CGRect){
self.layer.sublayers?.forEach { ($0 as? CAShapeLayer)?.removeFromSuperlayer() }
let mutablePath = CGMutablePath()
mutablePath.addArc(center: CGPoint(x: x, y: y + radius), radius: radius, startAngle: 0.0, endAngle: 2 * 3.14, clockwise: false)
mutablePath.addRect(view.bounds)
let path = UIBezierPath(roundedRect: downloadRect, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 5, height: 5))
mutablePath.addPath(path.cgPath)
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
mask.backgroundColor = UIColor.clear.cgColor
view.layer.mask = mask
}
Pass your same UIView, it removes previous layers and applies new masks on same UIView.
Here mask.fillRule = kCAFillRuleEvenOdd is important. If you notice there are 3 mutablePath.addPath() functions, what kCAFillRuleEvenOdd does is, it first creates a hole with the arc then adds Rect of that view's bound and then another mask to create 2nd hole.
You can do something like the following, if you don't only have "simple shapes" but actual layers from e.g. other views, like UILabel or UIImageView.
let maskLayer = CALayer()
maskLayer.frame = viewToBeMasked.bounds
maskLayer.addSublayer(self.imageView.layer)
maskLayer.addSublayer(self.label.layer)
viewToBeMasked.bounds.layer.mask = maskLayer
So basically I just create a maskLayer, that contains all the other view's layer as sublayer and then use this as a mask.

Adding the same layer in a subview changes its visual position

I added a subview (with a black border) in a view and centered it.
Then I generate 2 identical triangles with CAShapeLayer and add one to the subview and the other to the main view.
Here is the visual result in Playground where we can see that the green triangle is totally off and should have been centered.
And here is the code:
let view = UIView()
let borderedView = UIView()
var containedFrame = CGRect(x: 0, y: 0, width: 100, height: 100)
func setupUI() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 600)
view.backgroundColor = .white
borderedView.frame = containedFrame
borderedView.center = view.center
borderedView.backgroundColor = .clear
borderedView.layer.borderColor = UIColor.black.cgColor
borderedView.layer.borderWidth = 1
view.addSubview(borderedView)
setupTriangles()
}
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green)) // GREEN triangle
}
private func createTriangle(color: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = borderedView.center
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: All position (of view, the borderedView and both triangles) are the same (150.0, 300.0)
Question: Why is the green layer not in the right position?
#DuncanC is right that each view has its own coordinate system. Your problem is this line:
layer.position = borderedView.center
That sets the layer's position to the center of the frame for the borderedView which is in the coordinate system of view. When you create the green triangle, it needs to use the coordinate system of borderedView.
You can fix this by passing the view to your createTriangle function, and then use the center of the bounds of that view as the layer position:
private func setupTriangles() {
view.layer.addSublayer(createTriangle(color: .red, for: view)) // RED triangle
borderedView.layer.addSublayer(createTriangle(color: .green, for: borderedView)) // GREEN triangle
}
private func createTriangle(color: UIColor, for view: UIView) -> CAShapeLayer {
let layer = CAShapeLayer()
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 0, y: 0))
bezierPath.addLine(to: CGPoint(x: -containedFrame.width, y: 0))
bezierPath.addLine(to: CGPoint(x: 0, y: -containedFrame.height))
bezierPath.close()
layer.position = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
layer.path = bezierPath.cgPath
layer.fillColor = color.cgColor
return layer
}
Note: When you do this, the green triangle appears directly below the red one, so it isn't visible.
Every view/layer uses the coordinate system of it's superview/superlayer. If you add a layer to self.view.layer, it will be positioned in self.view.layer's coordinate system. If you add a layer to borderedView.layer, it will be in borderedView.layer's coordinate system.
Think of the view/layer hierarchy as stacks of pieces of graph paper. You place a new piece of paper on the current piece (the superview/layer) in the current piece's coordinates system, but then if you draw on the new view/layer, or add new views/layer inside that one, you use the new view/layer's coordinate system.

Add camera layer on irregular shape images IOS

I need to add the camera layer on any irregular shaped image i.e. lets say i have a image which is having irregular shape and inside image there is a circular or any other irregular shape in which i want to embed the live camera.
Any idea how i can achieve this functionality?
You can use UIBezierPath to draw irregular share for a mask CAShapeLayer
let size = 200.0
Create a CAShapeLayer and draw shape in which you wanna embed a cameraPreviewLayer.
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath()
maskPath.move(to: .zero)
maskPath.addLine(to: CGPoint(x: 10, y: -size))
maskPath.addLine(to: CGPoint(x: size/2, y: -size))
maskPath.addLine(to: CGPoint(x: size*2, y: size))
maskPath.close()
maskLayer.anchorPoint = .zero
Set the mask positon
maskLayer.position = CGPoint(x: 100, y: 400)
maskLayer.path = maskPath.cgPath
self.yourVideoPreviewLayer.mask = maskLayer
self.yourVideoPreviewLayer.masksToBounds = true
Or you can make an image with a shape in which you wanna embed a cameraPreviewLayer. Or if your image's inner shape have an alpha value = 0 you can reverse alpha of your original image and use it as a mask.
let maskLayer = CAShapeLayer()
maskLayer.anchorPoint = .zero
maskLayer.frame = videoPreviewLayer.bounds
maskLayer.contents = YourReversedImage.cgImage
self.videoPreviewLayer.mask = maskLayer
self.videoPreviewLayer.masksToBounds = true
Add additional UIView upon of your UIImageView with same frames(width, height and position). it shouldn't be a subview of UIImageView!
Set background of this UIView to clearColor and create whatever layer you want.
Now you can use this layer as AVCaptureVideoPreviewLayer instead of using UIImageView layers

Resources