How to draw a border around two overlapping UIViews - uiview

I have two UIViews that create a unique shape when overlapping and I want to draw a border around the combined views.
What is the proper method for doing this?
The UIViews are:
Circle image view to display a user profile image
A rectangle view that will container user profile data (i.e. name, dob, etc)
Here is an image of what the two view together will look like:

Ok figured it out by doing the following:
Setting a border on the rectangle UIView
Creating a CAShapeLayer with UIBezierPath to draw a semi-circle and add it to the circle UIView's layer in the drawRect method:
override func draw(_ rect: CGRect) {
super.draw(rect)
let shapeLayer = CAShapeLayer()
let topSemiCirclePath = UIBezierPath(arcCenter: userImageView.center, radius: userImageView.bounds.size.width / 2.0, startAngle: CGFloat(Double.pi), endAngle: CGFloat(Double.pi / 180), clockwise: true)
topSemiCirclePath.lineWidth = 2.0
UIColor.lightGray.setStroke()
topSemiCirclePath.stroke()
shapeLayer.path = topSemiCirclePath.cgPath
userImageView.layer.addSublayer(shapeLayer)
}

Related

How to clip outer area of circle in swift?

I have drawn semi circle in swift using UIBezierPath the parent is UIImageView now i am looking to clip the outer area of circle. I have tried clipsToBounds but it didn't works.
let path = UIBezierPath(arcCenter: CGPoint(x: -60, y: imgView.frame.size.height/2),
radius: imgView.frame.size.width*1.3,
startAngle: CGFloat(90).toRadians(),
endAngle: CGFloat(270).toRadians(),
clockwise: false)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 5
imgView.clipsToBounds = true
imgView.layer.addSublayer(shapeLayer)
extension CGFloat {
func toRadians() -> CGFloat {
return self * .pi / 180
}
}
EDIT:1 i have tried imgView.layer.mask = shapeLayer but it wipes out strokeColor
try :- imgView.layer.maskstoBounds = true
A shape layer either draws on top of another layer or, if you use it as a mask, clips another layer. It can't do both at the same time.
You need two shape layers. Leave the one you have now, stroked in blue, as a sublayer of your image view layer.
Create a second shape layer, install another circle path into it, set it's fill color to any opaque color, and use that second shape layer as a mask on your image view. You might need to add a stroke color and borderWidth to your mask layer so it is big enough not to mask your blue circle. (I forget if strokes are drawn inside, outside, or centered on the edge of their shape boundaries. I think they are centered, so unless you also stroke your mask layer at the same thickness, it will clip the outer half of your blue circle shape layer, but I'd have to try it to be sure.)

How I can create this circular shape in iOS Swift4?

I have to create this shape like the below image in a UIView. But I am not get any idea how to draw this shape. I am trying to draw this shape using UIBezierPath path with CAShapeLayer the circular line draw successfully but I am unable to draw the circular points and circular fill colour. Can anyone please suggest me how I can achieve this type shape using any library or UIBezierPath.
This is my code which I am using try to draw this circular shape.
class ViewController: UIViewController {
var firstButton = UIButton()
var mylabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.creatingLayerWithInformation()
}
func creatingLayerWithInformation(){
let safeAreaHeight = self.view.safeAreaInsets.top
let navBarHeight = self.navigationController?.navigationBar.frame.height
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: 0, yPoint: navBarHeight!, layerColor: UIColor.green, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: self.view.frame.width, yPoint: self.view.frame.height - 150, layerColor: UIColor.blue, fillcolor: .clear)
let aa = self.view.frame.width * 0.72
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+aa, yPoint: navBarHeight!+5, layerColor: UIColor.blue, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+15, yPoint: navBarHeight!+aa, layerColor: UIColor.blue, fillcolor: .clear)
}
func addLayer(isClockWise: Bool, radius: CGFloat, xPoint: CGFloat, yPoint: CGFloat, layerColor: UIColor, fillcolor: UIColor) {
let pi = CGFloat(Float.pi)
let start:CGFloat = 0.0
let end :CGFloat = 20
// circlecurve
let path: UIBezierPath = UIBezierPath();
path.addArc(
withCenter: CGPoint(x:xPoint, y:yPoint),
radius: (radius),
startAngle: start,
endAngle: end,
clockwise: isClockWise
)
let layer = CAShapeLayer()
layer.lineWidth = 3
layer.fillColor = fillcolor.cgColor
layer.strokeColor = layerColor.cgColor
layer.path = path.cgPath
self.view.layer.addSublayer(layer)
}}
But I am getting the below result.
Please suggest me how I can achieve this shape. Correct me if I am doing anything wrong. If there is any library present then also please suggest. Please give me some solution.
Advance thanks to everyone.
There are many ways to achieve this effect, but a simple solution is to not draw the large circle as a single arc, but rather as a series of arcs that start and stop at the edges of the smaller circles. To do this, you need to know what the offset is from the inner circles. Doing a little trigonometry, you can calculate that as:
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
Thus:
let path = UIBezierPath()
path.move(to: point(from: arcCenter, radius: mainRadius, angle: startAngle))
let anglePerChoice = (endAngle - startAngle) / CGFloat(choices.count)
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
var from = startAngle
for index in 0 ..< choices.count {
var to = from + anglePerChoice / 2 - angleOffset
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
to = from + anglePerChoice
from += anglePerChoice / 2 + angleOffset
path.move(to: point(from: arcCenter, radius: mainRadius, angle: from))
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
from = to
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
layer.addSublayer(shapeLayer)
Where:
func point(from point: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: point.x + radius * cos(angle),
y: point.y + radius * sin(angle))
}
That yields:
So, when you then add the inner circles:
While the above is simple, it has limitations. Specifically if the lineWidth of the big arc was really wide in comparison to that of the small circles, the breaks in the separate large arcs won’t line up nicely with the edges of the small circles. E.g. imagine that the small circles had a radius 22 points, but that the big arc’s stroke was comparatively wide, e.g. 36 points.
If you have this scenario (not in your case, but for the sake of future readers), the other approach, as matt suggested, is to draw the big arc as a single stroke, but then mask it using the paths for the small circles.
So, imagine that you had:
a single CAShapeLayer, called mainArcLayer, for the big arc; and
an array of UIBezierPath, called smallCirclePaths, for all of the small circles.
You could then create a mask in layoutSubviews of your UIView subclass (or viewDidLayoutSubviews in your UIViewController subclass) like so:
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
smallCirclePaths.forEach { path.append($0) }
let mask = CAShapeLayer()
mask.fillColor = UIColor.white.cgColor
mask.strokeColor = UIColor.clear.cgColor
mask.lineWidth = 0
mask.path = path.cgPath
mask.fillRule = .evenOdd
mainArcLayer.mask = mask
}
That yields:
This is a slightly more generalized solution to this problem.
Think about the problem in the simplest possible form. Imagine a straight line with one small circle superimposed over the middle of it.
Let's divide our thinking into three layers:
The background that needs to show through
The layer that holds the straight line
The layer that holds the small circle
Calculate the location of the small circle layer relative to the straight line layer.
Place the small circle layer at that location. Okay, but the straight line shows through.
Go back to the straight line layer. Give it a mask. Construct that mask with a transparent circle at exactly the location of the small circle layer.
Now the mask "punches a hole" through the straight line — at exactly the place where the circle covers it. Thus we appear to see through the circle to the background, because the straight line is missing at exactly that place.
In real life there will be multiple circle layers and the mask will have multiple transparent circles and the straight line will be a curve, but that is a minor issue once you have the hole-punching worked out.

How to resize a view across all screen sizes?

I am creating a simple timer app and want to resize the timer circle to the size of the view with the rounded corners. The timer path is added within the background views class.
On my iPhone XS Max, it looks like this:
But, on the iPhone SE simulator, it looks like this:
How can I ensure the timer circles resize correctly?
This is my code for the sizing of the circle paths:
private func addTimerCircle() {
let translateAmount = -(CGFloat.pi/2)
let circlePath = UIBezierPath(arcCenter: CGPoint.zero, radius: (self.frame.width - 64)/2, startAngle: translateAmount, endAngle: (2*CGFloat.pi)+translateAmount, clockwise: true)
addTrackLayer(withPath: circlePath)
}
private func addTrackLayer(withPath circlePath: UIBezierPath) {
let trackLayer = CAShapeLayer()
trackLayer.path = circlePath.cgPath
trackLayer.strokeColor = UIColor.red.withAlphaComponent(0.5).cgColor
trackLayer.lineWidth = 10
trackLayer.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
trackLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(trackLayer)
}
Why does the circlePath not resize based on frame.width on the different screen sizes?
Thanks for any help!
View controller views are loaded with the size they have in Interface Builder, and once they are added to the hierarchy the view is resized to the actual size.
I'm guessing you're calling addTimerCircle() on viewDidLoad(), which happens before the final sizing is applied.
You could override viewDidLayoutSubviews(), which will be called when the view size changes, and add/update the shape there, but my suggestion would be to subclass UIView, add the shape layer as a property, and update it's frame on layoutSubviews.

How to apply multiple masks to UIView

I have a question about how to apply multiple masks to a UIView that already has a mask.
The situation:
I have a view with an active mask that creates a hole in its top left corner, this is a template UIView that is reused everywhere in the project. Later in the project I would like to be able to create a second hole but this time in the bottom right corner, this without the need to create a completely new UIView.
The problem:
When I apply the bottom mask, it of course replaces the first one thus removing the top hole ... Is there a way to combine them both? And for that matter to combine any existing mask with a new one?
Thank you in advance!
Based on #Sharad's answer, I realised that re-adding the view's rect would enable me to combine the original and new mask into one.
Here is my solution:
func cutCircle(inView view: UIView, withRect rect: CGRect) {
// Create new path and mask
let newMask = CAShapeLayer()
let newPath = UIBezierPath(ovalIn: rect)
// Create path to clip
let newClipPath = UIBezierPath(rect: view.bounds)
newClipPath.append(newPath)
// If view already has a mask
if let originalMask = view.layer.mask,
let originalShape = originalMask as? CAShapeLayer,
let originalPath = originalShape.path {
// Create bezierpath from original mask's path
let originalBezierPath = UIBezierPath(cgPath: originalPath)
// Append view's bounds to "reset" the mask path before we re-apply the original
newClipPath.append(UIBezierPath(rect: view.bounds))
// Combine new and original paths
newClipPath.append(originalBezierPath)
}
// Apply new mask
newMask.path = newClipPath.cgPath
newMask.fillRule = kCAFillRuleEvenOdd
view.layer.mask = newMask
}
This is code I have used in my project to create one circle and one rectangle mask in UIView, you can replace the UIBezierPath line with same arc code :
func createCircleMask(view: UIView, x: CGFloat, y: CGFloat, radius: CGFloat, downloadRect: CGRect){
self.layer.sublayers?.forEach { ($0 as? CAShapeLayer)?.removeFromSuperlayer() }
let mutablePath = CGMutablePath()
mutablePath.addArc(center: CGPoint(x: x, y: y + radius), radius: radius, startAngle: 0.0, endAngle: 2 * 3.14, clockwise: false)
mutablePath.addRect(view.bounds)
let path = UIBezierPath(roundedRect: downloadRect, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 5, height: 5))
mutablePath.addPath(path.cgPath)
let mask = CAShapeLayer()
mask.path = mutablePath
mask.fillRule = kCAFillRuleEvenOdd
mask.backgroundColor = UIColor.clear.cgColor
view.layer.mask = mask
}
Pass your same UIView, it removes previous layers and applies new masks on same UIView.
Here mask.fillRule = kCAFillRuleEvenOdd is important. If you notice there are 3 mutablePath.addPath() functions, what kCAFillRuleEvenOdd does is, it first creates a hole with the arc then adds Rect of that view's bound and then another mask to create 2nd hole.
You can do something like the following, if you don't only have "simple shapes" but actual layers from e.g. other views, like UILabel or UIImageView.
let maskLayer = CALayer()
maskLayer.frame = viewToBeMasked.bounds
maskLayer.addSublayer(self.imageView.layer)
maskLayer.addSublayer(self.label.layer)
viewToBeMasked.bounds.layer.mask = maskLayer
So basically I just create a maskLayer, that contains all the other view's layer as sublayer and then use this as a mask.

Animated CGAffineTransform(rotate) of a UIBezier path that is under func draw()

I want to make an animated of a path with CGAffineTransform(rotationAngle:) that is under a UIView`s draw. I couldn't find any way to do it...
This is my code:
public override func draw(_ rect: CGRect) {
//Drawing a dot at the center
let dotRadius = max(bounds.width/15, bounds.height/15)
let dotWidth:CGFloat = 10
let dotCenter = CGPoint(x:bounds.width/2, y: bounds.height/2)
let dotStartAngle: CGFloat = 0
let dotEndAngle: CGFloat = CGFloat(2 * M_PI) // π
var path = UIBezierPath(arcCenter: dotCenter,
radius: dotRadius/2,
startAngle: dotStartAngle,
endAngle: dotEndAngle,
clockwise: true)
path.lineWidth = dotWidth
counterColor.setStroke()
counterColor.setFill()
path.stroke()
path.fill()
arrowLayer.frame = CGRect(x:bounds.width/2, y: bounds.height/2, width: bounds.width, height: bounds.height)
arrow.path = UIBezierPath(rect: CGRect(x: 0, y:0 - 1, width: -250, height: 1)).cgPath
arrow.backgroundColor = UIColor.red.cgColor
arrow.fillColor = UIColor.clear.cgColor
arrow.strokeColor = counterColor.cgColor
arrow.anchorPoint = CGPoint(x:0.5, y:0.5)
arrow.lineWidth = 3
arrow.setAffineTransform(CGAffineTransform(rotationAngle:self.radians(arrowDegree)))
arrowLayer.addSublayer(arrow)
self.layer.addSublayer(arrowLayer)
}
And this is what I'm trying to do:
func rotateButton() {
UIView.animate(withDuration: 1, animations: {
self.arrow.setAffineTransform(CGAffineTransform(rotationAngle:self.radians(self.arrowDegree + 10)))
self.setNeedsDisplay()
})
}
I don't know if I was specific.. tell me if more information is needed.
I would recommend that you separate the animation from the drawing. Overriding draw() is generally suited for custom drawing but ill-suited for any type of animation.
Based on the properties that's being assigned to arrow it looks like it's a CAShapeLayer. This is good because it means that there is already some separation between the two.
However, it's not a good idea to add subviews or sublayers inside of draw(). It should only be used for drawing. Otherwise these things are going to get re-added every time the view is redrawn. In this case it's less noticeable because the same layer is added over and over. But it should still be avoided.
You can still use draw() to render the centered dot. But it's not expected to be part of the animation.
As for the rotation animation; unless I greatly misremember the underlying mechanics, standalone layers (those that aren't backing a view (most likely you've created them)) don't care about the UIView animation context. Instead you would use a Core Animation level API to animate them. This could either be done by changing the property within a CATransaction, or by creating a CABasicAnimation from one angle to the other and adding it to the layer.
Note that adding an animation doesn't change the "model" value (the properties of the layer), and that once the animation finishes the layer would render as it did before the animation was added. If the layer is expected to animate to a value and stay there you would both add the animation and update the layer property.

Resources