This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 6 years ago.
In my app I have a square UIView and I want to cut a hole/notch out of the top of. All the tutorials online are all the same and seemed quite straightforward but every single one of them always delivered the exact opposite of what I wanted.
For example this is the code for the custom UIView:
class BottomOverlayView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}
fileprivate func drawCircle(){
let circleRadius: CGFloat = 80
let topMidRectangle = CGRect(x: 0, y: 0, width: circleRadius*2, height: circleRadius*2)
let circle: CAShapeLayer = CAShapeLayer()
circle.position = CGPoint(x: (frame.width/2)-circleRadius, y: 0-circleRadius)
circle.fillColor = UIColor.black.cgColor
circle.path = UIBezierPath(ovalIn: topMidRectangle).cgPath
circle.fillRule = kCAFillRuleEvenOdd
self.layer.mask = circle
self.clipsToBounds = true
}
}
Here is what I hope to achieve (the light blue is the UIView, the dark blue is the background):
But here is what I get instead. (Every single time no matter what I try)
I'm not sure how I would achieve this, aside from making a mask that is already the exact shape that I need. But if I was able to do that then I wouldn't be having this issue in the first place. Does anyone have any tips on how to achieve this?
EDIT: The question that this is supposedly a duplicate of I had already attempted and was not able to get working. Perhaps I was doing it wrong or using it in the wrong context. I wasn't familiar with any of the given methods and also the use of pointers made it seem a bit outdated. The accepted answer does a much better job of explaining how this can be implemented using much more widely used UIBezierPaths and also within the context of a custom UIView.
I'd suggest drawing a path for your mask, e.g. in Swift 3
// BottomOverlayView.swift
import UIKit
#IBDesignable
class BottomOverlayView: UIView {
#IBInspectable
var radius: CGFloat = 100 { didSet { updateMask() } }
override func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
private func updateMask() {
let path = UIBezierPath()
path.move(to: bounds.origin)
let center = CGPoint(x: bounds.midX, y: bounds.minY)
path.addArc(withCenter: center, radius: radius, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
Note, I tweaked this to set the mask in two places:
From layoutSubviews: That way if the frame changes, for example as a result of auto layout (or by manually changing the frame or whatever), it will update accordingly; and
If you update radius: That way, if you're using this in a storyboard or if you change the radius programmatically, it will reflect that change.
So, you can overlay a half height, light blue BottomOverlayView on top of a dark blue UIView, like so:
That yields:
If you wanted to use the "cut a hole" technique suggested in the duplicative answer, the updateMask method would be:
private func updateMask() {
let center = CGPoint(x: bounds.midX, y: bounds.minY)
let path = UIBezierPath(rect: bounds)
path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
let mask = CAShapeLayer()
mask.fillRule = .evenOdd
mask.path = path.cgPath
layer.mask = mask
}
I personally find the path within a path with even-odd rule to be a bit counter-intuitive. Where I can (such as this case), I just prefer to just draw the path of the mask. But if you need a mask that has a cut-out, this even-odd fill rule approach can be useful.
Related
I am trying to make a UIView with pointed edges like this. Did some searching around and found some questions with slanting just one edge like this one but can't find an answer with intersecting (points) edges like the one in the picture that dynamically sizes based on the UIView height.
I used Rob's answer to create something like this:
#IBDesignable
class PointedView: UIView
{
#IBInspectable
/// Percentage of the slant based on the width
var slopeFactor: CGFloat = 15
{
didSet
{
updatePath()
}
}
private let shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 0
// with masks, the color of the shape layer doesn’t matter;
// it only uses the alpha channel; the color of the view is
// dictate by its background color
shapeLayer.fillColor = UIColor.white.cgColor
return shapeLayer
}()
override func layoutSubviews()
{
super.layoutSubviews()
updatePath()
}
private func updatePath()
{
let path = UIBezierPath()
// Start from x = 0 but the mid point of y of the view
path.move(to: CGPoint(x: 0, y: bounds.midY))
// Calculate the slant based on the slopeFactor
let slantEndX = bounds.maxX * (slopeFactor / 100)
// Create the top slanting line
path.addLine(to: CGPoint(x: slantEndX, y: bounds.minY))
// Straight line from end of slant to the end of the view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
// Straight line to come down to the bottom, perpendicular to view
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
// Go back to the slant end position but from the bottom
path.addLine(to: CGPoint(x: slantEndX, y: bounds.maxY))
// Close path back to where you started
path.close()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}
}
The end result should give you a view close to what you which can be modified on the storyboard
And can also be created using code, here is a frame example since the storyboard showed its compatibility with autolayout
private func createPointedView()
{
let pointedView = PointedView(frame: CGRect(x: 0,
y: 0,
width: 200,
height: 60))
pointedView.backgroundColor = .red
pointedView.slopeFactor = 50
pointedView.center = view.center
view.addSubview(pointedView)
}
I have an extension to curve the bottom edge of views since this styling is used on multiple screens in the app I am trying to create.
However, I have noticed I can only make it work with views that I have added trough interface builder. If I try to apply it on view created programmatically they do not render.
I have created a simple example to illustrate the problem. The main storyboard contains two viewControllers with a single colored view in the middle: one created with Interface Builder while the other programmatically.
In StoryboardVC, the view with the curve is rendered correctly without any problem. The setBottomCurve() method is used to create the curve.
If you compare this to setting the entry point to ProgrammaticVC, running the app you can see a plain white screen. Comment this line out to see the view appear again.
This is the extension used:
extension UIView {
func setBottomCurve(curve: CGFloat = 40.0){
self.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - curve
let curveTo:CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I expect ProgrammaticVC to look identical to StoryboardVC (except for the difference in color)
The example project can be found here:
https://github.com/belamatedotdotipa/CurveTest2
I suggest to create a subclass instead of using an extension, this is a specific behaviour.
In this case you cannot see the result expected, when you are adding the view programmatically, because in the viewDidLoad you don't have the frame of your view, in this example you can use the draw function:
class BottomCurveView: UIView {
#IBInspectable var curve: CGFloat = 40.0 {
didSet {
setNeedsLayout()
}
}
override func draw(_ rect: CGRect) {
setBottomCurve()
}
private func setBottomCurve() {
let rect = bounds
let y: CGFloat = rect.size.height - curve
let curveTo: CGFloat = rect.size.height
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
I attached the View image. I want to achieve the small cut in bottom between the buy button and above flight information view.
I think the easiest way would be to create 2 circles as plain UIView instances and set their center as the left and right edges of the parent view respectively.
Since you set clipsToBounds to true, they will be clipped and only half of them will be visible on the screen.
public class TestView: UIView {
private let leftCircle = UIView(frame: .zero)
private let rightCircle = UIView(frame: .zero)
public var circleY: CGFloat = 0
public var circleRadius: CGFloat = 0
public override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
addSubview(leftCircle)
addSubview(rightCircle)
}
public override func layoutSubviews() {
super.layoutSubviews()
leftCircle.frame = CGRect(x: -circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
leftCircle.layer.masksToBounds = true
leftCircle.layer.cornerRadius = circleRadius
rightCircle.frame = CGRect(x: bounds.width - circleRadius, y: circleY,
width: circleRadius * 2 , height: circleRadius * 2)
rightCircle.layer.masksToBounds = true
rightCircle.layer.cornerRadius = circleRadius
}
}
I've created a sample project demonstrating that. Here is how it looks in my simulator (iPhone SE 11.2):
I had to do this with shadows. I tried creating a layer and subtracting another layer from it using evenOdd fillRule, however that didn't work since I need a specific path for shadows and evenOdd applies to the filling in the path instead.
In the end I just created the path manually
func setShadowPath() {
let path = UIBezierPath()
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: cutoutView.frame.minX, y: bounds.minY))
path.addArc(withCenter: CGPoint(x: cutoutView.frame.midX, y: bounds.minY),
radius: cutoutView.bounds.width/2, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
layer.shadowPath = path.cgPath
}
I created a "cutoutView" in my xib so I could trace around it easily.
That makes the shadow the correct shape, and then in order to create the cut itself I just created a layer using that same path
func setupBackground() {
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = layer.shadowPath
backgroundLayer.fillColor = UIColor.white.cgColor
layer.insertSublayer(backgroundLayer, at: 0)
}
Could you please advice why CATransform3DRotate doesnt work for layer in my case, its blue layer on the image.
I have custom view, where I draw nature view, I want to animate changing the moon phase that will be done inside the white circle layer as its mask. I suppose that it is good idea to apply here 3DRotation, but for some reason it doesn't work even without animation, could be please advice what I am doing wrong?
func drawMoonPhase(inRect rect:CGRect, inContext context: CGContext) {
let moonShape = UIBezierPath(ovalIn: rect)
moonShape.lineWidth = 4.0
UIColor.white.setStroke()
moonShape.stroke()
moonShape.close()
let moonLayer = CAShapeLayer()
moonLayer.path = moonShape.cgPath
moonLayer.opacity = 0
self.layer.addSublayer(moonLayer)
let circlePath = UIBezierPath(ovalIn: rect)
UIColor.blue.setFill()
circlePath.fill()
circlePath.close()
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.opacity = 0
var transform = CATransform3DIdentity
transform.m34 = -1 / 500.0
transform = CATransform3DRotate(transform, (CGFloat(Double.pi * 0.3)), 0, 1, 0)
circleShape.transform = transform
moonLayer.mask = circleShape
}
Thanks in advance
EDIT:
Maybe I want clear , the effect i want is the below:
The transform occurs around the layer's anchor point that by default is in the center. Therefore what it is happening there is that the shape rotates around itself causing no visible result. :)
what you should do in this layout of layer is to use cos and sin math functions in order to determine the x and y position of your moon.
Let me know if you need more insights I will be happy to help.
also, please note that you don't need 2 shapeLayers in order to have the blue moon with the white stroke. CAShapeLayer has properties for both fill and stroke so you can simplify your code.
Based on the new info here is my new answer:
I was not able to get a nice effect by using the transform, so I decided to write the mask manually. this is the result:
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.black.cgColor
return mask
}
The function above draws a shapelier that can be used as mask for your moonLayer. This layer will be drawnin relation to a progress parameter that you will pass in the function where 1 is full moon and 0 is new moon.
You can put everything together to have the desired effect, and you can extract the path creation code to make a nice animation if you want.
This should answer your question I hope.
To quick test I wrote this playground:
import UIKit
let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
let view = UIView(frame: rect)
view.backgroundColor = .black
let layer = view.layer
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.white.cgColor
return mask
}
let moonLayer = CAShapeLayer()
moonLayer.path = UIBezierPath(ovalIn: rect).cgPath
moonLayer.fillColor = UIColor.clear.cgColor
moonLayer.strokeColor = UIColor.white.cgColor
moonLayer.lineWidth = 2
moonLayer.shadowColor = UIColor.white.cgColor
moonLayer.shadowOpacity = 1
moonLayer.shadowRadius = 10
moonLayer.shadowPath = moonLayer.path
moonLayer.shadowOffset = .zero
layer.addSublayer(moonLayer)
let moonPhase = moonMask(in: rect, forProgress: 0.3)
moonPhase.shadowColor = UIColor.white.cgColor
moonPhase.shadowOpacity = 1
moonPhase.shadowRadius = 10
moonPhase.shadowPath = moonLayer.path
moonPhase.shadowOffset = .zero
layer.addSublayer(moonPhase)
view
I'm training my SpritKit skills, but I can't figure out why my SKShapeNode are "reversed", I think I'm missing something.
I was trying example from this question: Question
So, I tried the example with 2 rounded corners, and another example which is a half-circle.
I don't understand why the two corners curved are not the one specified on the UIBezierPath object.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let rectangle = CGRect(x: 0, y: 0, width: 400, height: 400)
let path = UIBezierPath(roundedRect: rectangle, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 50, height: 50))
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
And why, my half circle are not oriented in the good direction either.
While in the documentation - Apple documentation - It says:
For example, specifying a start angle of 0 radians, an end angle of π radians, and setting the clockwise parameter to true draws the bottom half of the circle.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 200, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
Finally, can some one explain me how the array of 'corners' is considered as a mask?