Cut transparent hole in UIView using UIImageView - ios

I'm trying to create the image below for my QR camera view.
I have an image for the camera overlay (which is the square in the middle of the screen).
Ive looked at similar topics on stackoverflow and found out how to cut out the camera overlay image from the black background (0.75% transparent) so that it leaves the empty space in the middle, however I'm having some real issues with its placement, and I cant find out what is behaving so weirdly.
Here is the code that I use to create the background black image and also to cut out the square in the center:
// Create a view filling the screen.
let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
// Set a semi-transparent, black background.
backgroundView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = backgroundView.bounds
maskLayer.fillColor = UIColor.black.cgColor
// Create the path.
let path = UIBezierPath(rect: backgroundView.bounds)
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath
// Set the mask of the view.
backgroundView.layer.mask = maskLayer
// Add the view so it is visible.
self.view.addSubview(backgroundView)
The camOverlayImageView is created in storyboard and all I'm using on it are constraints to center it both vertically and horizontally to the superview like this:
However, when I do this, this is what I'm getting on the device:
If anyone might know what might be causing this or how to fix it, it would be greatly appreciated as I can't seem to find it.
I can of course manually move the frame and offset it like this but that isn't the correct way to do this:
let overlayFrame = camOverlayImageView.frame
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: CGRect(
x: overlayFrame.origin.x + 18,
y: overlayFrame.origin.y - 6,
width: overlayFrame.size.width,
height: overlayFrame.size.height))
)
maskLayer.path = path.cgPath
Instead of doing what I was previously doing:
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath

Most likely...
You are creating your mask too early - before auto-layout has sized / positioned the views.
Try it like this:
class HoleInViewController: UIViewController {
#IBOutlet var camOverlayImageView: UIView!
let backgroundView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.black.withAlphaComponent(0.75)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(backgroundView)
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = backgroundView.bounds
maskLayer.fillColor = UIColor.black.cgColor
// Create the path.
let path = UIBezierPath(rect: backgroundView.bounds)
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath
// Set the mask of the view.
backgroundView.layer.mask = maskLayer
}
}

Related

How to cut portion of CALayer in iOS Swift? [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 3 years ago.
I am trying to create a QR Reader. For that I am showing a rectOfInterest with some CALayer for visual representation. I want to show a box with some border at the corners and black background with some opacity so hide the other view from the AVCaptureVideoPreviewLayer. What I have achieved till now looks like this:
As you can see the CALayer is there but I want to cut the box portion of the layer so that that blackish thing does not come there. The code I am using to do this is like below:
func createTransparentLayer()->CALayer{
let shape = CALayer()
shape.frame = self.scanView.layer.bounds
shape.backgroundColor = UIColor.black.cgColor
shape.opacity = 0.7
return shape
}
I looked into other questions for this, seems like you have the mask the layer with the cut portion. So I subclassed the CALayer and cleared the context in drawInContext and set the mask property of the super layer to this. After that I get nothing. Everything is invisible there. What is wrong in this?
The code I tried is this:
class TransparentLayer: CALayer {
override func draw(in ctx: CGContext) {
self.backgroundColor = UIColor.black.cgColor
self.opacity = 0.7
self.isOpaque = true
ctx.clear(CGRect(x: superlayer!.frame.size.width / 2 - 100, y: superlayer!.frame.size.height / 2 - 100, width: 200, height: 200))
}
}
then set the mask property like this:
override func viewDidLayoutSubviews() {
self.rectOfInterest = CGRect(x: self.scanView.layer.frame.size.width / 2 - 100, y: self.scanView.layer.frame.size.height / 2 - 100, width: 200, height: 200)
scanView.rectOfInterest = self.rectOfInterest
let shapeLayer = self.createFrame()
scanView.doInitialSetup()
self.scanView.layer.mask = self.createTransparentLayer()
self.scanView.layer.addSublayer(shapeLayer)
}
here the shapeLayer is the bordered corner in the screenshot. How can I achieve this?
I have added view in my view controller which is centre align vertically and horizontally. Also fixed height and width to 200. Then created extension of UIView and added following code:
extension UIView {
func strokeBorder() {
self.backgroundColor = .clear
self.clipsToBounds = true
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = UIBezierPath(rect: self.bounds).cgPath
self.layer.mask = maskLayer
let line = NSNumber(value: Float(self.bounds.width / 2))
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.cgColor
borderLayer.lineDashPattern = [line]
borderLayer.lineDashPhase = self.bounds.width / 4
borderLayer.lineWidth = 10
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
}
}
Use:
By using outlet of view and call method to set border as you have request.
self.scanView.strokeBorder()
To clear the backgroundColor respective to mask view, I have added following code to clear it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.scanView.strokeBorder()
self.backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
// Draw a graphics with a mostly solid alpha channel
// and a square of "clear" alpha in there.
UIGraphicsBeginImageContext(self.backgroundView.bounds.size)
let cgContext = UIGraphicsGetCurrentContext()
cgContext?.setFillColor(UIColor.white.cgColor)
cgContext?.fill(self.backgroundView.bounds)
cgContext?.clear(CGRect(x:self.scanView.frame.origin.x, y:self.scanView.frame.origin.y, width: self.scanView.frame.width, height: self.scanView.frame.height))
let maskImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Set the content of the mask view so that it uses our
// alpha channel image
let maskView = UIView(frame: self.backgroundView.bounds)
maskView.layer.contents = maskImage?.cgImage
self.backgroundView.mask = maskView
}
Output:
I'm not using camera in background.

Add subtract mask on top of a UIView

I want to put a UIView on top of another UIView as a subtract mask. Is it possible to do that?
I have a rectangular photo and I want to put a mask on top of it that has black rounded corners. I don't want to put the rounded corners on the photo itself. Essentially the mask is kind of like a photo frame that you can look through.
I did that once using this as reference. It's basically setting up a CAShapeLayer on a UIView on the top of your UIImageView. This is what you need:
// Add image view to the root view
let image = UIImage(named: "SomeImage.jpg")
let imageView = UIImageView(frame: view.bounds)
imageView.image = image
view.addSubview(imageView)
// Add mask view on the of image view
let maskView = UIView(frame: view.bounds)
maskView.backgroundColor = .black
view.addSubview(maskView)
// The layer that defines your maskView's behavior
let maskLayer = CAShapeLayer()
maskLayer.frame = maskView.bounds
// Create the frame for the circle.
let radius: CGFloat = 100.0
let roundedRectPath = UIBezierPath(roundedRect: maskView.frame, cornerRadius: radius)
let path = UIBezierPath(rect: maskView.bounds)
// Append the rounded rect to the external path
path.append(roundedRectPath)
maskLayer.fillRule = .evenOdd
maskLayer.path = path.cgPath
maskView.layer.mask = maskLayer
And here's the result:
Hope that helps!
I have a similar idea as other mask, But just use a UIView mask.
let maskView = UIView.init()
maskView.frame = view.bounds
let shape = CAShapeLayer.init()
shape.path = UIBezierPath.init(ovalIn: maskView.bounds).cgPath
shape.fillColor = UIColor.white.cgColor
maskView.layer.addSublayer(shape)
maskView.backgroundColor = UIColor.init(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.0)
view.mask = maskView // view is the imageView.

Swift - Shadow for a irregular shape of a View

i am struggling to add shadow to a custom shape.
Here is a picture of what i want to construct:
(Dont mind the text and the symbol)
You can see the custom shape with the curved corner on the right and the rectangular shape on the left with shadow.
I am using UIView, and added corner to the left.
This is the code i have so far that shape the view correct:
View1.backgroundColor = .green //green color is just to see the shape well
let path = UIBezierPath(roundedRect:View1.bounds,
byRoundingCorners:[.topRight, .bottomRight],
cornerRadii: CGSize(width: self.frame.height/2, height: self.frame.height/2))
let maskLayer = CAShapeLayer()
I Have tried to add shadow to it, but the shadow does not apear.
Here is the code i have tried to add shadow:
View1.layer.masksToBounds = false
View1.layer.layer.shadowPath = maskLayer.path
View1.layer.shadowColor = UIColor.black.cgColor
View1.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
View1.layer.shadowOpacity = 0.5
View1.layer.shadowRadius = 1.0
How can you add shadow to this shape?
You can achieve this by using a single UIView (shadowView), adding a shapeLayer sublayer and setting the shadow of the shadowView's layer.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
#IBOutlet var shadowView: UIView!
func setup() {
// setup irregular shape
let path = UIBezierPath.init(roundedRect: shadowView.bounds, byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize.init(width: 20, height: 20))
let layer = CAShapeLayer.init()
layer.frame = shadowView.bounds
layer.path = path.cgPath
layer.fillColor = UIColor.white.cgColor
layer.masksToBounds = true
shadowView.layer.insertSublayer(layer, at: 0)
// setup shadow
shadowView.layer.shadowRadius = 8
shadowView.layer.shadowOpacity = 0.2
shadowView.layer.shadowOffset = CGSize.init(width: 0, height: 2.5)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowPath = path.cgPath
}
}
Note:
The shadowView.clipToBounds must be false for the shadows to take effect.
To see the layer.fillColor, set the shadowView.backgroundColor to .clear.
You can easily achieve the above via Interface Builder by setting the 'Background' property and unchecking the 'Clip to Bounds' checkbox.

Chopped edge around border of image

I have an image with a border
let smallicon: UIImageView = {
let smallicon = UIImageView()
smallicon.layer.borderWidth = 2
smallicon.layer.borderColor = UIColor.whiteColor().CGColor
smallicon.hidden = true
return smallicon
}()
The problem is that is has tiny chopped edge around border (small image with yellow and black lines)
How to get rid of it ?
Solution
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Create a view with red background for demonstration
let v = UIView(frame: CGRectMake(0, 0, 100, 100))
v.center = view.center
v.backgroundColor = UIColor.redColor()
view.addSubview(v)
// Add rounded corners
let maskLayer = CAShapeLayer()
maskLayer.frame = v.bounds
maskLayer.path = UIBezierPath(roundedRect: v.bounds, byRoundingCorners: .TopRight | .TopLeft, cornerRadii: CGSize(width: 25, height: 25)).CGPath
v.layer.mask = maskLayer
// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = UIColor.greenColor().CGColor
borderLayer.lineWidth = 5
borderLayer.frame = v.bounds
v.layer.addSublayer(borderLayer)
}
}
NOTE! in order this to work target view must have frame property setup with size. without size your view will not be seen at all

Simply mask a UIView with a rectangle

I want to know how to simply mask the visible area of a UIView of any kind. All the answers/tutorials I've read so far describe masking with an image, gradient or creating round corners which is way more advanced than what I am after.
Example: I have a UIView with the bounds (0, 0, 100, 100) and I want to cut away the right half of the view using a mask. Therefore my mask frame would be (0, 0, 50, 100).
Any idea how to do this simply? I don't want to override the drawrect method since this should be applicable to any UIView.
I've tried this but it just makes the whole view invisible.
CGRect mask = CGRectMake(0, 0, 50, 100);
UIView *maskView = [[UIView alloc] initWithFrame:mask];
viewToMask.layer.mask = maskView.layer;
Thanks to the link from MSK, this is the way I went with which works well:
// Create a mask layer and the frame to determine what will be visible in the view.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectMake(0, 0, 50, 100);
// Create a path with the rectangle in it.
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
// Set the path to the mask layer.
maskLayer.path = path;
// Release the path since it's not covered by ARC.
CGPathRelease(path);
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
Thanks for answers guys.
In case someone can't find suitable answer on SO for this question for hours, like i just did, i've assembled a working gist in Swift 2.2 for masking/clipping UIView with CGRect/UIBezierPath:
https://gist.github.com/Flar49/7e977e81f1d2827f5fcd5c6c6a3c3d94
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
Usage:
let viewSize = targetView.bounds.size
let rect = CGRect(x: 20, y: 20, width: viewSize.width - 20*2, height: viewSize.height - 20*2)
// Cuts rectangle inside view, leaving 20pt borders around
targetView.mask(withRect: rect, inverse: true)
// Cuts 20pt borders around the view, keeping part inside rect intact
targetView.mask(withRect: rect)
Hope it will save someone some time in the future :)
No need any mask at all.
Just put it into a wrapper view with the smaller frame, and set clipsToBounds.
wrapper.clipsToBounds = true
Very simple example in a Swift ViewController, based on the accepted answer:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView(frame: view.bounds)
view.addSubview(redView)
view.backgroundColor = UIColor.green
redView.backgroundColor = UIColor.red
mask(redView, maskRect: CGRect(x: 50, y: 50, width: 50, height: 50))
}
func mask(_ viewToMask: UIView, maskRect: CGRect) {
let maskLayer = CAShapeLayer()
let path = CGPath(rect: maskRect, transform: nil)
maskLayer.path = path
// Set the mask of the view.
viewToMask.layer.mask = maskLayer
}
}
Output
An optional layer whose alpha channel is used as a mask to select
between the layer's background and the result of compositing the
layer's contents with its filtered background.
#property(retain) CALayer *mask
The correct way to do what you want is to create the maskView of the same frame (0, 0, 100, 100) as the viewToMask which layer you want to mask. Then you need to set the clearColor for the path you want to make invisible (this will block the view interaction over the path so be careful with the view hierarchy).
Swift 5 , thanks #Dan Rosenstark
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let red = UIView(frame: view.bounds)
view.addSubview(red)
view.backgroundColor = UIColor.cyan
red.backgroundColor = UIColor.red
red.mask(CGRect(x: 50, y: 50, width: 50, height: 50))
}
}
extension UIView{
func mask(_ rect: CGRect){
let mask = CAShapeLayer()
let path = CGPath(rect: rect, transform: nil)
mask.path = path
// Set the mask of the view.
layer.mask = mask
}
}
Setting MaskToBounds is not enough, for example, in scenario where you have UIImageView that is positioned inside RelativeLayout. In case that you put your UIImageView to be near top edge of the layout and then you rotate your UIImageView, it will go over layout and it won't be cropped. If you set that flag to true, it will be cropped yes, but it will also be cropped if UIImageView is on the center of layout, and that is not what you want.
So, determinating maskRect is the right approach.

Resources