Creating a blurView with a clear cut out - ios

I am creating an overlay of blurredView with a clear rectangle cutout in the middle. My code so far has achieved the opposite, ie a clearView with a blur cutout in the middle instead.
I have adapted my steps from the following posts, I appreciate if anyone could point me in the right direction.
Swift mask of circle layer over UIView
CALayer with transparent hole in it
let blurView = UIVisualEffectView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
blurView.effect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let scanLayer = CAShapeLayer()
scanLayer.path = CGPath(rect: scanRect, transform: nil)
view.addSubview(blurView)
blurView.layer.addSublayer(scanLayer)
blurView.layer.mask = scanLayer

You can do it like this
let scanLayer = CAShapeLayer()
let scanRect = CGRect.init(x: 100, y: 100, width: 200, height: 100)
let outerPath = UIBezierPath(rect: scanRect)
let superlayerPath = UIBezierPath.init(rect: blurView.frame)
outerPath.usesEvenOddFillRule = true
outerPath.append(superlayerPath)
scanLayer.path = outerPath.cgPath
scanLayer.fillRule = kCAFillRuleEvenOdd
scanLayer.fillColor = UIColor.black.cgColor
view.addSubview(blurView)
blurView.layer.mask = scanLayer

Related

CAShapeLayer Image Not Appearing

I have an image.cgImage that I set as the contents to a CAShapeLayer() but it's not appearing. I force unwrap the image so I know that it definitely exists. The round shape appears but the image inside of it doesn't.
let shapeLayer = CAShapeLayer()
var didSubviewsLayout = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !didSubviewsLayout {
didSubviewsLayout = true
setupShapeLayer()
}
}
func setupShapeLayer() {
shapeLayer.path = UIBezierPath(roundedRect: CGRect(x: 200, y: 200, width: 50, height: 50), cornerRadius: 25).cgPath
shapeLayer.strokeColor = nil
shapeLayer.fillColor = UIColor.orange.cgColor
shapeLayer.contents = UIImage(named: "myIcon")!.cgImage // this doesn't crash so the image definitely exists
view.layer.addSublayer(shapeLayer)
}
The problem is that your shape layer has no size. You have not given it a frame, so it is just an invisible dot at the top right corner. Unfortunately, you can still see the shape, so you're not aware of the fact that you're doing this all wrong. But the content doesn't draw, because the layer is just a dot, so it reveals your code's feet of clay.
So, in this case, change
shapeLayer.path = UIBezierPath(roundedRect: CGRect(x: 200, y: 200, width: 50, height: 50), cornerRadius: 25).cgPath
To
shapeLayer.frame = CGRect(x: 200, y: 200, width: 50, height: 50)
shapeLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 50, height: 50), cornerRadius: 25).cgPath
Incidentally, that is not how to draw a circle correctly, so if that's your goal, stop it. Use ovalIn; that's what it's for. Complete example:
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 200, y: 200, width: 50, height: 50)
shapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 50, height: 50)).cgPath
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.lineWidth = 4
shapeLayer.fillColor = nil
shapeLayer.contents = UIImage(named: "smiley")?.cgImage
self.view.layer.addSublayer(shapeLayer)
Result:
Note too that you are probably making this mistake with all your shape layers, not just this one, so you will want to go back through all the code in this app (or better, all the code you've ever written!) and write all your shape layers correctly. Layers need frames!
You could create the rounded path in a separate layer and add it as a mask to the layer that contains the image. Firstly, I would add a frame to the parent layer so that the image can be centred within the layer using contentsGravity.
shapeLayer.frame = CGRect(x: 200, y: 200, width: 50, height: 50)
shapeLayer.contents = UIImage(named: "myIcon")!.cgImage
// Center the image within the layer
shapeLayer.contentsGravity = .center
// We can set the orange color here
shapeLayer.backgroundColor = UIColor.orange.cgColor
Then, create the layer that would have the rounded path and set it as a mask to the image layer:
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 50, height: 50), cornerRadius: 25).cgPath
shapeLayer.mask = maskLayer
Your modified code:
func setupShapeLayer() {
shapeLayer.frame = CGRect(x: 200, y: 200, width: 50, height: 50)
shapeLayer.contents = UIImage(named: "myIcon")!.cgImage
// Center the image within the layer
shapeLayer.contentsGravity = .center
// We can set the orange color here
shapeLayer.backgroundColor = UIColor.orange.cgColor
// Create mask layer
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 50, height: 50), cornerRadius: 25).cgPath
shapeLayer.mask = maskLayer
view.layer.addSublayer(shapeLayer)
}

UIImage to the Shapelayer in Swift 4

How can we fill a shape layer with image in iOS using swift 4 ? I have tried using below. I got an Black background. Here self is a Shapelayer where i am going to set an Image
let image = UIImage(named: "ACVResources.bundle/temp.png")!
let imageLayer = CALayer()
imageLayer.contents = UIImage(named: "image")?.cgImage // Assign your image
imageLayer.frame = self.frame
imageLayer.mask = self
self.masksToBounds = true
self.setNeedsDisplay()
First, you have to add CALayer(imageLayer) that fill your view after that you need to set that your view.layer mask your shapeLayer.
I give you an example:
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 500))
customView.backgroundColor = .red
view.addSubview(customView)
let imageLayer = CALayer()
let image = UIImage(named: "pxl.jpeg")
imageLayer.contents = image?.cgImage
imageLayer.frame = customView.frame
customView.layer.addSublayer(imageLayer)
let shape = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: customView.frame.size.width / 2, y: 50))
path.addLine(to: CGPoint(x: customView.frame.size.width, y: customView.frame.size.height / 2))
path.addLine(to: CGPoint(x: 0, y: customView.frame.size.height / 2))
path.close()
shape.path = path.cgPath
customView.layer.mask = shape
screenshot of that code

How to set the contentGravity variable of CALayer to 'resizeAspectFill'?

I am trying to set the contentGravity variable of a CALayer but I am not quite sure how to do this. I have tried the following without success:
var layer = CAShapeLayer.init()
layer.contentGravity = .resizeAspectFill // This is not recognised
My UIImageView presents the image taken from the camera of the phone. I then use the Vision Framework to detect rectangles, which return the four corner coordinates of the detected rectangle. I then draw the rectangle in the layer of the UIImageView. The content mode of the UIIMageView is 'Aspect Fill'. My rectangle is currently misaligned and i think it has to do with the contentGravity of the layer. Please see image below:
Rectangle drawn into CAShapelayer:
let rec = UIBezierPath.init()
rec.move(to: CGPoint.init(x: topLeft.x, y: topLeft.y))
rec.addLine(to: CGPoint.init(x: topRight.x, y: topRight.y))
rec.addLine(to: CGPoint.init(x: bottomRight.x, y: bottomRight.y))
rec.addLine(to: CGPoint.init(x: bottomLeft.x, y: bottomLeft.y))
rec.close()
self.layer.opacity = 0.4
self.layer.path = rec.cgPath
self.layer.lineWidth = 5
self.layer.strokeColor = UIColor.yellow.cgColor
self.layer.frame = self.imgPreViewOutlet.bounds
self.imgPreViewOutlet.layer.addSublayer(self.layer)
image show misaligned rectangle drawn in the layer of UIImageView
There is no purpose in setting the content gravity of a CAShapeLayer, as it will in no way affect how the shape is drawn. Content gravity is meaningful only for a layer with content, e.g. a CGImage that is drawn into its contents property.
Here's an example where contentsGravity matters; same image, same layer size, different content gravity:
override func viewDidLoad() {
super.viewDidLoad()
do {
let layer = CALayer()
layer.frame = CGRect(x: 100, y: 100, width: 250, height: 200)
layer.contents = UIImage(named:"smiley")!.cgImage
layer.borderWidth = 1
self.view.layer.addSublayer(layer)
layer.contentsGravity = .center
}
do {
let layer = CALayer()
layer.frame = CGRect(x: 100, y: 300, width: 250, height: 200)
layer.contents = UIImage(named:"smiley")!.cgImage
layer.borderWidth = 1
self.view.layer.addSublayer(layer)
layer.contentsGravity = .resize
}
}
But if we try exactly the same thing with a shape layer, the content gravity makes no difference:
override func viewDidLoad() {
super.viewDidLoad()
do {
let layer = CAShapeLayer()
layer.frame = CGRect(x: 100, y: 100, width: 250, height: 200)
layer.path = CGPath(rect: CGRect(x: 0, y: 0, width: 20, height: 20), transform: nil)
layer.borderWidth = 1
self.view.layer.addSublayer(layer)
layer.contentsGravity = .center
}
do {
let layer = CAShapeLayer()
layer.frame = CGRect(x: 100, y: 300, width: 250, height: 200)
layer.path = CGPath(rect: CGRect(x: 0, y: 0, width: 20, height: 20), transform: nil)
layer.borderWidth = 1
self.view.layer.addSublayer(layer)
layer.contentsGravity = .resize
}
}

CAShapeLayer cutout in UIView

My goal is to create a pretty opaque background with a clear rounded rect inset
let shape = CGRect(x: 0, y: 0, width: 200, height: 200)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 16).cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
let background = UIView()
background.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view.addSubview(background)
constrain(background) {
$0.edges == $0.superview!.edges
}
background.layer.addSublayer(maskLayer)
// background.layer.mask = maskLayer
When I uncomment background.layer.mask = maskLayer, the view is totally clear. When I have it commented out, I see the semi-opaque background color but no mask cutout
Any idea what I'm doing wrong here? Thanks!
Here is a playground that I think implements your intended effect (minus .edges and constrain - that appear to be your own extensions).
a) used a red background instead of black with alpha (just to make the effect stand out)
b) set maskLayer.backgroundColor instead of maskLayer.fillColor
Adding the maskLayer as another layer to uiview is superfluous (as indicated by #Ken) but seems to do no harm.
import UIKit
import PlaygroundSupport
let bounds = CGRect(x: 0, y: 0, width: 400, height: 200)
let uiview = UIView(frame: bounds)
uiview.backgroundColor = UIColor.red.withAlphaComponent(0.8)
PlaygroundPage.current.liveView = uiview
let shape = CGRect(x: 100, y: 50, width: 200, height: 100)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 16).cgPath
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
uiview.layer.mask = maskLayer
image of clear inset
On the other hand, if you intended a picture frame / colored-envelope-with-transparent-window effect like below, then see gist
image of picture frame
Your mask Layer has no size.
Add this line maskLayer.frame = shape
and you don't need background.layer.addSublayer(maskLayer)

How to animate the path of a UIView's mask?

I have been exploring this questions through playgrounds for days now without finding any solution.
I am been managing to create a hole in a blurred UIView. Now I am wondering how to animate the radius of this circle, ergo animating the blurred view's mask's path.
Here is my code so far:
import Foundation
import UIKit
import PlaygroundSupport
let contentRect = CGRect(x: 0, y: 0, width: 400, height: 400)
let contentView = UIView(frame: contentRect)
contentView.backgroundColor = .white
PlaygroundPage.current.liveView = contentView
let image = UIImage(named: "landscape.jpg")
let imageView = UIImageView(frame: contentRect, image: image)
imageView.contentMode = .scaleAspectFill
let blur = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blur)
blurView.frame = contentRect
let path = UIBezierPath(rect: contentRect)
let circle = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 300, height: 300))
path.append(circle)
path.usesEvenOddFillRule = true
let toPath = UIBezierPath(rect: contentRect)
let toCircle = UIBezierPath(ovalIn: CGRect(x: 150, y: 150, width: 100, height: 100))
toPath.append(toCircle)
toPath.usesEvenOddFillRule = true
let hole = CAShapeLayer()
hole.path = path.cgPath
hole.fillColor = UIColor.green.cgColor
hole.fillRule = kCAFillRuleEvenOdd
let mask = UIView(frame: contentRect)
contentView.addSubview(imageView)
contentView.addSubview(blurView)
mask.layer.addSublayer(hole)
blurView.mask = mask
I have been trying this, without success:
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = hole.path
animation.toValue = toPath.cgPath
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
hole.add(animation, forKey: nil)
CATransaction.begin()
CATransaction.disableActions()
hole.path = toPath.cgPath
CATransaction.commit()
Also tried UIView.animate(... or extracting blurView.layer.sublayers to add them to my contentView, but it just gets messy.
Another approach would be to use a CGAffineTransform, which I tried, but the blur gets disrupted. Plus I have trouble to use a workaround when I feel so close to a solution.
Any help would be greatly appreciated! Thanks!

Resources