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.
Related
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
}
Let's say I have some UIView and also I have a "closed" UIBezierPath inside of that view. What I want to achieve is to have everything "outside" this path be white and inside - transparent (like a hole of that path-shape). Solutions like this don't work, for some reason, part of the shape inside is also white.
You can use the view.layer.mask property to make holes in a a view.
I'm using this piece of code to make mask from images, but you could also just draw you UIBezierPath to CALayer and use that instead.
/*
Get a layer that can be used to mask an image. All pixels with alpha 0 in mask image will be hidden
use as:
self.view.layer.mask = <Mask returned from this call>
self.view.layer.masksToBounds = YES;
*/
+(CALayer*)getMaskLayerFromImage:(UIImage*)image
{
CALayer *mask = [CALayer layer];
mask.contents =(id) image.CGImage;
mask.frame = CGRectMake(0, 0, image.size.width, image.size.height);
return mask;
}
I have achieved this thing by using UIBezierPath and CAShapeLayer
I am taking outlet of view from storyboard.
#IBOutlet weak var myView: UIView!
Create an object of UIBezierPath()
var path = UIBezierPath()
Create a method which take center of view and we create another UIBezierPath() as circlePath which is circle and we append the circle on previous UIBezierPath() path.
Know take a CAShapeLayer and cut the circlePath
Change center point to draw circle on specific point.
func overLay() {
let sizes = CGSize(width: 30, height: 30)
let circlePath = UIBezierPath(ovalIn: CGRect(origin: self.view.center, size: sizes))
path.append(circlePath)
let maskLayer = CAShapeLayer() //create the mask layer
maskLayer.path = path.cgPath // Give the mask layer the path you just draw
maskLayer.fillRule = kCAFillRuleEvenOdd // Cut out the intersection part
myView.layer.mask = maskLayer
}
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.
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
This is my attempt:
func round() {
let width = bounds.width < bounds.height ? bounds.width : bounds.height
let mask = CAShapeLayer()
mask.path = UIBezierPath(ovalInRect: CGRectMake(bounds.midX - width / 2, bounds.midY - width / 2, width, width)).CGPath
self.layer.mask = mask
// add border
let frameLayer = CAShapeLayer()
frameLayer.path = mask.path
frameLayer.lineWidth = 4.0
frameLayer.strokeColor = UIColor.whiteColor().CGColor
frameLayer.fillColor = nil
self.layer.addSublayer(frameLayer)
}
It works on the iphone 6 simulator (storyboard has size of 4.7), but on the 5s and 6+ it looks weird:
Is this an auto layout issue? Without the border, auto layout works correct. This is my first time working with masks and so I am not sure if what I have done is correct.
round function is called in viewDidLayoutSubviews.
Any thoughts?
If you have subclassed UIImageView, for example, you can override layoutSubviews such that it (a) updates the mask; (b) removes any old border; and (c) adds a new border. In Swift 3:
import UIKit
#IBDesignable
class RoundedImageView: UIImageView {
/// saved rendition of border layer
private weak var borderLayer: CAShapeLayer?
override func layoutSubviews() {
super.layoutSubviews()
// create path
let width = min(bounds.width, bounds.height)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
// update mask and save for future reference
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
// create border layer
let frameLayer = CAShapeLayer()
frameLayer.path = path.cgPath
frameLayer.lineWidth = 32.0
frameLayer.strokeColor = UIColor.white.cgColor
frameLayer.fillColor = nil
// if we had previous border remove it, add new one, and save reference to new one
borderLayer?.removeFromSuperlayer()
layer.addSublayer(frameLayer)
borderLayer = frameLayer
}
}
That way, it responds to changing of the layout, but it makes sure to clean up any old borders.
By the way, if you are not subclassing UIImageView, but rather are putting this logic inside the view controller, you would override viewWillLayoutSubviews instead of layoutSubviews of UIView. But the basic idea would be the same.
--
By the way, I use a mask in conjunction with this shape layer because if you merely apply rounded corners of a UIView, it can result in weird artifacts (look at very thin gray line at lower part of the circular border):
If you use the bezier path approach, no such artifacts result:
For Swift 2.3 example, see earlier revision of this answer.
The easiest way is to manipulate the layer of the image view itself.
imageView.layer.cornerRadius = imageView.bounds.size.width / 2.0
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor.whiteColor.CGColor
imageView.layer.masksToBounds = true
You can include this in viewDidLayoutSubviews or layoutSubviews to make sure the size is always correct.
NB: Maybe this technique makes your circle mask obsolete ;-). As a rule of thumb, always choose the simplest solution.