How to clip outer area of circle in swift? - ios

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.)

Related

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.

Make transparent "star"-looking hole in UIView

Let's say I have some UIView and also I have a "closed" UIBezierPath inside of that view. What I want to achieve is to have everything "outside" this path be white and inside - transparent (like a hole of that path-shape). Solutions like this don't work, for some reason, part of the shape inside is also white.
You can use the view.layer.mask property to make holes in a a view.
I'm using this piece of code to make mask from images, but you could also just draw you UIBezierPath to CALayer and use that instead.
/*
Get a layer that can be used to mask an image. All pixels with alpha 0 in mask image will be hidden
use as:
self.view.layer.mask = <Mask returned from this call>
self.view.layer.masksToBounds = YES;
*/
+(CALayer*)getMaskLayerFromImage:(UIImage*)image
{
CALayer *mask = [CALayer layer];
mask.contents =(id) image.CGImage;
mask.frame = CGRectMake(0, 0, image.size.width, image.size.height);
return mask;
}
I have achieved this thing by using UIBezierPath and CAShapeLayer
I am taking outlet of view from storyboard.
#IBOutlet weak var myView: UIView!
Create an object of UIBezierPath()
var path = UIBezierPath()
Create a method which take center of view and we create another UIBezierPath() as circlePath which is circle and we append the circle on previous UIBezierPath() path.
Know take a CAShapeLayer and cut the circlePath
Change center point to draw circle on specific point.
func overLay() {
let sizes = CGSize(width: 30, height: 30)
let circlePath = UIBezierPath(ovalIn: CGRect(origin: self.view.center, size: sizes))
path.append(circlePath)
let maskLayer = CAShapeLayer() //create the mask layer
maskLayer.path = path.cgPath // Give the mask layer the path you just draw
maskLayer.fillRule = kCAFillRuleEvenOdd // Cut out the intersection part
myView.layer.mask = maskLayer
}

How to draw a border around two overlapping UIViews

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

Swift 3: CAShapeLayer mask not animating for pie chart animation

I have code that successfully creates a pie chart given arbitrary input data. It produces a pie chart that looks like this:
What I want:
I wish to create an animation that draws those pieces of the pie chart in a sequential order. That is, I wish to perform a "clock swipe" animation of the pie chart. The screen should start of black and then gradually fill to produce the above image of the pie chart.
The problem:
I am trying to create a layer mask for each pie chart piece and animate the layer mask so that the pie chart piece fills in an animation. However, when I implement the layer mask, it does not animate and the screen remains black.
The code:
class GraphView: UIView {
// Array of colors for graph display
var colors: [CGColor] = [UIColor.orange.cgColor, UIColor.cyan.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]
override func draw(_ rect: CGRect) {
drawPieChart()
}
func drawPieChart() {
// Get data from file, yields array of numbers that sum to 100
let data: [String] = getData(from: "pie_chart")
// Create the pie chart
let pieCenter: CGPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
let radius = frame.width / 3
let rad = 2 * Double.pi
var lastEnd: Double = 0
var start: Double = 0
for i in 0...data.count - 1 {
let size = Double(data[i])! / 100
let end: Double = start + (size * rad)
// Create the main layer
let path = UIBezierPath()
path.move(to: pieCenter)
path.addArc(withCenter: pieCenter, radius: radius, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = colors[i]
// Create the mask
let maskPath = CGMutablePath()
maskPath.move(to: pieCenter)
maskPath.addArc(center: pieCenter, radius: radius, startAngle: CGFloat(start), endAngle: CGFloat(end), clockwise: true)
let shapeLayerMask = CAShapeLayer()
shapeLayerMask.path = maskPath
shapeLayerMask.fillColor = colors[i]
shapeLayerMask.lineWidth = radius
shapeLayerMask.strokeStart = 0
shapeLayerMask.strokeEnd = 1
// Set the mask
shapeLayer.mask = shapeLayerMask
shapeLayer.mask!.frame = shapeLayer.bounds
// Add the masked layer to the main layer
self.layer.addSublayer(shapeLayer)
// Animate the mask
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = shapeLayerMask.strokeStart
animation.toValue = shapeLayerMask.strokeEnd
animation.duration = 4
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.isRemovedOnCompletion = false
animation.autoreverses = false
shapeLayerMask.add(animation, forKey: nil)
}
start = end
}
}
}
Again, this code just yields a blank screen. If you comment out the mask related code and add shapeLayer as the subview, you will see the un-animated pie chart.
What am I doing wrong and how can I get my pie chart to animate?
The sublayers must be added to a parent layer before they can be masked:
// Code above this line adds the pie chart slices to the parentLayer
for layer in parentLayer.sublayers! {
// Code in this block creates and animates a mask for each layer
}
// Add the parent layer to the highest parent view/layer
That's really all there is to it. You must ensure that a one-to-one correspondence exists between pie chart slices and sublayers.
The reason for this is because you cannot mask multiple views/layers with one instance of a layer mask. When you do this, the layer mask becomes part of the Layer Hierarchy of the layers that you are trying mask. A parent-child relationship between the mask and the layer to-be-masked must be established for this to work.

How to add a border to a circular image with mask

This is my attempt:
func round() {
let width = bounds.width < bounds.height ? bounds.width : bounds.height
let mask = CAShapeLayer()
mask.path = UIBezierPath(ovalInRect: CGRectMake(bounds.midX - width / 2, bounds.midY - width / 2, width, width)).CGPath
self.layer.mask = mask
// add border
let frameLayer = CAShapeLayer()
frameLayer.path = mask.path
frameLayer.lineWidth = 4.0
frameLayer.strokeColor = UIColor.whiteColor().CGColor
frameLayer.fillColor = nil
self.layer.addSublayer(frameLayer)
}
It works on the iphone 6 simulator (storyboard has size of 4.7), but on the 5s and 6+ it looks weird:
Is this an auto layout issue? Without the border, auto layout works correct. This is my first time working with masks and so I am not sure if what I have done is correct.
round function is called in viewDidLayoutSubviews.
Any thoughts?
If you have subclassed UIImageView, for example, you can override layoutSubviews such that it (a) updates the mask; (b) removes any old border; and (c) adds a new border. In Swift 3:
import UIKit
#IBDesignable
class RoundedImageView: UIImageView {
/// saved rendition of border layer
private weak var borderLayer: CAShapeLayer?
override func layoutSubviews() {
super.layoutSubviews()
// create path
let width = min(bounds.width, bounds.height)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
// update mask and save for future reference
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
// create border layer
let frameLayer = CAShapeLayer()
frameLayer.path = path.cgPath
frameLayer.lineWidth = 32.0
frameLayer.strokeColor = UIColor.white.cgColor
frameLayer.fillColor = nil
// if we had previous border remove it, add new one, and save reference to new one
borderLayer?.removeFromSuperlayer()
layer.addSublayer(frameLayer)
borderLayer = frameLayer
}
}
That way, it responds to changing of the layout, but it makes sure to clean up any old borders.
By the way, if you are not subclassing UIImageView, but rather are putting this logic inside the view controller, you would override viewWillLayoutSubviews instead of layoutSubviews of UIView. But the basic idea would be the same.
--
By the way, I use a mask in conjunction with this shape layer because if you merely apply rounded corners of a UIView, it can result in weird artifacts (look at very thin gray line at lower part of the circular border):
If you use the bezier path approach, no such artifacts result:
For Swift 2.3 example, see earlier revision of this answer.
The easiest way is to manipulate the layer of the image view itself.
imageView.layer.cornerRadius = imageView.bounds.size.width / 2.0
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor.whiteColor.CGColor
imageView.layer.masksToBounds = true
You can include this in viewDidLayoutSubviews or layoutSubviews to make sure the size is always correct.
NB: Maybe this technique makes your circle mask obsolete ;-). As a rule of thumb, always choose the simplest solution.

Resources