Animating CALayer's shadowPath property - ios

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

Related

Shadow inside table view cell not working

The shadow in my cell is not working at all.
This is one of the view that I want to add shadow but it's not working. I added this code inside my custom cell class.
super.layoutSubviews()
UIview1.layer.cornerRadius = 7
UIview1.layer.borderWidth = 1.0
UIview1.layer.borderColor = HexColor.hexStringToUIColor(hex: "FA2537").cgColor
UIview1.layer.masksToBounds = true
UIview1.layer.shadowColor = HexColor.hexStringToUIColor(hex: "01A4B7").cgColor
UIview1.layer.shadowOpacity = 0.5
UIview1.layer.shadowOffset = CGSize.zero
UIview1.layer.shadowRadius = 5
}
For showing shadow on a view, you need to set its layer's masksToBounds property false.
or you can try this.
You can make a method like this and can use:
extension UIView {
func setShadowWith(color: UIColor = UIColor.black, shadowOpacity: Float = 0.2, radius: Float = 1.0, shadowOffSet: CGSize = CGSize(width: 0, height: 1)) {
self.layer.shadowColor = color.cgColor
self.layer.shadowOpacity = shadowOpacity
self.layer.shadowOffset = shadowOffSet
self.layer.shadowRadius = CGFloat(radius)
}
}
and can use function like:
yourContainerView.setShadowWith()
Here parameters used in functions are taking default values. you can change accordingly.
Set maskToBounds to false instead of true:
UIview1.layer.masksToBounds = false

UIView shadow issue on real device - iOS

I have a tableview cell with view inside and I want to add shadow to it.
For this reason I add this code:
self.PrimaView.layer.shadowPath = UIBezierPath(rect: self.PrimaView.bounds).cgPath
self.PrimaView.layer.shadowRadius = 3
self.PrimaView.layer.shadowOffset = .zero
self.PrimaView.layer.shadowOpacity = 3
This is tableview cell structure:
My problem is this:
when I execute code on simulator I get this (this is what I want to get):
but when I execute the same code on the real device I get this:
Why is it different than the previous image?
#Sulthan is right or may be you have safe area issue as well if you have same device if not.
Then you can use this code for shadow
import UIKit
#IBDesignable
class CardViewMaterial: UIView {
// #IBInspectable var cornerRadius: CGFloat = 2
#IBInspectable var shadowOffsetWidth: Int = 0
#IBInspectable var shadowOffsetHeight: Int = 3
#IBInspectable var shadowColor: UIColor? = UIColor.black
#IBInspectable var shadowOpacity: Float = 0.5
override func layoutSubviews() {
layer.cornerRadius = cornerRadius
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
layer.masksToBounds = false
layer.shadowColor = shadowColor?.cgColor
layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight);
layer.shadowOpacity = shadowOpacity
layer.shadowPath = shadowPath.cgPath
}
}

Circular ImageView Swift

I have tried every answer variation I have found and all give me the same result. A weird almost diamond shaped image view. I was using this:
imageView.layer.cornerRadius = imageView.frame.width/2
imageView.clipsToBounds = true
This worked with a previous project I was working on but now when I try it I get the weird diamond.
Here is the solution how to create UIImageView Circular. This Approach is new
Create a new Designable.swift file in your project.
Copy the following code in your Designable.swift file.
import UIKit
#IBDesignable class DesignableImageView: UIImageView { }
#IBDesignable class DesignableButton:UIButton { }
#IBDesignable class DesignableTextField:UITextField { }
extension UIView {
#IBInspectable
var borderWidth :CGFloat {
get {
return layer.borderWidth
}
set(newBorderWidth){
layer.borderWidth = newBorderWidth
}
}
#IBInspectable
var borderColor: UIColor? {
get{
return layer.borderColor != nil ? UIColor(CGColor: layer.borderColor!) :nil
}
set {
layer.borderColor = newValue?.CGColor
}
}
#IBInspectable
var cornerRadius :CGFloat {
get {
return layer.cornerRadius
}
set{
layer.cornerRadius = newValue
layer.masksToBounds = newValue != 0
}
}
#IBInspectable
var makeCircular:Bool? {
get{
return nil
}
set {
if let makeCircular = newValue where makeCircular {
cornerRadius = min(bounds.width, bounds.height) / 2.0
}
}
}
}
Now after this select your ImageView on StoryBoard and Select Identity Inspector from Utilities Panel.
In Custom Class section select the custom class from the drop-down menu naming DesignableImageView and hit return. You will see the designable update after hitting return.
Now go to attribute inspector in utility panel and you can add the desired Corner Radius for the image to be circular.
P.S. If your image is rectangle this will try to make it square and then best possible circle.
Attached images for reference.
Try changing the view mode for the uiimageview. It might have been set to Aspect fit making it to look like a diamond.
Or you can create a circular uiimage using the code below,
imageLayer.contents = yourImage.CGImage
let mask = CAShapeLayer()
let dx = lineWidth + 1.0
let path = UIBezierPath(ovalInRect: CGRectInset(self.bounds, dx, dx))
mask.fillColor = UIColor.blackColor().CGColor
mask.path = path.CGPath
mask.frame = self.bounds
layer.addSublayer(mask)
imageLayer = CAShapeLayer()
imageLayer.frame = self.bounds
imageLayer.mask = mask
imageLayer.contentsGravity = kCAGravityResizeAspectFill
layer.addSublayer(imageLayer)
If you want to create with new image it may be like this
let img = UIImage(named: "logo.png")
UIGraphicsBeginImageContextWithOptions(imgView.bounds.size, false, 0.0);
// Add a clip before drawing anything, in the shape of an rounded rect
UIBezierPath(roundedRect: imgView.bounds, cornerRadius:imgView.bounds.size.width/2).addClip()
img!.drawInRect(imgView.bounds)
imgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
Ref & If you want to make extension for it : https://stackoverflow.com/a/25459500/4557505
You can find more information in the above link for other alternative solutions
Your image view should be a square in order for this to work.
if you are trying this code in viewDidLoad(), please try it in viewDidLayoutSubviews()
viewDidLayoutSubviews() {
imageView.layer.cornerRadius = imageView.frame.size.width/2
imageView.layer.masksToBounds = true
}
extension UIImageView {
public func maskCircle(inputImage: UIImage) {
self.contentMode = UIViewContentMode.scaleAspectFill
self.layer.cornerRadius = self.frame.height / 2
self.layer.masksToBounds = false
self.clipsToBounds = true
self.image = inputImage
}
}
Usage example of the above extension:
let yourImage:UIImage = UIImage(named: "avatar")!
yourImageView.maskCircle(yourImage)

Animating on some part of UIBezier path

I have two UIBezierPath...
First path shows the total path from to and fro destination and the second path is a copy of the first path but that copy should be a percentage of the first path which I am unable to do.
Basically I would like the plane to stop at the part where green UIBezier path ends and not go until the past green color.
I am attaching a video in hte link that will show the animation I am trying to get. http://cl.ly/302I3O2f0S3Y
Also a similar question asked is Move CALayer via UIBezierPath
Here is the relevant code
override func viewDidLoad() {
super.viewDidLoad()
let endPoint = CGPointMake(fullFlightLine.frame.origin.x + fullFlightLine.frame.size.width, fullFlightLine.frame.origin.y + 100)
self.layer = CALayer()
self.layer.contents = UIImage(named: "Plane")?.CGImage
self.layer.frame = CGRectMake(fullFlightLine.frame.origin.x - 10, fullFlightLine.frame.origin.y + 10, 120, 120)
self.path = UIBezierPath()
self.path.moveToPoint(CGPointMake(fullFlightLine.frame.origin.x, fullFlightLine.frame.origin.y + fullFlightLine.frame.size.height/4))
self.path.addQuadCurveToPoint(endPoint, controlPoint:CGPointMake(self.view.bounds.size.width/2, -200))
self.animationPath = self.path.copy() as! UIBezierPath
let w = (viewModel?.completePercentage) as CGFloat!
// let animationRectangle = UIBezierPath(rect: CGRectMake(fullFlightLine.frame.origin.x-20, fullFlightLine.frame.origin.y-270, fullFlightLine.frame.size.width*w, fullFlightLine.frame.size.height-20))
// let currentContext = UIGraphicsGetCurrentContext()
// CGContextSaveGState(currentContext)
// self.animationPath.appendPath(animationRectangle)
// self.animationPath.addClip()
// CGContextRestoreGState(currentContext)
self.shapeLayer = CAShapeLayer()
self.shapeLayer.path = path.CGPath
self.shapeLayer.strokeColor = UIColor.redColor().CGColor
self.shapeLayer.fillColor = UIColor.clearColor().CGColor
self.shapeLayer.lineWidth = 10
self.animationLayer = CAShapeLayer()
self.animationLayer.path = animationPath.CGPath
self.animationLayer.strokeColor = UIColor.greenColor().CGColor
self.animationLayer.strokeEnd = w
self.animationLayer.fillColor = UIColor.clearColor().CGColor
self.animationLayer.lineWidth = 3
fullFlightLine.layer.addSublayer(shapeLayer)
fullFlightLine.layer.addSublayer(animationLayer)
fullFlightLine.layer.addSublayer(self.layer)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
updateAnimationForPath(self.animationLayer)
}
func updateAnimationForPath(pathLayer : CAShapeLayer) {
let animation : CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "position")
animation.path = pathLayer.path
animation.calculationMode = kCAAnimationPaced
animation.delegate = self
animation.duration = 3.0
self.layer.addAnimation(animation, forKey: "bezierPathAnimation")
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Your animation for traveling a path is exactly right. The only problem now is that you are setting the key path animation's path to the whole bezier path:
animation.path = pathLayer.path
If you want the animation to cover only part of that path, you have two choices:
Supply a shorter version of the bezier path, instead of pathLayer.path.
Alternatively, wrap the whole animation in a CAAnimationGroup with a shorter duration. For example, if your three-second animation is wrapped inside a two-second animation group, it will stop two-thirds of the way through.

What's the best way to add a drop shadow to my UIView

I am trying to add a drop shadow to views that are layered on top of one another, the views collapse allowing content in other views to be seen, in this vein i want to keep view.clipsToBounds ON so that when the views collapse their content is clipped.
This seems to have made it difficult for me to add a drop shadow to the layers as when i turn clipsToBounds ON the shadows are clipped also.
I have been trying to manipulate view.frame and view.bounds in order to add a drop shadow to the frame but allow the bounds to be large enough to encompass it, however I have had no luck with this.
Here is the code I am using to add a Shadow (this only works with clipsToBounds OFF as shown)
view.clipsToBounds = NO;
view.layer.shadowColor = [[UIColor blackColor] CGColor];
view.layer.shadowOffset = CGSizeMake(0,5);
view.layer.shadowOpacity = 0.5;
Here is a screenshot of the shadow being applied to the top lightest grey layer. Hopefully this gives an idea of how my content will overlap if clipsToBounds is OFF.
How can I add a shadow to my UIView and keep my content clipped?
Edit: Just wanted to add that I have also played around with using background images with shadows on, which does work well, however I would still like to know the best coded solution for this.
Try this:
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:view.bounds];
view.layer.masksToBounds = NO;
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
view.layer.shadowOpacity = 0.5f;
view.layer.shadowPath = shadowPath.CGPath;
First of all: The UIBezierPath used as shadowPath is crucial. If you don't use it, you might not notice a difference at first, but the keen eye will observe a certain lag occurring during events like rotating the device and/or similar. It's an important performance tweak.
Regarding your issue specifically: The important line is view.layer.masksToBounds = NO. It disables the clipping of the view's layer's sublayers that extend further than the view's bounds.
For those wondering what the difference between masksToBounds (on the layer) and the view's own clipToBounds property is: There isn't really any. Toggling one will have an effect on the other. Just a different level of abstraction.
Swift 2.2:
override func layoutSubviews()
{
super.layoutSubviews()
let shadowPath = UIBezierPath(rect: bounds)
layer.masksToBounds = false
layer.shadowColor = UIColor.blackColor().CGColor
layer.shadowOffset = CGSizeMake(0.0, 5.0)
layer.shadowOpacity = 0.5
layer.shadowPath = shadowPath.CGPath
}
Swift 3:
override func layoutSubviews()
{
super.layoutSubviews()
let shadowPath = UIBezierPath(rect: bounds)
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
layer.shadowOpacity = 0.5
layer.shadowPath = shadowPath.cgPath
}
Wasabii's answer in Swift 2.3:
let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.blackColor().CGColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.CGPath
And in Swift 3/4/5:
let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.cgPath
Put this code in layoutSubviews() if you're using AutoLayout.
In SwiftUI, this is all much easier:
Color.yellow // or whatever your view
.shadow(radius: 3)
.frame(width: 200, height: 100)
The trick is defining the masksToBounds property of your view's layer properly:
view.layer.masksToBounds = NO;
and it should work.
(Source)
You can create an extension for UIView to access these values in the design editor
extension UIView{
#IBInspectable var shadowOffset: CGSize{
get{
return self.layer.shadowOffset
}
set{
self.layer.shadowOffset = newValue
}
}
#IBInspectable var shadowColor: UIColor{
get{
return UIColor(cgColor: self.layer.shadowColor!)
}
set{
self.layer.shadowColor = newValue.cgColor
}
}
#IBInspectable var shadowRadius: CGFloat{
get{
return self.layer.shadowRadius
}
set{
self.layer.shadowRadius = newValue
}
}
#IBInspectable var shadowOpacity: Float{
get{
return self.layer.shadowOpacity
}
set{
self.layer.shadowOpacity = newValue
}
}
}
You can set shadow to your view from storyboard also
On viewWillLayoutSubviews:
override func viewWillLayoutSubviews() {
sampleView.layer.masksToBounds = false
sampleView.layer.shadowColor = UIColor.darkGrayColor().CGColor;
sampleView.layer.shadowOffset = CGSizeMake(2.0, 2.0)
sampleView.layer.shadowOpacity = 1.0
}
Using Extension of UIView:
extension UIView {
func addDropShadowToView(targetView:UIView? ){
targetView!.layer.masksToBounds = false
targetView!.layer.shadowColor = UIColor.darkGrayColor().CGColor;
targetView!.layer.shadowOffset = CGSizeMake(2.0, 2.0)
targetView!.layer.shadowOpacity = 1.0
}
}
Usage:
sampleView.addDropShadowToView(sampleView)
So yes, you should prefer the shadowPath property for performance, but also:
From the header file of CALayer.shadowPath
Specifying the path explicitly using this property will usually
* improve rendering performance, as will sharing the same path
* reference across multiple layers
A lesser known trick is sharing the same reference across multiple layers. Of course they have to use the same shape, but this is common with table/collection view cells.
I don't know why it gets faster if you share instances, i'm guessing it caches the rendering of the shadow and can reuse it for other instances in the view. I wonder if this is even faster with

Resources