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

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;
}
}

Related

Custom frame UIView [duplicate]

I want to achieve this result
So I need to add mask at bottom but Here What I got
Code I have tried
private func setupImageCutPath () {
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0 , y: self.imgView.frame.height))
path.addLine(to: CGPoint(x: self.imgView.frame.width , y: self.imgView.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.imgView.frame.width, y: 0))
// Multiple tried to check from where masking is comming
self.maskLayer.fillColor = UIColor.gray.cgColor
self.maskLayer.lineWidth = 1.0
self.maskLayer.borderColor = UIColor.gray.cgColor
self.maskLayer.borderWidth = 1.0
self.maskLayer.path = path.cgPath
self.imgView.layer.mask = self.maskLayer
}
After scroll First time I am getting correct result
I don't understand What I am missing Any Help ?
I solved issue with
path.move(to: .zero)
/* THIS HELPLS ME */ path.addLine(to: CGPoint(x: 0 , y: self.imgView.frame.height - imageCutAwayPart / 2 ))
path.addLine(to: CGPoint(x: self.imgView.frame.width , y: self.imgView.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.imgView.frame.width, y: 0))
But I don't understand why I need to move CGPoint(x: 0 , y: self.imgView.frame.height - imageCutAwayPart / 2 ) instead of this CGPoint(x: 0 , y: self.imgView.frame.height)
If any one know this please post as answer I will surely accept the answer
EDIT
There was silly but smart mistake in Storybaord
Mistake was there is UIView which contains Label shown in screenshot. The Part visible is is the UIView I thought it masking is not working properly. Still there is issue that very first time in pageview controller mask is not proper but after scroll one time it is perfect.
EDIT2 Thanks to #DonMag
Following is working solution for every case
class CutOfImageView:UIImageView {
let imageCutAwayPart:CGFloat = 80
let maskLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
self.setupImageCutPath()
self.layer.mask = self.maskLayer
}
private func setupImageCutPath () {
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0 , y: self.frame.height ))
path.addLine(to: CGPoint(x: self.frame.width , y: self.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.close()
self.maskLayer.path = path.cgPath
}
}

UIImageView Mask Layer not coming from corners Until Scroll PageViewController First time

I want to achieve this result
So I need to add mask at bottom but Here What I got
Code I have tried
private func setupImageCutPath () {
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0 , y: self.imgView.frame.height))
path.addLine(to: CGPoint(x: self.imgView.frame.width , y: self.imgView.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.imgView.frame.width, y: 0))
// Multiple tried to check from where masking is comming
self.maskLayer.fillColor = UIColor.gray.cgColor
self.maskLayer.lineWidth = 1.0
self.maskLayer.borderColor = UIColor.gray.cgColor
self.maskLayer.borderWidth = 1.0
self.maskLayer.path = path.cgPath
self.imgView.layer.mask = self.maskLayer
}
After scroll First time I am getting correct result
I don't understand What I am missing Any Help ?
I solved issue with
path.move(to: .zero)
/* THIS HELPLS ME */ path.addLine(to: CGPoint(x: 0 , y: self.imgView.frame.height - imageCutAwayPart / 2 ))
path.addLine(to: CGPoint(x: self.imgView.frame.width , y: self.imgView.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.imgView.frame.width, y: 0))
But I don't understand why I need to move CGPoint(x: 0 , y: self.imgView.frame.height - imageCutAwayPart / 2 ) instead of this CGPoint(x: 0 , y: self.imgView.frame.height)
If any one know this please post as answer I will surely accept the answer
EDIT
There was silly but smart mistake in Storybaord
Mistake was there is UIView which contains Label shown in screenshot. The Part visible is is the UIView I thought it masking is not working properly. Still there is issue that very first time in pageview controller mask is not proper but after scroll one time it is perfect.
EDIT2 Thanks to #DonMag
Following is working solution for every case
class CutOfImageView:UIImageView {
let imageCutAwayPart:CGFloat = 80
let maskLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
self.setupImageCutPath()
self.layer.mask = self.maskLayer
}
private func setupImageCutPath () {
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0 , y: self.frame.height ))
path.addLine(to: CGPoint(x: self.frame.width , y: self.frame.height - imageCutAwayPart))
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.close()
self.maskLayer.path = path.cgPath
}
}

CAShapeLayer groove path

I wanted to make a white color view with a triangle shaped pointer being grooved inside like this:
As shown in the image above, goal is to create a "rounded groove" inset into the whiteview
let pointerRadius:CGFloat = 4
pointerLayer = CAShapeLayer()
pointerLayer.path = pointerPathForContentSize(contentSize: bounds.size).cgPath
pointerLayer.lineJoin = kCALineJoinRound
pointerLayer.lineWidth = 2*pointerRadius
pointerLayer.fillColor = UIColor.white.cgColor
pointerLayer.strokeColor = UIColor.black.cgColor
pointerLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(pointerLayer)
But what I get is this:
But,if I set the stroke color to white
pointerLayer.strokeColor = UIColor.white.cgColor
In the groove I wanted to have a rounded edge in bottom (just like in the first pic) which no more remains visible when fillColor and strokeColor get matched (both white).
How can I fix it?
Is there any other way to achieve this?
Here is the code for pointer path:
private func pointerPathForContentSize(contentSize: CGSize) -> UIBezierPath
{
let rect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let width:CGFloat = 20
let height:CGFloat = 20
let path = UIBezierPath()
let startX:CGFloat = 50
let startY:CGFloat = rect.minY
path.move(to: CGPoint(x: startX , y: startY))
path.addLine(to: CGPoint(x: (startX + width*0.5), y: startY + height))
path.addLine(to: CGPoint(x: (startX + width), y: startY))
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.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
path.close()
return path
}
Since you have already outlined the shape you want by stroking the path, I think the simplest solution is probably to use the stroked and filled path as a mask.
For example, here is a rectangular red view:
And here is the same red view with the notch cut out of the top. This seems to be the sort of thing you're after:
What I did there was to mask the red view with a special mask view that draws the notch using .clear blend mode:
class MaskView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fill(self.bounds)
con.setBlendMode(.clear)
con.move(to: CGPoint(x:0, y:-4))
con.addLine(to: CGPoint(x:100, y:-4))
con.addLine(to: CGPoint(x:110, y:15))
con.addLine(to: CGPoint(x:120, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-20))
con.addLine(to: CGPoint(x: 0, y:-20))
con.closePath()
con.setLineJoin(.round)
con.setLineWidth(10)
con.drawPath(using: .fillStroke) // stroke it and fill it
}
}
So then when I'm ready to cut out the notch on the red view, I just say:
self.redView.mask = MaskView(frame:self.redView.bounds)

Animate UIBezierPaths- not working- Swift

I am trying to create an arrow that animates together. But, there are two problems:
1.The lines are not animating, the whole arrow is just appearing on screen
2.The arrow is filled in black, not just the outlines (which I am aiming for)
let shapeLayer = CAShapeLayer()
let bezierPath = UIBezierPath()
//set up lines
bezierPath.move(to: CGPoint(x: 50.5, y: 158.5))
bezierPath.addLine(to: CGPoint(x: 221.5, y: 158.5))
bezierPath.addLine(to: CGPoint(x: 171.5, y: 108.5))
bezierPath.addLine(to: CGPoint(x: 197.5, y: 82.5))
bezierPath.addLine(to: CGPoint(x: 292.5, y: 177.5))
bezierPath.addLine(to: CGPoint(x: 197.5, y: 272.5))
bezierPath.addLine(to: CGPoint(x: 171.5, y: 246.5))
bezierPath.addLine(to: CGPoint(x: 221.5, y: 196.5))
bezierPath.addLine(to: CGPoint(x: 50.5, y: 196.5))
bezierPath.addLine(to: CGPoint(x: 50.5, y: 158.5))
UIColor.black.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
shapeLayer.path = bezierPath.cgPath
// set up animation
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 2.5
shapeLayer.add(animation2, forKey: "drawLineAnimation")
eeView.layer.addSublayer(shapeLayer)
If anyone could help me with their of those that would be amazing. Any help would be immensely appreciated!! Thanks so much in advance.
Cheers,
Theo
The problem is this sequence:
shapeLayer.add(animation2, forKey: "drawLineAnimation")
eeView.layer.addSublayer(shapeLayer)
You cannot add an animation to a layer and then add the layer to the interface. You can only add animation to a layer that is already in the interface.
Also, you need to set the shape layer's fillColor and strokeColor.
Here's actual code. In one place, you would say something like this:
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
eeView.layer.addSublayer(shapeLayer)
self.shape = shapeLayer // self.shape is a property
Then, at some later time, you would say:
let shapeLayer = self.shape!
let bezierPath = UIBezierPath()
// ... rest of your code goes here: create path, add path to shape layer ...
// ... create animation, add animation to shape layer

Creating Triangle with UIBezierPath in Swift

I am trying to understand how to create a triangle shape with Swift. I found this code that creates a triangle.
class TriangleLayer: CAShapeLayer {
let innerPadding: CGFloat = 30.0
override init() {
super.init()
fillColor = Colors.red.CGColor
strokeColor = Colors.red.CGColor
lineWidth = 7.0
lineCap = kCALineCapRound
lineJoin = kCALineJoinRound
path = trianglePathSmall.CGPath
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var trianglePathSmall: UIBezierPath {
let trianglePath = UIBezierPath()
trianglePath.moveToPoint(CGPoint(x: 5.0 + innerPadding, y: 95.0)) // #1
trianglePath.addLineToPoint(CGPoint(x: 50.0, y: 12.5 + innerPadding)) // #2
trianglePath.addLineToPoint(CGPoint(x: 95.0 - innerPadding, y: 95.0)) // #3
trianglePath.closePath()
return trianglePath
}
And this code creates a shape like this
in the middle of the screen.
I tried to tweak and play around with it to understand how it works; however, at this point I realised that I got lost with the logic quite a bit. I placed the CGPoints of above triangle on an x-y axis in my head and it seems something like:
#1 x:35, y:95 #3 x:65, y:95
  #2 x:50, y: 42.5
But the triangle is created upside-down if I place the dots on the x-y axis.
What I want to achieve is what the axis tells, and I want to achieve..
. . .
<like this. not this>
. . .
You just have the axes in your head upside down. The coordinate system starts at 0,0 and extends right in X and down in Y.
So your points are really:
#2 x:50, y: 42.5
#1 x:35, y:95 #3 x:65, y:95
to get your desired triangle you'd have something like:
#1 x:35, y:95 #3 x:65, y:95
#2 x:50, y: 147.5
Result triangles
Code in swift5
//TriangleView
extension UIView {
func setRightTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width //you can use triangleView.frame.size.height
let path = CGMutablePath()
path.move(to: CGPoint(x: heightWidth/2, y: 0))
path.addLine(to: CGPoint(x:heightWidth, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth/2, y:heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setLeftTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: heightWidth/2, y: 0))
path.addLine(to: CGPoint(x:0, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth/2, y:heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setUpTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: heightWidth))
path.addLine(to: CGPoint(x:heightWidth/2, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth, y:heightWidth))
path.addLine(to: CGPoint(x:0, y:heightWidth))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
func setDownTriangle(targetView:UIView?){
let heightWidth = targetView!.frame.size.width
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x:heightWidth/2, y: heightWidth/2))
path.addLine(to: CGPoint(x:heightWidth, y:0))
path.addLine(to: CGPoint(x:0, y:0))
let shape = CAShapeLayer()
shape.path = path
shape.fillColor = UIColor.blue.cgColor
targetView!.layer.insertSublayer(shape, at: 0)
}
}
Swift 4.*
The easiest way of doing it by using AutoLayout:
Open your Storyboard and drag a UIView in UIViewController, position it and set the size as you wish (that's the place where the triangle will be). Set the view background to be transparent.
Create a new class, you can name it however you want (I named mine TriangleView). This will be the content of that class:
class TriangleView: UIView {
// predefined variables that can be changed
var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.5)
var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.5)
var firstGradientColor: UIColor = UIColor.white
var secondGradientColor: UIColor = UIColor.blue
let gradient: CAGradientLayer = CAGradientLayer()
override func draw(_ rect: CGRect) {
let height = self.layer.frame.size.height
let width = self.layer.frame.size.width
// draw the triangle
let path = UIBezierPath()
path.move(to: CGPoint(x: width / 2, y: 0))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.close()
// draw the triangle 'upside down'
// 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()
// add path to layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = 1.0
// Add the gradient for the view background if needed
gradient.colors = [firstGradientColor.cgColor, secondGradiendColor.cgColor]
gradient.startPoint = startPoint
gradient.endPoint = endPoint
gradient.frame = self.bounds
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
}
Go to your Storyboard, select the UIView and in Identity Inspector write the class name TriangleView
Enjoy your triangle! :)

Resources