Round borders aren't clipping/masking perfectly - ios

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

Related

Shadows masks to bounds only on the left and upper side. What causes this?

I'm trying to achieve a feature that allows adding a shadow to a transparent button. For this, I'm creating a layer that masks the shadows inside the view. However, my shadows are clipped on the left and upper sides but not clipped on the right and lower sides.
Here is how it looks (This is not a transparent button but they're also working fine except the shadow being clipped like this.):
And here is my code for achieving this:
private func applyShadow() {
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
path.append(UIBezierPath(rect: UIScreen.main.bounds))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
I think that's something related to the Even-Odd Fill Rule algorithm, I'm not sure. But how can I overcome this clipping problem?
Thanks in advance.
EDIT:
This is what a transparent button with borders and text on it looks when shadow applied.. Which I don't want.
What it should look like this. No shadows inside but also a clear background color. (Except the clipped top and left sides):
I think a couple issues...
You are appending UIBezierPath(rect: UIScreen.main.bounds) to your path, but that puts the top-left corner at the top-left corner of the layer... which "clips" the top-left shadow.
If you DO have a background color, you'll need to clip those corners as well, or they will "bleed" outside the rounded corners.
Give this a try (only slightly modified). It's designated #IBDesignable so you can see how it looks in Storyboard / Interface Builder (I did not set any of the properties to inspectable -- I'll leave that up to you if you want to do so):
#IBDesignable
class MyRSButton: UIButton {
var shouldApplyShadow: Bool = true
var innerShadows: Bool?
var cornerRadi: CGFloat = 8.0
var shadowLayer: CAShapeLayer!
var shadowRadius: CGFloat?
var shadowColor: UIColor?
var shadowOffset: CGSize?
var shadowOpacity: Float?
override func layoutSubviews() {
super.layoutSubviews()
applyShadow()
}
private func applyShadow() {
// needed to prevent background color from bleeding past
// the rounded corners
cornerRadi = bounds.size.height * 0.5
layer.cornerRadius = cornerRadi
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
// define a rect that is 80-pts wider and taller
// than the button... this will "expand" it from center
let r = bounds.insetBy(dx: -40, dy: -40)
path.append(UIBezierPath(rect: r))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
}
Result:

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.

Set gradient color on custom UIView boarder

I have a UIView that contains a 2 UILabels with a button inside and I would like to add a gradient color to its boarder. I have managed to add it and button has ended up moving outside the custom UIView with the custom UIView also shrinking all the way outside on smaller devices. How can I fix the UIView to remain the same size when I add a gradient color
Here is the initial UIView with two UILabels and a button inside with a normal border colour before
And here how it looks after applying a gradient color to it
Here is my code on how I apply the gradient.
#IBOutlet weak var customView: UIView!
let gradient = CAGradientLayer()
gradient.frame.size = self.customView.frame.size
gradient.colors = [UIColor.green.cgColor, UIColor.yellow.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0.1, y: 0.78)
gradient.endPoint = CGPoint(x: 1.0, y: 0.78)
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(rect: self.customView.bounds).cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 4
gradient.mask = shapeLayer
self.customView.layer.addSublayer(gradient)
Layers do not resize when the view resizes, so you want to create a custom view and override layoutSubviews().
Here's an example:
#IBDesignable
class GradBorderView: UIView {
var gradient = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(gradient)
}
override func layoutSubviews() {
gradient.frame = bounds
gradient.colors = [UIColor.green.cgColor, UIColor.yellow.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0.1, y: 0.78)
gradient.endPoint = CGPoint(x: 1.0, y: 0.78)
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 4
gradient.mask = shapeLayer
}
}
Now, when your view changes size based on constraints and auto-layout, your gradient border will "auto-resize" correctly.
Also, by using #IBDesignable, you can see the results when laying out your views in Storyboard / Interface Builder.
Here's how it looks with the Grad Border View width set to 240:
and with the Grad Border View width set to 320:
Edit
If we want to use rounded corners, we can set the shape layer path to a rounded rect bezier path, and then also set the corner radius of the view's layer.
For example:
override func layoutSubviews() {
let cRadius: CGFloat = 8.0
let bWidth: CGFloat = 4.0
// layer border is centered on layer edge
let half: CGFloat = bWidth * 0.5
// make gradient frame size of view + half the border width
gradient.frame = bounds.insetBy(dx: -half, dy: -half)
gradient.colors = [UIColor.green.cgColor, UIColor.yellow.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0.1, y: 0.78)
gradient.endPoint = CGPoint(x: 1.0, y: 0.78)
let shapeLayer = CAShapeLayer()
// make shapeLayer path the size of view OFFSET by half the border width
// with rounded corners
shapeLayer.path = UIBezierPath(roundedRect: bounds.offsetBy(dx: half, dy: half), cornerRadius: cRadius).cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = bWidth
gradient.mask = shapeLayer
// same corner radius as shapeLayer path
layer.cornerRadius = cRadius
}

Change corner radius of section header in UItableview ios swift

I need to change custom UITableview section header corner radius dynamically. Initially section header corner radius is 5 for all corners. After adding cell to section, bottom left and bottom right corners should remove. I changed that in layout subview method of custom UITableViewHeaderFooter class. But its not reflecting, corner radius added for all corners, not changing when adding cell. My code is:
override func layoutSubviews() {
super.layoutSubviews()
textLabel?.textColor = UIColor.black
contentView.backgroundColor = UIColor.clear
backgroundView?.backgroundColor = UIColor.white
if let sections = self.sections, let sectionIndex = self.sectionIndex, let values = sections[sectionIndex].values, values.count > 0, sections[sectionIndex].expanded {
layer.cornerRadius = 5
self.setRoundCorners(cornerRadius: 5, corners: [.topLeft, .topRight])
} else {
layer.cornerRadius = 5
layer.mask = nil
}
}
UIView extension
func setRoundCorners(cornerRadius: CGFloat, corners: UIRectCorner) {
let path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
layer.mask = maskLayer
}

Don't want bezier path get clipped by view frame

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

Resources