Don't want bezier path get clipped by view frame - ios

I thought that Bezier paths don't get clipped by the containing view, unless explicitly specified. I've verified in storyboard and can confirm that "Clip subviews" is unchecked. Also I've added clipsToBounds = falsein drawRect(),
Is there a way to not clip the bezier path at edges of containing view?
I don't want to have a clipped circle like shown in the image:
Here's my code:
override func drawRect(rect: CGRect) {
let lineWidth: CGFloat = 6
let color: UIColor = UIColor.yellowColor()
clipsToBounds = false
let haloRectFrame = CGRectMake(0, 0, bounds.width, bounds.height)
let haloPath = UIBezierPath(ovalInRect: haloRectFrame)
haloPath.lineWidth = lineWidth
color.set()
haloPath.stroke()

Ok, I've implemented two solutions. First solution utilises Bezier path with stroke. Here the stroke gets' clipped, so to fix it I've reduced the frame size. The second solution utilises CAShapeLayer, a subclass of CALayer. Here I use Bezier path, but since CAShapeLayer doesn't get clipped I don't need to reduce the frame size.
Solution 1 (Bezier path with stroke):
override func drawRect(rect: CGRect) {
let haloRectFrame = CGRectMake(
lineWidth / 2,
lineWidth / 2,
rect.width - lineWidth,
rect.height - lineWidth)
let haloPath = UIBezierPath(ovalInRect: haloRectFrame)
haloPath.lineWidth = lineWidth
color.set()
haloPath.stroke()
}
Solution 2 (CAShapeLayer):
override func drawRect(rect: CGRect) {
let haloPath = UIBezierPath(ovalInRect: rect).CGPath
let haloLayer = CAShapeLayer(layer: layer)
haloLayer.path = haloPath
haloLayer.fillColor = UIColor.clearColor().CGColor
haloLayer.strokeColor = color.CGColor
haloLayer.lineWidth = lineWidth
layer.addSublayer(haloLayer)
}

Related

How to cut portion of CALayer in iOS Swift? [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 3 years ago.
I am trying to create a QR Reader. For that I am showing a rectOfInterest with some CALayer for visual representation. I want to show a box with some border at the corners and black background with some opacity so hide the other view from the AVCaptureVideoPreviewLayer. What I have achieved till now looks like this:
As you can see the CALayer is there but I want to cut the box portion of the layer so that that blackish thing does not come there. The code I am using to do this is like below:
func createTransparentLayer()->CALayer{
let shape = CALayer()
shape.frame = self.scanView.layer.bounds
shape.backgroundColor = UIColor.black.cgColor
shape.opacity = 0.7
return shape
}
I looked into other questions for this, seems like you have the mask the layer with the cut portion. So I subclassed the CALayer and cleared the context in drawInContext and set the mask property of the super layer to this. After that I get nothing. Everything is invisible there. What is wrong in this?
The code I tried is this:
class TransparentLayer: CALayer {
override func draw(in ctx: CGContext) {
self.backgroundColor = UIColor.black.cgColor
self.opacity = 0.7
self.isOpaque = true
ctx.clear(CGRect(x: superlayer!.frame.size.width / 2 - 100, y: superlayer!.frame.size.height / 2 - 100, width: 200, height: 200))
}
}
then set the mask property like this:
override func viewDidLayoutSubviews() {
self.rectOfInterest = CGRect(x: self.scanView.layer.frame.size.width / 2 - 100, y: self.scanView.layer.frame.size.height / 2 - 100, width: 200, height: 200)
scanView.rectOfInterest = self.rectOfInterest
let shapeLayer = self.createFrame()
scanView.doInitialSetup()
self.scanView.layer.mask = self.createTransparentLayer()
self.scanView.layer.addSublayer(shapeLayer)
}
here the shapeLayer is the bordered corner in the screenshot. How can I achieve this?
I have added view in my view controller which is centre align vertically and horizontally. Also fixed height and width to 200. Then created extension of UIView and added following code:
extension UIView {
func strokeBorder() {
self.backgroundColor = .clear
self.clipsToBounds = true
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = UIBezierPath(rect: self.bounds).cgPath
self.layer.mask = maskLayer
let line = NSNumber(value: Float(self.bounds.width / 2))
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.cgColor
borderLayer.lineDashPattern = [line]
borderLayer.lineDashPhase = self.bounds.width / 4
borderLayer.lineWidth = 10
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
}
}
Use:
By using outlet of view and call method to set border as you have request.
self.scanView.strokeBorder()
To clear the backgroundColor respective to mask view, I have added following code to clear it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.scanView.strokeBorder()
self.backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
// Draw a graphics with a mostly solid alpha channel
// and a square of "clear" alpha in there.
UIGraphicsBeginImageContext(self.backgroundView.bounds.size)
let cgContext = UIGraphicsGetCurrentContext()
cgContext?.setFillColor(UIColor.white.cgColor)
cgContext?.fill(self.backgroundView.bounds)
cgContext?.clear(CGRect(x:self.scanView.frame.origin.x, y:self.scanView.frame.origin.y, width: self.scanView.frame.width, height: self.scanView.frame.height))
let maskImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Set the content of the mask view so that it uses our
// alpha channel image
let maskView = UIView(frame: self.backgroundView.bounds)
maskView.layer.contents = maskImage?.cgImage
self.backgroundView.mask = maskView
}
Output:
I'm not using camera in background.

How to intersect two rects in iOS?

I would like to remove the "blue" circle and just have a "hollow" center (so there's only the red layer and you can see the background inside). Filling the inside with the clear color doesn't work.
class AddButtonView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// Outer circle
UIColor.red.setFill()
let outerPath = UIBezierPath(ovalIn: rect)
outerPath.fill()
// Center circle
UIColor.blue.setFill()
let centerRect = rect.insetBy(dx: rect.width * 0.55 / 2, dy: rect.height * 0.55 / 2)
let centerPath = UIBezierPath(ovalIn: centerRect)
centerPath.fill()
}
}
How can I do it? intersects didn't do anything.
override func draw(_ rect: CGRect) {
super.draw(rect)
// Outer circle
UIColor.red.setFill()
let outerPath = UIBezierPath(ovalIn: rect)
outerPath.fill()
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.setBlendMode(.destinationOut)
// Center circle
UIColor.blue.setFill()
let centerRect = rect.insetBy(dx: rect.width * 0.55 / 2, dy: rect.height * 0.55 / 2)
let centerPath = UIBezierPath(ovalIn: centerRect)
centerPath.fill()
context.restoreGState()
}
Do not forget to give the view a backgroundColor of .clear and set its isOpaque property to false.
Inspired by Draw transparent UIBezierPath line inside drawRect.
Result:

Round borders aren't clipping/masking perfectly

I make a label in Interface Builder, with constraints for fixed height and fixed width:
I subclass it to give it a white round border:
class CircularLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
layer.cornerRadius = frame.size.height / 2
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 5
layer.masksToBounds = true
clipsToBounds = true
}
}
But the clipping/masking isn't good at runtime:
I was expecting a perfect white border, without orange pixels.
iPhone 8 (Simulator and real device), iOS 11.2, Xcode 9.2, Swift 3.2
MCVE at https://github.com/Coeur/stackoverflow48658502
You should use UIBezierPath to round corners and draw border line with same path.
I my case i created CAShapeLayer with all adjustments and added it as sublayer to view.
let borderLayer = CAShapeLayer()
let shapeLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: *get your view bounds*, cornerRadius: *needed radius*).cgPath
//Set this rounding path to both layers
shapeLayer.path = path
borderLayer.path = path
//adjust border layer
borderLayer.lineWidth = *border width*
borderLayer.strokeColor = *cgColor of your border*
//apply shape layer as mask to your view, it will cut your view by the corners
*yourViewInstance*.layer.mask = shapeLayer
//Set fill color for border layer as clear, because technically it just puts colored layer over your view
borderLayer.fillColor = UIColor.clear.cgColor
//Add border layer as sublayer to your view's main layer
*your view instance*.layer.addSublayer(borderLayer)
In your case may be the problem with dynamic label's text: if text will be e.g. 900000 it will be drew under border. To solve this you could place you UILAbel inside another view (which will contain shape and border adjustments) and layout it.
Example:
Structure and constraints
What i got: container BG - orange, border - white, superview's BG - red
Controller's viewDidLoad method code:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.red
self.containerView.backgroundColor = UIColor.orange
self.label.backgroundColor = UIColor.clear
self.label.textAlignment = .center
self.label.adjustsFontSizeToFitWidth = true
self.label.text = "9000000"
//Create Border and shape and apply it to container view
let borderLayer = CAShapeLayer()
let shapeLayer = CAShapeLayer()
let path = UIBezierPath(roundedRect: containerView.bounds, cornerRadius: containerView.bounds.width / 2).cgPath
//Set this rounding path to both layers
shapeLayer.path = path
borderLayer.path = path
//adjust border layer
borderLayer.lineWidth = 20
borderLayer.strokeColor = UIColor.white.cgColor
//apply shape layer as mask to your view, it will cut your view by the corners
self.containerView.layer.mask = shapeLayer
//Set fill color for border layer as clear, because technically it just puts colored layer over your view
borderLayer.fillColor = UIColor.clear.cgColor
//Add border layer as sublayer to your view's main layer
self.containerView.layer.addSublayer(borderLayer)
}
Mystery solved.
Nice solution
Add a 1 pixel stroke and masksToBounds will do the job for clipping the edges correctly:
override func draw(_ rect: CGRect) {
super.draw(rect)
// workaround incomplete borders: https://stackoverflow.com/a/48663935/1033581
UIColor(cgColor: layer.borderColor!).setStroke()
let path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius)
path.lineWidth = 1
path.stroke()
}
Explanations
Actually, from my tests, setting layer.borderWidth = 5 is equivalent to formula:
let borderWidth: CGFloat = 5
UIColor(cgColor: layer.borderColor!).setStroke()
let path = UIBezierPath(roundedRect: bounds.insetBy(dx: borderWidth / 2, dy: borderWidth / 2),
cornerRadius: layer.cornerRadius - borderWidth / 2)
path.lineWidth = borderWidth
path.stroke()
But on the other hand layer.cornerRadius = frame.size.height / 2 + layer.masksToBounds = true is going to clip with a different unknown method that has a different aliasing formula on the edges. Because the clipping and the drawing don't have the same aliasing, there are some pixels displaying the background color instead of the border color.
Another solution is to forget about imperfect borderWidth altogether and use two views inside each other:
extension UIView {
func roundBounds() {
layer.cornerRadius = frame.size.height / 2
clipsToBounds = true
}
}
class RoundLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
roundBounds()
}
}
class RoundView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
roundBounds()
}
}

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.

Simply mask a UIView with a rectangle

I want to know how to simply mask the visible area of a UIView of any kind. All the answers/tutorials I've read so far describe masking with an image, gradient or creating round corners which is way more advanced than what I am after.
Example: I have a UIView with the bounds (0, 0, 100, 100) and I want to cut away the right half of the view using a mask. Therefore my mask frame would be (0, 0, 50, 100).
Any idea how to do this simply? I don't want to override the drawrect method since this should be applicable to any UIView.
I've tried this but it just makes the whole view invisible.
CGRect mask = CGRectMake(0, 0, 50, 100);
UIView *maskView = [[UIView alloc] initWithFrame:mask];
viewToMask.layer.mask = maskView.layer;
Thanks to the link from MSK, this is the way I went with which works well:
// Create a mask layer and the frame to determine what will be visible in the view.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectMake(0, 0, 50, 100);
// Create a path with the rectangle in it.
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
// Set the path to the mask layer.
maskLayer.path = path;
// Release the path since it's not covered by ARC.
CGPathRelease(path);
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
Thanks for answers guys.
In case someone can't find suitable answer on SO for this question for hours, like i just did, i've assembled a working gist in Swift 2.2 for masking/clipping UIView with CGRect/UIBezierPath:
https://gist.github.com/Flar49/7e977e81f1d2827f5fcd5c6c6a3c3d94
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
Usage:
let viewSize = targetView.bounds.size
let rect = CGRect(x: 20, y: 20, width: viewSize.width - 20*2, height: viewSize.height - 20*2)
// Cuts rectangle inside view, leaving 20pt borders around
targetView.mask(withRect: rect, inverse: true)
// Cuts 20pt borders around the view, keeping part inside rect intact
targetView.mask(withRect: rect)
Hope it will save someone some time in the future :)
No need any mask at all.
Just put it into a wrapper view with the smaller frame, and set clipsToBounds.
wrapper.clipsToBounds = true
Very simple example in a Swift ViewController, based on the accepted answer:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView(frame: view.bounds)
view.addSubview(redView)
view.backgroundColor = UIColor.green
redView.backgroundColor = UIColor.red
mask(redView, maskRect: CGRect(x: 50, y: 50, width: 50, height: 50))
}
func mask(_ viewToMask: UIView, maskRect: CGRect) {
let maskLayer = CAShapeLayer()
let path = CGPath(rect: maskRect, transform: nil)
maskLayer.path = path
// Set the mask of the view.
viewToMask.layer.mask = maskLayer
}
}
Output
An optional layer whose alpha channel is used as a mask to select
between the layer's background and the result of compositing the
layer's contents with its filtered background.
#property(retain) CALayer *mask
The correct way to do what you want is to create the maskView of the same frame (0, 0, 100, 100) as the viewToMask which layer you want to mask. Then you need to set the clearColor for the path you want to make invisible (this will block the view interaction over the path so be careful with the view hierarchy).
Swift 5 , thanks #Dan Rosenstark
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let red = UIView(frame: view.bounds)
view.addSubview(red)
view.backgroundColor = UIColor.cyan
red.backgroundColor = UIColor.red
red.mask(CGRect(x: 50, y: 50, width: 50, height: 50))
}
}
extension UIView{
func mask(_ rect: CGRect){
let mask = CAShapeLayer()
let path = CGPath(rect: rect, transform: nil)
mask.path = path
// Set the mask of the view.
layer.mask = mask
}
}
Setting MaskToBounds is not enough, for example, in scenario where you have UIImageView that is positioned inside RelativeLayout. In case that you put your UIImageView to be near top edge of the layout and then you rotate your UIImageView, it will go over layout and it won't be cropped. If you set that flag to true, it will be cropped yes, but it will also be cropped if UIImageView is on the center of layout, and that is not what you want.
So, determinating maskRect is the right approach.

Resources