How to draw same size dotted line on UIView - ios

Can someone please guide how I can draw same size dotted line on UIView for all sides?
I am using below code to draw line.
let dashBorder = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: 0, width: frameSize.width, height: frameSize.height)
dashBorder.bounds = shapeRect
dashBorder.position = CGPoint(x: frameSize.width/2, y: frameSize.height/2)
dashBorder.fillColor = UIColor.clear.cgColor
dashBorder.strokeColor = UIColor.blue.cgColor
dashBorder.lineJoin = CAShapeLayerLineJoin.round
dashBorder.lineDashPattern = [3, 3]
dashBorder.path = UIBezierPath(roundedRect: shapeRect, cornerRadius: 10.0).cgPath
layer.addSublayer(dashBorder)
It is shown as above
as you can see the line width for left and right edge width are not same as top and bottom edge height. Is there any way if I want to make same for all sides like below?

The trick is to inset the drawing frame with half the value of stroke width. Or half of your stroke will be clipped by the view, since bezier path centers the stroke to drawing line.
I also change your frame slightly and removed position. Here is the updated code.
let lineWidth = 3.0
let dashBorder = CAShapeLayer()
let frameSize = bounds.insetBy(dx: lineWidth/2, dy: lineWidth/2)
dashBorder.frame = bounds
dashBorder.fillColor = UIColor.clear.cgColor
dashBorder.strokeColor = UIColor.blue.cgColor
dashBorder.lineJoin = CAShapeLayerLineJoin.round
dashBorder.lineDashPattern = [3, 3]
dashBorder.lineWidth = lineWidth
dashBorder.path = UIBezierPath(roundedRect: frameSize, cornerRadius: 10.0).cgPath
layer.addSublayer(dashBorder)

Related

CAShapeLayer can't use rect path?

I draw a dash border
let centerView = UIView()
centerView.frame = CGRect(x: 100, y: 100, width: 80, height: 50)
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.gray.cgColor
shapeLayer.lineDashPattern = [3, 2]
shapeLayer.lineWidth = 0.5
shapeLayer.path = UIBezierPath(
roundedRect: centerView.bounds,
cornerRadius: 22
).cgPath
view.addSubview(centerView)
centerView.layer.addSublayer(shapeLayer)
When I tap Debug View Hierarchy
Runtime issue:
A CAShapeLayer is used with a path that's a rect, a rounded-rect, or an ellipse. Instead, use an appropriately transformed plain CALayer with cornerRadius set.
How to fix it?
Use the layer's shadowPath instead of path
like this:
shapeLayer.shadowPath = UIBezierPath(
roundedRect: centerView.bounds,
cornerRadius: 22
).cgPath

Issue with rounded corners of ImageView with gradient colour

I created a rectangle using following code and now I need to rounded the corners of this rectangle. I set layer.cornerRadius also, can anyone help me?
My code as below,
private func setGradientBorder(_ ivUser:UIImageView) {
ivUser.layer.masksToBounds = true
ivUser.layer.cornerRadius = ivUser.frame.width / 2
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: ivUser.frame.size)
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
let maskPath = UIBezierPath(roundedRect: ivUser.bounds,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: ivUser.frame.width/2, height: ivUser.frame.height/2)).cgPath
let shape = CAShapeLayer()
shape.lineWidth = 2
shape.path = maskPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
ivUser.layer.addSublayer(gradient)
}
output: gradient does not display properly rounded
This is how UIBezierPath works. Half of the line width will go on one side of the actual path, and half will go on another. Thats why it looks kinda cut out.
You will need to inset your paths rect by half of the line width, something like this:
let lineWidth: CGFloat = 2
let maskPath = UIBezierPath(roundedRect: ivUser.bounds.insetBy(dx: lineWidth/2, dy: lineWidth/2),
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: ivUser.frame.width/2, height: ivUser.frame.height/2)).cgPath

Thin border when using CAShapeLayer as mask for CAShapeLayer

In Swift, I have two semi-transparent circles, both of which are CAShapeLayer. Since they are semi-transparent, any overlap between them becomes visible like so:
Instead, I want them to visually "merge" together. The solution I have tried is to use circle 2 as a mask for circle 1, therefore cutting away the overlap.
This solution is generally working, but I get a thin line on the outside of circle 2:
My question: How can I get rid of the thin, outside line on the right circle? Why is it even there?
The code is as follows (Xcode playground can be found here):
private let yPosition: CGFloat = 200
private let circle1Position: CGFloat = 30
private let circle2Position: CGFloat = 150
private let circleDiameter: CGFloat = 200
private var circleRadius: CGFloat { return self.circleDiameter/2.0 }
override func loadView() {
let view = UIView()
view.backgroundColor = .black
self.view = view
let circle1Path = UIBezierPath(
roundedRect: CGRect(
x: circle1Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle2Path = UIBezierPath(
roundedRect: CGRect(
x: circle2Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle1Layer = CAShapeLayer()
circle1Layer.path = circle1Path.cgPath
circle1Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
let circle2Layer = CAShapeLayer()
circle2Layer.path = circle2Path.cgPath
circle2Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
self.view.layer.addSublayer(circle1Layer)
self.view.layer.addSublayer(circle2Layer)
//Create a mask from the surrounding rectangle of circle1, and
//then cut out where it overlaps circle2
let maskPath = UIBezierPath(rect: CGRect(x: circle1Position, y: yPosition, width: circleDiameter, height: circleDiameter))
maskPath.append(circle2Path)
maskPath.usesEvenOddFillRule = true
maskPath.lineWidth = 0
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.black.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
circle1Layer.mask = maskLayer
}
If both CAShapeLayers have the same alpha value, you could place them inside a new parent CALayer then set the alpha of the parent instead.

Centre Align CAShape layer in a UIView

The following is a image for which the the code does not align the centre of the UIView , white color.
CAShapeLayer *layer = [CAShapeLayer layer];
layer.anchorPoint=CGPointMake(0.5, 0.5);
layer.path=[UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 75.0, 75.0)].CGPath;
layer.fillColor =[UIColor redColor].CGColor;
[self.shapeView.layer addSublayer:layer];
anchorPoint is not for setting the position of CAShapeLayer, use position instead.
let layer = CAShapeLayer()
layer.borderColor = UIColor.blackColor().CGColor
layer.borderWidth = 100
layer.bounds = CGRectMake(0, 0, 50, 50)
layer.position = myView.center
layer.fillColor = UIColor.redColor().CGColor
view.layer.addSublayer(layer)
Edit
Sorry for such a rough answer before, the code above may not work as you want, here is the correct answer I just come out. A important thing to note is: How you draw the oval path did effect the position of your CAShapeLayer
let layer = CAShapeLayer()
myView.layer.addSublayer(layer)
layer.fillColor = UIColor.redColor().CGColor
layer.anchorPoint = CGPointMake(0.5, 0.5)
layer.position = CGPointMake(myView.layer.bounds.midX, myView.layer.bounds.midY)
layer.path = UIBezierPath(ovalInRect: CGRectMake(-75.0 / 2, -75.0 / 2, 75.0, 75.0)).CGPath
From Apple Document,
The oval path is created in a clockwise direction (relative to the default
coordinate system)
In other word, the oval path is drawn from the edge instead of the center, so you must take into accound the radius of the oval you are drawing. In your case, you need to do this:
layer.path = UIBezierPath(ovalInRect: CGRectMake(-75.0 / 2, -75.0 / 2, 75.0, 75.0)).CGPath
Now we ensure that the center of drawn path is equal to the center of CAShapeLayer. Then we can use position property to move the CAShapeLayer as we want. The image below could help you understand.
CAShapeLayer *layer = [CAShapeLayer layer];
layer.anchorPoint=CGPointMake(0.5, 0.5);
layer.path=[UIBezierPath bezierPathWithOvalInRect:CGRectMake((self.shapeView.frame.size.width/2)-37.5, (self.shapeView.frame.size.height/2)-37.5, 75.0, 75.0)].CGPath;
//37.5 means width & height divided by 2
layer.fillColor =[UIColor redColor].CGColor;
[self.shapeView.layer addSublayer:layer];
For those still have issue with centering this approach worked for me:
1- Set the shape layer frame equal to parent view frame.
shapeLayer.frame = view.frame
2- remove the shape layer position property.
// shapeLayer.position = CGPoint(x: ,y: )
3- set x and y value of your bezier path equal to zero.
UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.width, height:
self.height))
Only these should be enough, get rid of the rest.
======
If that didn't work, then try this one:
#IBDesignable class YourView: UIView {
private var shapeLayer: CAShapeLayer!
override func draw(_ rect: CGRect) {
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = bezierPath.cgPath
self.shapeLayer.fillColor = UIColor.blue.cgColor
let bezierPathHeight = self.bezierPath.cgPath.boundingBox.height
let bezierPathWidth = self.bezierPath.cgPath.boundingBox.width
self.shapeLayer.setAffineTransform(CGAffineTransform.init(translationX: self.bounds.width / bezierPathWidth, y: self.bounds.height / bezierPathHeight))
self.shapeLayer.setAffineTransform(CGAffineTransform.init(scaleX: self.bounds.width / bezierPathHeight, y: self.bounds.height / bezierPathHeight))
self.layer.addSublayer(self.shapeLayer)
}
I've encounter same case like this. The final resolution is shapeLayer.needsDisplayOnBoundsChange = true and update CAShapeLayer frame in layoutSubviews, CenteredAlignShapeView can adjust when using Auto Layout.
class CenteredAlignShapeView: UIView {
public let dot = CAShapeLayer()
init() {
super.init(frame: .zero)
dot.needsDisplayOnBoundsChange = true
layer.insertSublayer(dot, at: 0)
dot.fillColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
dot.frame = bounds
let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.width/2, y: bounds.height/2), radius: 4, startAngle: 0, endAngle: CGFloat.pi*2, clockwise: true)
dot.path = circlePath.cgPath
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

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