Making a crater in UIView with CALayer
Although title sounds horrible, believe me - It's not that bad. This is a trick I learned from StackOverflow post. Although seemingly innocuous, it took me a while to figure out given the view, how to puncture any shape into it.
To make it more interesting let's not only add boring shapes, but we will spice up with few more shapes for entertainment. Beware though, you won't be dealing only with UIView
s, you will fight with CALayer
as well which makes it more interesting and frightening as well. (For me personally though, I cannot write a CAShapeLayer
code without referring to StackOverflow. Syntax just gets me every time)
So...here we go!
First off, let's make a view to make a hole into. For simplicity we will call it simply a view
. Let's build our view
. For a reference, we will color it with Blue background. Background will be colored in the Red
Before we begin, please note that this tutorial will only apply on views sized and positioned with frames. If you are using autolayouts, the frame size won't be immediately available and you potentially have to put your code inside
viewDidLayoutSubview
method which gets called as soon as autolayout is finished laying out views on the screen
self.view.backgroundColor = .red
let view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
view.backgroundColor = .blue
self.view.addSubview(view)
Next step, let's create a shape mask layer, say circle using UIBezierPath
let maskLayer = CAShapeLayer()
maskLayer.frame = view.bounds
// Create the frame for the circle.
let radius: CGFloat = 50.0
// Rectangle in which circle will be drawn
let rect = CGRect(x: 100, y: 100, width: 2 * radius, height: 2 * radius)
let circlePath = UIBezierPath(ovalIn: rect)
// Create a path
let path = UIBezierPath(rect: view.bounds)
// Append additional path which will create a circle
path.append(circlePath)
// Setup the fill rule to EvenOdd to properly mask the specified area and make a crater
maskLayer.fillRule = kCAFillRuleEvenOdd
// Append the circle to the path so that it is subtracted.
maskLayer.path = path.cgPath
// Mask our view with Blue background so that portion of red background is visible
view.layer.mask = maskLayer
The result will look like this,
To add some fun, let add few more shapes. All you have to do is to replace a circlePath
with any path you want.
Triangle
let trianglePath = UIBezierPath()
trianglePath.move(to: CGPoint(x: view.frame.size.width/2, y: 200))
trianglePath.addLine(to: CGPoint(x:100, y: 300))
trianglePath.addLine(to: CGPoint(x: view.frame.size.width - 100, y: 300))
trianglePath.close()
let path = UIBezierPath(rect: view.bounds)
// Append additional path which will create a circle
path.append(trianglePath)
Triangular crater
Possibilities are endless. Polygons, star, random shape, square etc. etc.
To make it more generic, let's create a function where you can simply pass any shape enum you want and it will pick it up from there.
enum ShapeType {
case Rectangle
case Circle
case Triangle
case Pentagon
case All
}
// A function which will pick a mask to apply based on the enum thus passed. Remember that All will imply to apply all masks
func maskLayerWith(inputView:UIView, shape: ShapeType) {
let finalPath = UIBezierPath(rect: inputView.bounds)
let maskLayer = CAShapeLayer()
maskLayer.frame = inputView.bounds
if (shape == .Rectangle) {
finalPath.append(self.rectanglePath())
} else if (shape == .Circle) {
finalPath.append(self.circlePath())
} else if (shape == .Triangle) {
finalPath.append(self.trianglePath())
} else if (shape == .Pentagon) {
finalPath.append(self.pentagonPath())
} else if (shape == .All) {
finalPath.append(self.rectanglePath())
finalPath.append(self.circlePath())
finalPath.append(self.trianglePath())
finalPath.append(self.pentagonPath())
}
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = finalPath.cgPath
inputView.layer.mask = maskLayer
}
func rectanglePath() -> UIBezierPath {
let rect = CGRect(x: 250, y: 50, width: 100, height: 100)
return UIBezierPath(rect: rect)
}
func circlePath() -> UIBezierPath {
let radius: CGFloat = 50.0
let rect = CGRect(x:100, y: 300, width: 2 * radius, height: 2 * radius)
return UIBezierPath(ovalIn: rect)
}
func trianglePath() -> UIBezierPath {
let trianglePath = UIBezierPath()
trianglePath.move(to: CGPoint(x: view.frame.size.width/2, y: 400))
trianglePath.addLine(to: CGPoint(x:100, y: 500))
trianglePath.addLine(to: CGPoint(x: view.frame.size.width - 100, y: 500))
trianglePath.close()
return trianglePath
}
func pentagonPath() -> UIBezierPath {
// Ref: https://developer.apple.com/library/content/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/BezierPaths/BezierPaths.html
let pentagonPath = UIBezierPath()
// Set the starting point of the shape.
pentagonPath.move(to: CGPoint(x: 100, y: 70))
pentagonPath.addLine(to: CGPoint(x: 200, y: 110))
pentagonPath.addLine(to: CGPoint(x: 160, y: 210))
pentagonPath.addLine(to: CGPoint(x: 40, y: 210))
pentagonPath.addLine(to: CGPoint(x: 0, y: 110))
pentagonPath.close()
return pentagonPath
}
If you want to have little fun, you can pass All
to the method which applies mask to the given UIView
self.maskLayerWith(inputView: view, shape: .All)
Result with multiple craters
The code is hosted on the GitHub repo. If you want to fiddle with it, you can. Any questions, comments, concerns, Welcome.
You can extend the repo further by adding any fun shapes. Thing to note down is that
func maskLayerWith(inputView:UIView, shape: ShapeType)
is a generic method. You can apply crater to anyinputView
with anyshape
you want by extending the present code