Mask of layer mask. Core animation - ios

I deep into the Core Animation and have some problem with layer masking
I try to make a complex UI layer with transparent things for study reasons
This view a have and put into the ViewController
public class TestView : UIView
{
public TestView()
{
Layer.Delegate = this;
Frame = new CGRect(100f,200f, 100f, 100f);
}
public override void LayoutSublayersOfLayer(CALayer layer)
{
base.LayoutSublayersOfLayer(layer);
var bounds = Bounds;
var circleFrame = new CGRect(20, 20, 60, 60);
var imageFrame = new CGRect(30, 30, 40, 40);
Layer.BorderWidth = 5;
Layer.BorderColor = UIColor.Red.CGColor;
Layer.CornerRadius = 4;
var gradientLayer = new CAGradientLayer();
gradientLayer.Colors = new[] { UIColor.Green.CGColor, UIColor.Red.CGColor };
gradientLayer.Frame = bounds;
Layer.AddSublayer(gradientLayer);
var maskOfGradientLayer = new CAShapeLayer();
var path = UIBezierPath.FromRoundedRect(circleFrame, 30);
path.UsesEvenOddFillRule = true;
maskOfGradientLayer.Path = path.CGPath;
maskOfGradientLayer.FillRule = CAShapeLayer.FillRuleEvenOdd;
maskOfGradientLayer.FillColor = UIColor.Black.CGColor;
maskOfGradientLayer.Frame = bounds;
gradientLayer.Mask = maskOfGradientLayer;
var maskOfMaskLayer = new CAShapeLayer();
maskOfMaskLayer.Frame = imageFrame;
maskOfMaskLayer.FillColor = UIColor.Black.CGColor;
maskOfMaskLayer.Contents = Bundles.ImageOff.CGImage;
Layer.AddSublayer(maskOfMaskLayer);
}
}
And that I have and this is exactly what I wanted.
But I also want can to make transparent in circle instead black color.
I tried to make like this maskOfGradientLayer.Mask = maskOfMaskLayer.
But this way maskOfMaskLayer do nothing! It's not working like mask must do.
What I should to do?

But I also want can to make transparent in circle instead black color.
Solution Exploratory Version:
You can also use UIBezierPath to draw what you want, and set to maskOfMaskLayer:
var maskOfMaskLayer = new CAShapeLayer();
maskOfMaskLayer.Frame = imageFrame;
maskOfMaskLayer.FillColor = UIColor.Black.CGColor;
maskOfMaskLayer.Contents = Bundles.ImageOff.CGImage;
Layer.AddSublayer(maskOfMaskLayer);
Up code repalced with follow:
UIBezierPath offCirclePath = new UIBezierPath();
// draw an arc
offCirclePath.AddArc(new CGPoint(50, 50), circleFrame.Width / 3, (nfloat)(1.75 * Math.PI), (nfloat)(1.25 * Math.PI), true);
//draw a line
offCirclePath.MoveTo(new CGPoint(50, 30));
offCirclePath.AddLineTo(new CGPoint(50, 45));
//set to maskOfMaskLayer
var maskOfMaskLayer = new CAShapeLayer();
maskOfMaskLayer.FillColor = UIColor.Clear.CGColor;
maskOfMaskLayer.Path = offCirclePath.CGPath;
maskOfMaskLayer.LineWidth = 5;
maskOfMaskLayer.StrokeColor = UIColor.White.CGColor;
maskOfMaskLayer.LineCap = new NSString("round");
Layer.AddSublayer(maskOfMaskLayer);
Other code not changed.
But I from starts thought about setting any custom image.
Solution Two:(Wrong direction)
I have tested that CAShapeLayer can not have this function to change color from image. CAShapeLayer Class
However , I found UIImageView Can do that.There is a TintColor propert of it.This property is from UiView .UIView.TintColor Property.
var maskOfMaskLayer = new CAShapeLayer();
maskOfMaskLayer.Frame = imageFrame;
maskOfMaskLayer.FillColor = UIColor.Black.CGColor;
maskOfMaskLayer.Contents = Bundles.ImageOff.CGImage;
Layer.AddSublayer(maskOfMaskLayer);
Change to:
UIImageView imageView = new UIImageView();
imageView.Frame = imageFrame;
imageView.Image = Bundles.ImageOff;
imageView.TintColor = UIColor.White;
AddSubview(imageView);
Right Solution :
Sorry for misunderstanding the real want effect,I update as follow:
Layer.Mask = maskOfMaskLayer;
If you use AddSublayer just overwrite your image on the Layer, and Layer.Mask is the opposite effect, it only shows the background that covers the part, and the rest of the area will be white.CALayer.Mask

All needed to do it is reverse image colors via Core Graphics, any color to transparent, transparent to any color. In this case, black color is enough
public static UIImage GetRevertImage(UIImage image, CGRect rect, CGRect cropFrame)
{
UIGraphics.BeginImageContextWithOptions(rect.Size, false, 1);
UIColor.Black.SetColor();
UIGraphics.RectFill(rect);
image.Draw(cropFrame, CGBlendMode.DestinationOut, 1);
var newImage = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return newImage;
}
And set mask of mask layer to parent layer
Layer.Mask = maskOfMaskLayer;
And final version of code:
public class TestView : UIView
{
public TestView()
{
Layer.Delegate = this;
Frame = new CGRect(50f, 50f, 100f, 100f);
}
public override void LayoutSublayersOfLayer(CALayer layer)
{
base.LayoutSublayersOfLayer(layer);
var bounds = Bounds;
var circleFrame = new CGRect(20, 20, 60, 60);
var imageFrame = new CGRect(30, 30, 40, 40);
Layer.BorderWidth = 5;
Layer.BorderColor = UIColor.Red.CGColor;
Layer.CornerRadius = 4;
Layer.MasksToBounds = true;
var gradientLayer = new CAGradientLayer();
gradientLayer.Colors = new[] {UIColor.Green.CGColor, UIColor.Red.CGColor};
gradientLayer.Frame = bounds;
Layer.AddSublayer(gradientLayer);
var maskOfGradientLayer = new CAShapeLayer();
var path = UIBezierPath.FromRoundedRect(circleFrame, 30);
path.UsesEvenOddFillRule = true;
maskOfGradientLayer.Path = path.CGPath;
maskOfGradientLayer.FillRule = CAShapeLayer.FillRuleEvenOdd;
maskOfGradientLayer.FillColor = UIColor.Black.CGColor;
maskOfGradientLayer.Frame = bounds;
gradientLayer.Mask = maskOfGradientLayer;
var maskOfMaskLayer = new CALayer();
maskOfMaskLayer.Frame = bounds;
var image = UIImage.FromBundle("Turn");
var reversedImage = GetReverseImage(image, bounds, imageFrame);
maskOfMaskLayer.Contents = reversedImage.CGImage;
Layer.Mask = maskOfMaskLayer;
}
private static UIImage GetReverseImage(UIImage image, CGRect rect, CGRect cropFrame)
{
UIGraphics.BeginImageContextWithOptions(rect.Size, false, 1);
UIColor.Black.SetColor();
UIGraphics.RectFill(rect);
image.Draw(cropFrame, CGBlendMode.DestinationOut, 1);
var newImage = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return newImage;
}
}

Related

Apply Gradient to dashed UIBezierPath

This gradient is what I'd like to achieve on dashed UIBezierPath.
Tried this code with no success.
let gradient = CAGradientLayer()
gradient.frame = outerPath.bounds
gradient.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor]
// mask
let shapeMask = CAShapeLayer()
shapeMask.path = outerPath.cgPath
self.insertSublayer(gradient, at: 0)
Let the CAShapeLayer make a dashed line mask.
class
TheView: UIView {
required init?( coder aDecoder: NSCoder ) {
super.init( coder: aDecoder )
let outerPath = UIBezierPath( ovalIn: bounds.insetBy( dx: 20, dy: 20 ) )
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [ UIColor.red.cgColor, UIColor.yellow.cgColor ]
let shapeMask = CAShapeLayer()
shapeMask.path = outerPath.cgPath
shapeMask.lineWidth = 8
shapeMask.lineDashPhase = 0
shapeMask.lineDashPattern = [ 6, 2 ]
shapeMask.lineCap = .butt
shapeMask.lineJoin = .bevel
shapeMask.strokeColor = UIColor.black.cgColor // Any color
shapeMask.fillColor = nil
gradient.mask = shapeMask
layer.addSublayer( gradient )
}
}
I think the best way is filling every square's color by yourself.In this case, the start color is green, end color is red, so how to calculate the internal color?
You can see this demo:
typedef struct{
float red;
float green;
float blue;
} TFColor;
- (void)viewDidLoad {
[super viewDidLoad];
TFColor start = {0,1,0}; //green
TFColor end = {1,0,0}; //red
for (int i = 0; i<20; i++) {
UIView *square = [[UIView alloc] initWithFrame:CGRectMake(150, 20*i, 75, 20)];
//the lerp rate changes from 1 to 0.
TFColor lerpColor = [self lerp:(19-i)/19.0f first:start second:end];
square.backgroundColor = [UIColor colorWithRed:lerpColor.red green:lerpColor.green blue:lerpColor.blue alpha:1];
[self.view addSubview:square];
}
}
#define lerpNum(a,b,r) (a*r+(1.0f-r)*b)
-(TFColor)lerp:(CGFloat)rate first:(TFColor)firstColor second:(TFColor)secondColor{
TFColor result;
result.red = lerpNum(firstColor.red, secondColor.red, rate);
result.green = lerpNum(firstColor.green, secondColor.green, rate);
result.blue = lerpNum(firstColor.blue, secondColor.blue, rate);
return result;
}
You can use the angle of every square to get smooth lerp rate.

Create a masked UIView

I am attempting to do something that I thought would be possible, but have no idea how to start.
I have created a UIView and would like it to be filled with colour, but in the shape defined by an image mask. i.e. I have a PNG with alpha, and I would like the UIView I have created to be UIColor.blue in the shape of that PNG.
To show just how stupid I am, here is the absolute rubbish I have attempted so far - trying to generate just a simple square doesn't even work for me, hence it's all commented out.
let rocketColourList: [UIColor] = [UIColor.blue, UIColor.red, UIColor.green, UIColor.purple, UIColor.orange]
var rocketColourNum: Int = 0
var rocketAngleNum: Int = 0
var rocketAngle: Double {
return Double(rocketAngleNum) * Double.pi / 4
}
var rocketColour = UIColor.black
public func drawRocket (){
rocketColour.set()
self.clipsToBounds = true
self.backgroundColor = UIColor.red
imageView.image = UIImage(named: ("rocketMask"))
addSubview(imageView)
//maskimage.frame = self.frame
//let someRect = CGRect (x: 1, y: 1, width: 1000, height: 1000)
//let someRect = CGRect (self.frame)
//let fillPath = UIBezierPath(rect:someRect)
//fillPath.fill()
//setNeedsDisplay()
}
}
Set image as template, then set tint color for UIImageView. Something like that:
guard let let image = UIImage(named: "rocketMask")?.withRenderingMode(.alwaysTemplate) else { return }
imageView.image = image
imageView.tintColor = .blue
Also you can do that through the images assets:

How to add label or text in to CAShapeLayer

Here is my class and it'll draw a circle, it looks like this:
class OvalLayer: CAShapeLayer {
let animationDuration: CFTimeInterval = 0.3
override init() {
super.init()
fillColor = Colors.green.CGColor
path = ovalPathSmall.CGPath
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var ovalPathStart: UIBezierPath {
let path = UIBezierPath(ovalInRect: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
return path
}
}
Now I need to add a text to middle of this circle, I tried to find it on google but nothing that works fine. I am not sure if it's possible or not, can anyone help me if it's possible?
I guess you should add CATextLayer as a sublayer to CALayer... That works fine that way: try adding CAShapeLayer first, and then CATextLayer (to same CALayer parent layer), for example in following order...
// assume self - is UIView instance
self.layer.addSublayer(shapedLayer) // shapedLayer - CAShapeLayer instance
self.layer.addSublayer(textLayer) // textLayer - CATextLayer instance
Swift 3.0
let label = UILabel()
label.font = UIFont(name: "Helvetica-Bold", size: 12)
label.frame = CGRect(x: OvalLayer.frame.origin.x + (circleWidth/2), y: OvalLayer.frame.origin.y, width: OvalLayer.bounds.width, height: OvalLayer.bounds.height)
label.text = "Hello"
label.textColor = UIColor.red
label.isHidden = false
OvalLayer.addSublayer(label.layer)
You just need to get the center of your UIBezierPath and add a label or a CATextLayer to your current layer.
let center : CGPoint = CGPoint(x: CGRectGetMidX(ovalPathStart.bounds), y: CGRectGetMidX(ovalPathStart.bounds))
Now, create a UILabel or CATextLayer and set the center.
Text on CAShapeLayer Using CATextLayer
Here i am create CAShapeLayer object and i am adding CATextLayer to CAShapeLayer
Here numberOfArcsCount means some 8
CAShapeLayer *progressLayer = [[CAShapeLayer alloc] init];
[progressLayer setPath:bezierPath.CGPath];
CATextLayer* text = [CATextLayer new];
for (int i = 1; i <= numberOfArcs.count; i++) {
text.string = [NSString stringWithFormat:#"%i", i];
text.font = (__bridge CFTypeRef _Nullable)([UIFont fontWithName:#"akaChen" size:42]);
text.font = (__bridge CFTypeRef _Nullable)([UIFont boldSystemFontOfSize:15]);
text.fontSize=25;
text.frame = CGRectMake(0,0,40,40);
text.position = CGPointMake(CGRectGetMidX(progressLayer.frame) ,CGRectGetMidY(progressLayer.frame) );
CGFloat vert = CGRectGetMidY(progressLayer.frame) / CGRectGetHeight(text.frame);
text.anchorPoint = CGPointMake(0.5, vert );
text.alignmentMode = kCAAlignmentCenter;
text.foregroundColor = [[UIColor whiteColor] CGColor];
[progressLayer addSublayer:text];
}
This way to add label to layer :
labelFrame - CGRect
someString - String
layer - your layer
let label = CATextLayer()
label.frame = labelFarme
label.string = someString
layer.addSublayer(label)

Animating CALayer's shadowPath property

I am aware that a CALayer's shadowPath is only animatable using explicit animations, however I still cannot get this to work. I suspect that I am not passing the toValue properly - as I understand this has to be an id, yet the property takes a CGPathRef. Storing this in a UIBezierPath does not seem to work. I am using the following code to test:
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:#"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = [UIBezierPath bezierPathWithRect:CGRectMake(-10.0, -10.0, 50.0, 50.0)];
[self.view.layer addAnimation:theAnimation forKey:#"animateShadowPath"];
(I am using minus values so as to ensure the shadow extends beyond a view that lies on top of it... the layer's masksToBounds property is set to NO).
How is animation of the shadowPath achieved?
UPDATE
Problem nearly solved. Unfortunately, the main problem was a somewhat careless error...
The mistake I made was to add the animation to the root layer of the view controller, rather than the layer I had dedicated to the shadow. Also, #pe8ter was correct in that the toValue needs to be a CGPathRef cast to id (obviously when I had tried this before I still had no animation due to the wrong layer mistake). The animation works with the following code:
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:#"shadowPath"];
theAnimation.duration = 3.0;
theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:myRect].CGPath;
[controller.shadowLayer addAnimation:theAnimation forKey:#"shadowPath"];
I appreciate this was difficult to spot from the sample code I provided. Hopefully it can still be of use to people in a similar situation though.
However, when I try and add the line
controller.shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:myRect].CGPath;
the animation stops working, and the shadow just jumps to the final position instantly. Docs say to add the animation with the same key as the property being changed so as to override the implicit animation created when setting the value of the property, however shadowPath can't generate implicit animations... so how do I get the new property to stay after the animation?
Firstly, you did not set the animation's fromValue.
Secondly, you're correct: toValue accepts a CGPathRef, except it needs to be cast to id. Do something like this:
theAnimation.toValue = (id)[UIBezierPath bezierPathWithRect:newRect].CGPath;
You'll also need to set the shadowPath property of the layer explicitly if you want the change to remain after animation.
let cornerRadious = 10.0
//
let shadowPathFrom = UIBezierPath(roundedRect: rect1, cornerRadius: cornerRadious)
let shadowPathTo = UIBezierPath(roundedRect: rect2, cornerRadius: cornerRadious)
//
layer.masksToBounds = false
layer.shadowColor = UIColor.yellowColor().CGColor
layer.shadowOpacity = 0.6
//
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = shadowPathFrom.CGPath
shadowAnimation.toValue = shadowPathTo.CGPath
shadowAnimation.duration = 0.4
shadowAnimation.autoreverses = true
shadowAnimation.removedOnCompletion = true
shadowAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
layer.addAnimation(shadowAnimation, forKey: "shadowAnimation")
I've met same problem for shadow animation and find solution for persisting shadow after end of animation. You need to set final path to your shadow layer before you start animation. Reverting to initial shadow happens because CoreAnimation does not update properties of original layer, it creates copy of this layer and display animation on this copy (check Neko1kat answer for more details). After animation ended system removes this animated layer and return original layer, that has not updated path and your old shadow appears. Try this code:
let shadowAnimation = CABasicAnimation(keyPath: "shadowPath")
shadowAnimation.fromValue = currentShadowPath
shadowAnimation.toValue = newShadowPath
shadowAnimation.duration = 3.0
shadowLayer.shadowPath = newShadowPath
shadowLayer.add(shadowAnimation, forKey: "shadowAnimation")
Although not directly answer this question, if you just needs a view can drop shadow, you can use my class directly.
/*
Shadow.swift
Copyright © 2018, 2020-2021 BB9z
https://github.com/BB9z/iOS-Project-Template
The MIT License
https://opensource.org/licenses/MIT
*/
/**
A view drops shadow.
*/
#IBDesignable
class ShadowView: UIView {
#IBInspectable var shadowOffset: CGPoint = CGPoint(x: 0, y: 8) {
didSet { needsUpdateStyle = true }
}
#IBInspectable var shadowBlur: CGFloat = 10 {
didSet { needsUpdateStyle = true }
}
#IBInspectable var shadowSpread: CGFloat = 0 {
didSet { needsUpdateStyle = true }
}
/// Set nil can disable shadow
#IBInspectable var shadowColor: UIColor? = UIColor.black.withAlphaComponent(0.3) {
didSet { needsUpdateStyle = true }
}
#IBInspectable var cornerRadius: CGFloat {
get { layer.cornerRadius }
set { layer.cornerRadius = newValue }
}
private var needsUpdateStyle = false {
didSet {
guard needsUpdateStyle, !oldValue else { return }
DispatchQueue.main.async { [self] in
if needsUpdateStyle { updateLayerStyle() }
}
}
}
private func updateLayerStyle() {
needsUpdateStyle = false
if let color = shadowColor {
Shadow(view: self, offset: shadowOffset, blur: shadowBlur, spread: shadowSpread, color: color, cornerRadius: cornerRadius)
} else {
layer.shadowColor = nil
layer.shadowPath = nil
layer.shadowOpacity = 0
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateLayerStyle()
}
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
lastLayerSize = layer.bounds.size
if shadowColor != nil, layer.shadowOpacity == 0 {
updateLayerStyle()
}
}
private var lastLayerSize = CGSize.zero {
didSet {
if oldValue == lastLayerSize { return }
guard shadowColor != nil else { return }
updateShadowPathWithAnimationFixes(bonuds: layer.bounds)
}
}
// We needs some additional step to achieve smooth result when view resizing
private func updateShadowPathWithAnimationFixes(bonuds: CGRect) {
let rect = bonuds.insetBy(dx: shadowSpread, dy: shadowSpread)
let newShadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
if let resizeAnimation = layer.animation(forKey: "bounds.size") {
let key = #keyPath(CALayer.shadowPath)
let shadowAnimation = CABasicAnimation(keyPath: key)
shadowAnimation.duration = resizeAnimation.duration
shadowAnimation.timingFunction = resizeAnimation.timingFunction
shadowAnimation.fromValue = layer.shadowPath
shadowAnimation.toValue = newShadowPath
layer.add(shadowAnimation, forKey: key)
}
layer.shadowPath = newShadowPath
}
}
/**
Make shadow with the same effect as Sketch app.
*/
func Shadow(view: UIView?, offset: CGPoint, blur: CGFloat, spread: CGFloat, color: UIColor, cornerRadius: CGFloat = 0) { // swiftlint:disable:this identifier_name
guard let layer = view?.layer else {
return
}
layer.shadowColor = color.cgColor
layer.shadowOffset = CGSize(width: offset.x, height: offset.y)
layer.shadowRadius = blur
layer.shadowOpacity = 1
layer.cornerRadius = cornerRadius
let rect = layer.bounds.insetBy(dx: spread, dy: spread)
layer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
}
via https://github.com/BB9z/iOS-Project-Template/blob/master/App/General/Effect/Shadow.swift

Why masksToBounds = YES prevents CALayer shadow?

With the following snippet, I'm adding a drop shadow effect to one my UIView. Which works pretty well. But as soon as I set the view's masksToBounds property to YES. The drop shadow effect isn't rendered any more.
self.myView.layer.shadowColor = [[UIColor blackColor] CGColor];
self.myView.layer.shadowOpacity = 1.0;
self.myView.layer.shadowRadius = 10.0;
self.myView.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
self.myView.layer.cornerRadius = 5.0;
self.myView.layer.masksToBounds = YES; // <-- This is causing the Drop shadow to not be rendered
UIBezierPath *path = [UIBezierPath bezierPathWithCurvedShadowForRect:self.myView.bounds];
self.myView.layer.shadowPath = path.CGPath;
self.myView.layer.shouldRasterize = YES;
Do you have any ideas on this?
Because shadow is an effect done outside the View, and that masksToBounds set to YES will tell the UIView not to draw anything that is outside itself.
If you want a roundedCorner view with shadow I suggest you do it with 2 views:
UIView *view1 = [[UIView alloc] init];
UIView *view2 = [[UIView alloc] init];
view1.layer.cornerRadius = 5.0;
view1.layer.masksToBounds = YES;
view2.layer.cornerRadius = 5.0;
view2.layer.shadowColor = [[UIColor blackColor] CGColor];
view2.layer.shadowOpacity = 1.0;
view2.layer.shadowRadius = 10.0;
view2.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
[view2 addSubview:view1];
[view1 release];
It's iOS 6 now, things might have changed. TheSquad's answer don't work for me until I managed to add one more line view2.layer.masksToBounds = NO;, otherwise shadow doesn't show. Although documentation says masksToBounds is NO by default, my code shows the opposite.
Here is how I make a rounded corner button with shadow, which is among the most commonly used code snippet in my app.
button.layer.masksToBounds = YES;
button.layer.cornerRadius = 10.0f;
view.layer.masksToBounds = NO; // critical to add this line
view.layer.cornerRadius = 10.0f;
view.layer.shadowOpacity = 1.0f;
// set shadow path to prevent horrible performance
view.layer.shadowPath =
[UIBezierPath bezierPathWithRoundedRect:_button.bounds cornerRadius:10.0f].CGPath;
[view addSubview:button];
EDIT
If views need to be animated or scrolled, masksToBounds = YES tax performance significantly, which means animation will probably get stuttered. To get rounded corner and shadow AND smooth animation or scrolling, use following code instead:
button.backgroundColor = [UIColor clearColor];
button.layer.backgroundColor = [UIColor redColor].CGColor;
button.layer.masksToBounds = NO;
button.layer.cornerRadius = 10.0f;
view.layer.shadowOpacity = 0.5f;
view.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:_button.bounds cornerRadius:10.0f].CGPath;
view.layer.shadowOffset = CGSizeMake(0.0f, 4.0f);
view.layer.shadowRadius = 2.0f;
view.layer.masksToBounds = NO;
view.layer.cornerRadius = 10.0f;
[view addSubview:button];
Swift 3.0 version with StoryBoard
The same idea with #TheSquad. Create a new view under the actual view and add shadow to the lower view.
1. Create a view under the actual view
Drag a UIView to StoryBoard with same constraint as your target view. Check clip to bound for the target view. Also make sure the new view is listed before the target view so that the target view will cover the new view.
2. Now link the new view to your code add add shadow on it
This is just a sample. You can do whatever way you want here
shadowView.layer.masksToBounds = false
shadowView.layer.shadowColor = UIColor.red.cgColor
shadowView.layer.shadowOpacity = 0.5
shadowView.layer.shadowOffset = CGSize(width: -1, height: 1)
shadowView.layer.shadowRadius = 3
shadowView.layer.shadowPath = UIBezierPath(rect: coverImage.bounds).cgPath
shadowView.layer.shouldRasterize = true
This is the Swift 3 and IBDesignable version of the answer posted by #TheSquad.
I used the same concept while making changes in the storyboard file. First I moved my targetView (the one which requires corner radius and shadow) inside a new containerView. Then I added the following lines of code (Reference: https://stackoverflow.com/a/35372901/419192) to add some IBDesignable attributes for UIView Class:
#IBDesignable extension UIView {
/* The color of the shadow. Defaults to opaque black. Colors created
* from patterns are currently NOT supported. Animatable. */
#IBInspectable var shadowColor: UIColor? {
set {
layer.shadowColor = newValue!.cgColor
}
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
else {
return nil
}
}
}
/* The opacity of the shadow. Defaults to 0. Specifying a value outside the
* [0,1] range will give undefined results. Animatable. */
#IBInspectable var shadowOpacity: Float {
set {
layer.shadowOpacity = newValue
}
get {
return layer.shadowOpacity
}
}
/* The shadow offset. Defaults to (0, -3). Animatable. */
#IBInspectable var shadowOffset: CGPoint {
set {
layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
}
get {
return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
}
}
/* The blur radius used to create the shadow. Defaults to 3. Animatable. */
#IBInspectable var shadowRadius: CGFloat {
set {
layer.shadowRadius = newValue
}
get {
return layer.shadowRadius
}
}
/* The corner radius of the view. */
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
}
get {
return layer.cornerRadius
}
}
After adding this code, I went back to the storyboard and on selecting my containerView I could now find a new set of attributes in the attributes inspector:
Other than adding values for these attributes as per my choice, I also added a corner radius to my targetView and set the masksToBounds property as true.
I hope this helps :)
I also had drastic performance issues with shadows and rounded corners. Instead of using the shadowPath part, I used the following lines which perfectly solved the performance hit:
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = UIScreen.mainScreen.scale;
Here is one of the solutions:
#IBOutlet private weak var blockView: UIView! {
didSet {
blockView.backgroundColor = UIColor.white
blockView.layer.shadowColor = UIColor.black.cgColor
blockView.layer.shadowOpacity = 0.5
blockView.layer.shadowOffset = CGSize.zero
blockView.layer.cornerRadius = 10
}
}
#IBOutlet private weak var imageView: UIImageView! {
didSet {
imageView.layer.cornerRadius = 10
imageView.layer.masksToBounds = true
imageView.layer.shouldRasterize = true
}
}

Resources