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

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:

Related

Swift - Shadow for a irregular shape of a View

i am struggling to add shadow to a custom shape.
Here is a picture of what i want to construct:
(Dont mind the text and the symbol)
You can see the custom shape with the curved corner on the right and the rectangular shape on the left with shadow.
I am using UIView, and added corner to the left.
This is the code i have so far that shape the view correct:
View1.backgroundColor = .green //green color is just to see the shape well
let path = UIBezierPath(roundedRect:View1.bounds,
byRoundingCorners:[.topRight, .bottomRight],
cornerRadii: CGSize(width: self.frame.height/2, height: self.frame.height/2))
let maskLayer = CAShapeLayer()
I Have tried to add shadow to it, but the shadow does not apear.
Here is the code i have tried to add shadow:
View1.layer.masksToBounds = false
View1.layer.layer.shadowPath = maskLayer.path
View1.layer.shadowColor = UIColor.black.cgColor
View1.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
View1.layer.shadowOpacity = 0.5
View1.layer.shadowRadius = 1.0
How can you add shadow to this shape?
You can achieve this by using a single UIView (shadowView), adding a shapeLayer sublayer and setting the shadow of the shadowView's layer.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
#IBOutlet var shadowView: UIView!
func setup() {
// setup irregular shape
let path = UIBezierPath.init(roundedRect: shadowView.bounds, byRoundingCorners: [.topRight, .bottomRight], cornerRadii: CGSize.init(width: 20, height: 20))
let layer = CAShapeLayer.init()
layer.frame = shadowView.bounds
layer.path = path.cgPath
layer.fillColor = UIColor.white.cgColor
layer.masksToBounds = true
shadowView.layer.insertSublayer(layer, at: 0)
// setup shadow
shadowView.layer.shadowRadius = 8
shadowView.layer.shadowOpacity = 0.2
shadowView.layer.shadowOffset = CGSize.init(width: 0, height: 2.5)
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowPath = path.cgPath
}
}
Note:
The shadowView.clipToBounds must be false for the shadows to take effect.
To see the layer.fillColor, set the shadowView.backgroundColor to .clear.
You can easily achieve the above via Interface Builder by setting the 'Background' property and unchecking the 'Clip to Bounds' checkbox.

How to set UITextField border with gradient from left to right with rounded corners using Swift 4

I am trying to implement a UITextField activity with rounded gradient border from left to right like below sample: -
Requirement:
Here I have 2 text fields. On load view controller 'Email' will be active with the gradient border. When user resigns 'Email' field then the border will change to white color and again if user click in 'Email' text field then the border will change to the gradient.
I tried below code but not working properly: -
class CustomTextField: UITextField {
var gradient:CAGradientLayer! = nil
func canBecomeFirstResponder() -> Bool {
return true
}
override func becomeFirstResponder() -> Bool {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: frame.size.height / 2, height: frame.size.height / 2))
gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: frame.size)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
let shape = CAShapeLayer()
shape.lineWidth = 10
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
layer.addSublayer(gradient)
super.becomeFirstResponder()
return true
}
override func resignFirstResponder() -> Bool {
gradient.colors = [UIColor.white.cgColor]
super.resignFirstResponder()
return true
}
}
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var txtEmail: CustomTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
txtEmail.resignFirstResponder()
return true
}
}
There is three problem which I am not able to resolve: -
1- I have set lineWidth 10 but its showing width 10 at corners and at horizontal/vertical only 5.
2- I want to show the gradient from left to right not top to bottom.
3- When I resign, it is not setting white border
Please help, thanks in advance.
The line is stroked centrally to the path, so 10 pixel width means 5 pixels either side. Your shape is being clipped at the sides, so you need to inset the path.
Problem 1
let lineWidth: CGFloat = 10
let rect = bounds.inset.bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath(roundedRect: rect, cornerRadius: frame.size.height / 2)
shape.lineWidth = lineWidth
Alternatively, if you want the line to be stroked centrally on the path, you'll need to set your CustomTextField's clipsToBounds = false
Problem 2
To change the angle of the gradient, use the startPoint and endPoint
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
Problem 3
Possibly try
gradient.colors = [UIColor.white.cgColor, UIColor.white.cgColor]
First select "NO Border in XCode's element inspector"
then use below code
let lineWidth: CGFloat = 1
let rect = myTextField.bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath(roundedRect: rect, cornerRadius: myTextField.frame.size.height / 2)
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: myTextField.frame.size)
gradient.colors = [UIColor.green.cgColor, UIColor.red.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
let shape = CAShapeLayer()
shape.lineWidth = lineWidth
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
myTextField.layer.addSublayer(gradient)
P.S. This answer is fully inspired by #Ashley's answer.

Inner shadow in UITextField with rounded corners

I want to implement this UITextField design:
In Zeplin here is the properties of the shadow:
What I have tried ?
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.size.height/2
self.addInnerShadow()
}
private func addInnerShadow() {
let innerShadow = CALayer()
innerShadow.frame = bounds
// Shadow path (1pt ring around bounds)
let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -1, dy: -1))
let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
path.append(cutout)
innerShadow.shadowPath = path.cgPath
innerShadow.masksToBounds = true
// Shadow properties
innerShadow.shadowColor = UIColor.black.cgColor
innerShadow.shadowOffset = CGSize(width: 0, height: 3)
innerShadow.shadowOpacity = 0.05
innerShadow.shadowRadius = 3
innerShadow.cornerRadius = self.frame.size.height/2
layer.addSublayer(innerShadow)
}
result:
Update:
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.size.height/2
self.addInnerShadow()
}
private func addInnerShadow() {
let innerShadow = CALayer()
innerShadow.frame = bounds
// Shadow path (1pt ring around bounds)
let path = UIBezierPath(roundedRect: innerShadow.bounds.insetBy(dx: -1, dy: -1), cornerRadius: self.frame.size.height/2)
let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
path.append(cutout)
innerShadow.shadowPath = path.cgPath
innerShadow.masksToBounds = true
// Shadow properties
innerShadow.shadowColor = UIColor.black.cgColor
innerShadow.shadowOffset = CGSize(width: 0, height: 3)
innerShadow.shadowOpacity = 0.05
innerShadow.shadowRadius = 3
//innerShadow.cornerRadius = self.frame.size.height/2
layer.addSublayer(innerShadow)
}
result:
the corner radius is causing a problem because the path is still rectangular and the shadow looks different
Just use a rounded rect path:
private func addInnerShadow() {
let innerShadow = CALayer()
innerShadow.frame = bounds
// Shadow path (1pt ring around bounds)
let radius = self.frame.size.height/2
let path = UIBezierPath(roundedRect: innerShadow.bounds.insetBy(dx: -1, dy:-1), cornerRadius:radius)
let cutout = UIBezierPath(roundedRect: innerShadow.bounds, cornerRadius:radius).reversing()
path.append(cutout)
innerShadow.shadowPath = path.cgPath
innerShadow.masksToBounds = true
// Shadow properties
innerShadow.shadowColor = UIColor.black.cgColor
innerShadow.shadowOffset = CGSize(width: 0, height: 3)
innerShadow.shadowOpacity = 0.15
innerShadow.shadowRadius = 3
innerShadow.cornerRadius = self.frame.size.height/2
layer.addSublayer(innerShadow)
}
To attain this - I added a uiimageview and input the image as the shadow text cell, then placed a label on top of the uiimageview with a clear background so you are able to see the shadow image through the text. This was all done with storyboard so I have no code to show and am not allowed to upload an image yet. Hope this helps.

How To Add Border To a UIView With Mask?

Here's what I need to do to my two buttons.
and here's what I got right now.
What I'm doing:
- Set these up in IB inside a stackView.
- Add masks
- Add borders with width and color
- Add shadow.
The borders are being added but being cut-off as well by the masks.
Codes:
public func addCornerRadiusToCorners(
_ corners: UIRectCorner,
cornerRadius: CGFloat,
borderColor: UIColor,
borderWidth: CGFloat) {
// self.layer.masksToBounds = true
// self.clipsToBounds = true
self.layer.borderWidth = borderWidth
self.layer.borderColor = borderColor.cgColor
let size = CGSize(width: cornerRadius, height: cornerRadius)
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: size)
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
public func addDefaultShadow() {
let shadowPath = UIBezierPath(rect: self.bounds)
self.layer.masksToBounds = false
self.layer.shadowOffset = CGSize(width: 1, height: 1)
self.layer.shadowOpacity = 0.5
self.layer.shadowPath = shadowPath.cgPath
}
Any idea how to achieve the result in the first photo?
EDIT: the border is being cut, the result was just got from the UI Debugger of Xcode. Sorry.
Add corner radius to you view on viewDidAppear
#IBOutlet weak var segmentView: UIView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
segmentView.layer.cornerRadius = segmentView.frame.size.height/2;
segmentView.layer.borderWidth = 1.0;
segmentView.clipsToBounds = true
segmentView.layer.borderColor = UIColor.lightGray.cgColor
segmentView.layer.shadowColor = UIColor.black.cgColor
segmentView.layer.shadowOpacity = 0.2
segmentView.layer.shadowRadius = 10.0
segmentView.layer.shadowOffset = CGSize(width: 1, height: 1)
segmentView.layer.masksToBounds = false
}
Sample Output:
you forgot to clip ur view.
self.clipsToBounds = true
you need to set maskstobounds to the view layer
segmentView.layer.masksToBounds = true

iOS invert mask in drawRect

With the code below, I am successfully masking part of my drawing, but it's the inverse of what I want masked. This masks the inner portion of the drawing, where I would like to mask the outer portion. Is there a simple way to invert this mask?
myPath below is a UIBezierPath.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
With even odd filling on the shape layer (maskLayer.fillRule = kCAFillRuleEvenOdd;) you can add a big rectangle that covers the entire frame and then add the shape you are masking out. This will in effect invert the mask.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd; // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
For Swift 3.0
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Based on the accepted answer, here's another mashup in Swift. I've made it into a function and made the invert optional
class func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
if (invert) {
CGPathAddRect(path, nil, viewToMask.bounds)
}
CGPathAddRect(path, nil, maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Here's my Swift 4.2 solution that allows a corner radius
extension UIView {
func mask(withRect maskRect: CGRect, cornerRadius: CGFloat, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (inverse) {
path.addPath(UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath)
}
path.addPath(UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
if (inverse) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
self.layer.mask = maskLayer;
}
}
Swift 5
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), invert: true)
}
}
extension UIView{
func mask(_ rect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(bounds)
}
path.addRect(rect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
// Set the mask of the view.
layer.mask = maskLayer
}
}
Thanks #arvidurs
For Swift 4.2
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = .evenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
In order to invert mask you can to something like this
Here I have mask of crossed rectancles like
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
And here I add 3rd rectancle around my crossed rectancles
so using .evenOdd it takes area that is equal to (New Rectancle - Old Crossed Rectangle) so in other words outside area of crossed rectancles
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: -5, dy: -5) )
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5)))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
There are many great answers, all of them using even-odd rule for resolving interior and exterior surfaces. Here is Swift 5 approach with non-zero rule:
extension UIView {
func mask(_ rect: CGRect, cornerRadius: CGFloat, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: 0))
path.addLine(to: .zero)
}
path.addPath(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
layer.mask = maskLayer
}
}
For masking operation it is pretty straight forward. We draw rect and use it as masking path.
For inverted masking we use bounds of the view that is going to be masked. Then we draw it counter clockwise. After that we add UIBezierPath rect, which is by default drawn clockwise. That way any point p inside the rect will have one clockwise intersection and one counter clockwise intersection, leading to total winding number of zero, making that point an exterior point.
For 2023. All answers currently here seem wrong as they don't adjust the mask when the layout changes (or - just one example - when the view is animating in size or shape).
It's very simple ..
1. have a layer (perhaps just a square of color, an image, whatever)
lazy var examp: CALayer = {
let l = CALayer()
l.background = UIColor.blue.cgColor
l.mask = shapeAsMask
layer.addSublayer(l)
return l
}()
Note that examp has its mask set already.
2. Whenever you want to mask, you of course need a masking layer on hand
lazy var shapeAsMask: CAShapeLayer = {
let s = CAShapeLayer()
s.fillRule = .evenOdd
layer.addSublayer(s)
layer.mask = s
return s
}()
Note that the fillrule is set as needed.
3. Now make a shape with a bezier curve. Let's just make a circle:
override func layoutSubviews() {
super.layoutSubviews()
examp.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
...
}
So that's a small circle in the middle of the view.
If you want to "have the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
shapeAsMask.path = thing.cgPath
}
If you want to "have the INVERSE OF the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
let p = CGMutablePath()
p.addRect(bounds)
p.addPath(thing.cgPath)
shapeAsMask.path = p
}
In short, the "negative" of this ..
let thing = UIBezierPath( ... some path
is just this:
let neg = CGMutablePath()
neg.addRect(bounds)
neg.addPath(thing.cgPath)
So that's it.
Don't forget that ...
Anytime you have a mask (or a layer!) you have to set it in layoutSubviews. (That's why layoutSubviews is named layoutSubviews !)

Resources