How to draw a UIView or a UIButton using UIBezierPath - ios

I want to draw a UIView or a UIButton like this one which is show in the image.
How it is possible to draw in swift?
I have done a lot of R $ D on this what I get is UIBezierPath.
So, How to draw using UIBezierPath. I am unable to draw exactly this.
Please help me to draw like this.
I want create a separate class for this so that I can inherit any where.
Thank you in advance.
I have tried something like this.
extension UIButton {
func roundCorners(corners: UIRectCorner, radius: Int = 8) {
let maskPath1 = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
Result is
But I want a sharp cut.Like first image

To draw that shape, you want to use maskPath1.move(to: pt) and then a series of maskPath1.addLine(to: pt).
You'll need six points:
so you could code it like this:
extension UIButton {
func angledCorners() {
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
let pt1: CGPoint = CGPoint(x: bounds.minX, y: bounds.maxY)
let pt2: CGPoint = CGPoint(x: bounds.minX, y: bounds.midY)
let pt3: CGPoint = CGPoint(x: bounds.minX + bounds.midY, y: bounds.minY)
let pt4: CGPoint = CGPoint(x: bounds.maxX, y: bounds.minY)
let pt5: CGPoint = CGPoint(x: bounds.maxX, y: bounds.midY)
let pt6: CGPoint = CGPoint(x: bounds.maxX - bounds.midY, y: bounds.maxY)
let maskPath1: UIBezierPath = UIBezierPath()
maskPath1.move(to: pt1)
maskPath1.addLine(to: pt2)
maskPath1.addLine(to: pt3)
maskPath1.addLine(to: pt4)
maskPath1.addLine(to: pt5)
maskPath1.addLine(to: pt6)
maskPath1.close()
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
A couple problems with that approach though... the mask will not "auto-resize" when the button frame changes. So, you have to wait to call button.angledCorners() until auto-layout has finished setting the button's frame, and you have to call it again any time the frame changes.
If you have explicit frame sizes, that's not a problem.
But if you have buttons that change based on other views, or perhaps change size when the device is rotated, it's a hassle... and you probably want to make this a feature of a custom subclass.

Related

CAShapeLayer path animation shrinking before completing animation

I am trying to animate a rotation of a layer's CGpath.
The rotation works perfectly, but for some reason, the path seems to shrink, then get bigger again. I just want it to show a rotating animation. I don't want it to shrink
here is a video.
here is the code
class ViewController: UIViewController {
#IBOutlet weak var overlayView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let maskLayer = CAShapeLayer()
let path = UIBezierPath(rect: view.bounds)
let mask = UIBezierPath.drawSquare(width: 100, center: view.center)
// Makes it so we can see through the center of the view
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
path.append(mask)
maskLayer.path = path.cgPath
overlayView.layer.mask = maskLayer
}
#IBAction func rotateView(_ sender: Any) {
let maskPath = UIBezierPath.drawSquare(width: 100, center: overlayView.center)
let path = UIBezierPath(rect: overlayView.frame)
let bounds: CGRect = maskPath.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = 90 / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
maskPath.apply(transform)
path.append(maskPath)
// create new animation
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = path.cgPath
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "path")
}
}
extension UIBezierPath{
static func drawSquare(width: CGFloat, center: CGPoint) -> UIBezierPath {
let rect = CGRect(x: center.x - width/2, y: center.y - width/2, width: width, height: width)
let path = UIBezierPath()
path.move(to: rect.origin)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
return path
}
}
Your "cutout box" is shrinking and growing because you're using path animation.
When you animate a path from one set of points to another, each point will take a straight line from its starting point to its ending point.
Couple ways to visualize this...
First, we'll add an "outline" frame to the cutout mask box:
as you see, each corner is taking a direct route to its next position.
If we highlight one side, it's maybe a bit more obvious:
and, we can number the corners for even more clarity:
So, if we want to rotate the square without changing its size, we could use keyframe animation and run the corners along a circle:
or, much easier, rotate the mask layer instead of its path:
let radians = 90 / 180.0 * .pi
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.toValue = radians
anim.duration = 1.0
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "layerRotate")
No doubt we notice that the mask layer frame does not completely cover the overlayView frame.
So, we can fix that by making the mask layer frame larger.
We could calculate exactly how big it needs to be, but because shape layers are very efficient, we'll just make it a square, 1.5 times bigger than the longer axis:
so when it rotates, it covers the view:
and we finish with this:

I want to Create UIView Rounded Shape on Bottom inside ScrollView In Swift

Not on Top Edges. I want to do Like this on Bottom Edges and inside ScrollView.
How to do This
This should be close to what you want. Play with the bezier path points if it's not quite right:
#IBDesignable
class MyRoundBottomView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let y = bounds.size.height - 80.0
let p1 = CGPoint(x: 0.0, y: y)
let p2 = CGPoint(x: bounds.size.width, y: y)
let cp1 = CGPoint(x: p1.x, y: bounds.size.height)
let cp2 = CGPoint(x: bounds.size.width, y: bounds.size.height)
let myBez = UIBezierPath()
myBez.move(to: CGPoint(x: 0.0, y: y))
myBez.addCurve(to: p2, controlPoint1: cp1, controlPoint2: cp2)
myBez.addLine(to: CGPoint(x: bounds.size.width, y: 0.0))
myBez.addLine(to: CGPoint.zero)
myBez.close()
let l = CAShapeLayer()
l.path = myBez.cgPath
layer.mask = l
}
}
It's marked as #IBDesignable so you can add a UIView (in Storyboard) and assign its Custom Class to MyRoundBottomView and you'll see it at design-time:
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
use it as
yourView.roundCorners(corners : [.bottomLeft, .bottomRight], radius : 20 )
Also, update your layouts in viewDidLayoutSubviews() otherwise you might face clipping in soem devices.
Configure maskedCorners and cornerRadius on view's layer, i.e.
customView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
customView.layer.cornerRadius = customView.bounds.width / 2.0
create the extension as follow
extension UIView {
func round(corners: UIRectCorner, cornerRadius: Double) {
let size = CGSize(width: cornerRadius, height: cornerRadius)
let bezierPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: size)
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = bezierPath.cgPath
self.layer.mask = shapeLayer
}
}
you can use it like this
self.myView.round(corners: [.bottomLeft, .bottomRight], cornerRadius: 35)
I hope it will work for you ... :)

How to add a custom shape to an UIImageView in Swift?

I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}

iOS - Custom UIView doesn't show up on device

I have a custom UIView subclass, that I want to add as a subview on my UIViewController. The problem is, that the view doesn't show up, when I run the app, even though everything is set correctly (in viewDidLoad the view has a correct frame and is not hidden) and it shows up in Interface Builder (picture). On the device I only get a red screen, no triangle in the middle.
Here's the view subclass:
#IBDesignable
class TriangleView: UIView {
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let path = UIBezierPath()
let width = rect.size.width
let height = rect.size.height
let startingPoint = CGPoint(x: center.x, y: center.y - height / 2)
path.moveToPoint(startingPoint)
path.addLineToPoint(CGPoint(x: center.x + width / 2, y: center.y - height / 2))
path.addLineToPoint(CGPoint(x: center.x, y: center.y + height / 2))
path.addLineToPoint(CGPoint(x: center.x - width / 2, y: center.y - height / 2))
path.closePath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = rect
shapeLayer.position = center
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.whiteColor().CGColor
layer.mask = shapeLayer
layer.backgroundColor = UIColor.redColor().CGColor
}
}
I don't have any other code to show, I just add the view to the ViewController and set its constraints and class to TriangleView.
Swift 3
I worked on simplifying what you have. I know this is old but here is something that works which I believe achieves what you were trying to do:
#IBDesignable
class TriangleView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let width = rect.size.width
let height = rect.size.height
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: width, y: 0))
path.addLine(to: CGPoint(x: width/2, y: height))
path.close()
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
layer.backgroundColor = UIColor.red.cgColor
}
}
A few differences:
I only use 3 points instead of 4 for the triangle then close it.
The first 2 points are in the corner of the UIView.
I added layer.addSublayer(shapeLayer). I believe this is why it wasn't showing up when running the app.
Removed some code that I don't think was needed but you can add it back if you believe so.
Have you tried,
[self.view bringSubviewToFront:TriangleView]
Put this after adding your TriangleView as a subview of your view controller.

How do I create a UIView with a transparent circle inside (in swift)?

I want to create a view that looks like this:
I figure what I need is a uiview with some sort of mask, I can make a mask in the shape of a circle using a UIBezierpath, however I cannot invert this makes so that it masks everything but the circle. I need this to be a mask of a view and not a fill layer because the view that I intend to mask has a UIBlurEffect on it. The end goal is to animate this UIView overtop of my existing views to provide instruction.
Please note that I am using swift. Is there away to do this? If so, how?
Updated again for Swift 4 & removed a few items to make the code tighter.
Please note that maskLayer.fillRule is set differently between Swift 4 and Swift 4.2.
func createOverlay(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
radius: CGFloat) -> UIView {
// Step 1
let overlayView = UIView(frame: frame)
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
// Step 2
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = kCAFillRuleEvenOdd
// For Swift 4.2
maskLayer.fillRule = .evenOdd
// Step 4
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
A rough breakdown on what is happening:
Create a view sized to the specified frame, with a black background set to 60% opacity
Create the path for drawing the circle using the provided starting point and radius
Create the mask for the area to remove
Apply the mask & clip to bounds
The following code snippet will call this and place a circle in the middle of the screen with radius of 50:
let overlay = createOverlay(frame: view.frame,
xOffset: view.frame.midX,
yOffset: view.frame.midY,
radius: 50.0)
view.addSubview(overlay)
Which looks like this:
You can use this function to create what you need.
func createOverlay(frame : CGRect)
{
let overlayView = UIView(frame: frame)
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.blackColor()
self.view.addSubview(overlayView)
let maskLayer = CAShapeLayer()
// Create a path with the rectangle in it.
var path = CGPathCreateMutable()
let radius : CGFloat = 50.0
let xOffset : CGFloat = 10
let yOffset : CGFloat = 10
CGPathAddArc(path, nil, overlayView.frame.width - radius/2 - xOffset, yOffset, radius, 0.0, 2 * 3.14, false)
CGPathAddRect(path, nil, CGRectMake(0, 0, overlayView.frame.width, overlayView.frame.height))
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
// Release the path since it's not covered by ARC.
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
}
Adjust the radius and xOffset and yOffset to change the radius and position of the circle.
For Swift 3, here is rakeshbs' answer formatted so it returns the UIView needed:
func createOverlay(frame : CGRect, xOffset: CGFloat, yOffset: CGFloat, radius: CGFloat) -> UIView
{
let overlayView = UIView(frame: frame)
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.black
// Create a path with the rectangle in it.
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset), radius: radius, startAngle: 0.0, endAngle: 2 * 3.14, clockwise: false)
path.addRect(CGRect(x: 0, y: 0, width: overlayView.frame.width, height: overlayView.frame.height))
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
// Release the path since it's not covered by ARC.
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
The above solution works great.
Say if you are looking for mask with rectangle area here is the snippet below
let fWidth = self.frame.size.width
let fHeight = self.frame.size.height
let squareWidth = fWidth/2
let topLeft = CGPoint(x: fWidth/2-squareWidth/2, y: fHeight/2-squareWidth/2)
let topRight = CGPoint(x: fWidth/2+squareWidth/2, y: fHeight/2-squareWidth/2)
let bottomLeft = CGPoint(x: fWidth/2-squareWidth/2, y: fHeight/2+squareWidth/2)
let bottomRight = CGPoint(x: fWidth/2+squareWidth/2, y: fHeight/2+squareWidth/2)
let cornerWidth = squareWidth/4
// Step 2
let path = CGMutablePath()
path.addRoundedRect(in: CGRect(x: topLeft.x, y: topLeft.y,
width: topRight.x - topLeft.x, height: bottomLeft.y - topLeft.y),
cornerWidth: 20, cornerHeight: 20)
path.addRect(CGRect(origin: .zero, size: self.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = kCAFillRuleEvenOdd
// For Swift 4.2
//maskLayer.fillRule = .evenOdd
// Step 4
self.layer.mask = maskLayer
self.clipsToBounds = true
rectangle mask looks like this

Resources