I am trying to create a simple and animated pie chart using CAShapeLayer. I want it to animate from 0 to a provided percentage.
To create the shape layer I use:
CGMutablePathRef piePath = CGPathCreateMutable();
CGPathMoveToPoint(piePath, NULL, self.frame.size.width/2, self.frame.size.height/2);
CGPathAddLineToPoint(piePath, NULL, self.frame.size.width/2, 0);
CGPathAddArc(piePath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, DEGREES_TO_RADIANS(-90), DEGREES_TO_RADIANS(-90), 0);
CGPathAddLineToPoint(piePath, NULL, self.frame.size.width/2 + radius * cos(DEGREES_TO_RADIANS(-90)), self.frame.size.height/2 + radius * sin(DEGREES_TO_RADIANS(-90)));
pie = [CAShapeLayer layer];
pie.fillColor = [UIColor redColor].CGColor;
pie.path = piePath;
[self.layer addSublayer:pie];
Then to animate I use:
CGMutablePathRef newPiePath = CGPathCreateMutable();
CGPathAddLineToPoint(newPiePath, NULL, self.frame.size.width/2, 0);
CGPathMoveToPoint(newPiePath, NULL, self.frame.size.width/2, self.frame.size.height/2);
CGPathAddArc(newPiePath, NULL, self.frame.size.width/2, self.frame.size.height/2, radius, DEGREES_TO_RADIANS(-90), DEGREES_TO_RADIANS(125), 0);
CGPathAddLineToPoint(newPiePath, NULL, self.frame.size.width/2 + radius * cos(DEGREES_TO_RADIANS(125)), self.frame.size.height/2 + radius * sin(DEGREES_TO_RADIANS(125)));
CABasicAnimation *pieAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pieAnimation.duration = 1.0;
pieAnimation.removedOnCompletion = NO;
pieAnimation.fillMode = kCAFillModeForwards;
pieAnimation.fromValue = pie.path;
pieAnimation.toValue = newPiePath;
[pie addAnimation:pieAnimation forKey:#"animatePath"];
Obviously, this is animating in a really odd way. The shape just kind of grows into its final state. Is there an easy way to make this animation follow the direction of the circle? Or is that a limitation of CAShapeLayer animations?
I know this question has long been answered, but I don't quite think this is a good case for CAShapeLayer and CAKeyframeAnimation. Core Animation has the power to do animation tweening for us. Here's a class (with a wrapping UIView, if you like) that I use to accomplish the effect pretty well.
The layer subclass enables implicit animation for the progress property, but the view class wraps its setter in a UIView animation method. The interesting (and ultimately useful) side effect of using a 0.0 length animation with UIViewAnimationOptionBeginFromCurrentState is that each animation cancels out the previous one, leading to a smooth, fast, high-framerate pie, like this (animated) and this (not animated, but incremented).
DZRoundProgressView.h
#interface DZRoundProgressLayer : CALayer
#property (nonatomic) CGFloat progress;
#end
#interface DZRoundProgressView : UIView
#property (nonatomic) CGFloat progress;
#end
DZRoundProgressView.m
#import "DZRoundProgressView.h"
#import <QuartzCore/QuartzCore.h>
#implementation DZRoundProgressLayer
// Using Core Animation's generated properties allows
// it to do tweening for us.
#dynamic progress;
// This is the core of what does animation for us. It
// tells CoreAnimation that it needs to redisplay on
// each new value of progress, including tweened ones.
+ (BOOL)needsDisplayForKey:(NSString *)key {
return [key isEqualToString:#"progress"] || [super needsDisplayForKey:key];
}
// This is the other crucial half to tweening.
// The animation we return is compatible with that
// used by UIView, but it also enables implicit
// filling-up-the-pie animations.
- (id)actionForKey:(NSString *) aKey {
if ([aKey isEqualToString:#"progress"]) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:aKey];
animation.fromValue = [self.presentationLayer valueForKey:aKey];
return animation;
}
return [super actionForKey:aKey];
}
// This is the gold; the drawing of the pie itself.
// In this code, it draws in a "HUD"-y style, using
// the same color to fill as the border.
- (void)drawInContext:(CGContextRef)context {
CGRect circleRect = CGRectInset(self.bounds, 1, 1);
CGColorRef borderColor = [[UIColor whiteColor] CGColor];
CGColorRef backgroundColor = [[UIColor colorWithWhite: 1.0 alpha: 0.15] CGColor];
CGContextSetFillColorWithColor(context, backgroundColor);
CGContextSetStrokeColorWithColor(context, borderColor);
CGContextSetLineWidth(context, 2.0f);
CGContextFillEllipseInRect(context, circleRect);
CGContextStrokeEllipseInRect(context, circleRect);
CGFloat radius = MIN(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect));
CGPoint center = CGPointMake(radius, CGRectGetMidY(circleRect));
CGFloat startAngle = -M_PI / 2;
CGFloat endAngle = self.progress * 2 * M_PI + startAngle;
CGContextSetFillColorWithColor(context, borderColor);
CGContextMoveToPoint(context, center.x, center.y);
CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
CGContextClosePath(context);
CGContextFillPath(context);
[super drawInContext:context];
}
#end
#implementation DZRoundProgressView
+ (Class)layerClass {
return [DZRoundProgressLayer class];
}
- (id)init {
return [self initWithFrame:CGRectMake(0.0f, 0.0f, 37.0f, 37.0f)];
}
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.opaque = NO;
self.layer.contentsScale = [[UIScreen mainScreen] scale];
[self.layer setNeedsDisplay];
}
return self;
}
- (void)setProgress:(CGFloat)progress {
[(id)self.layer setProgress:progress];
}
- (CGFloat)progress {
return [(id)self.layer progress];
}
#end
I suggest you make a keyframe animation instead:
pie.bounds = CGRectMake(-0.5 * radius,
-0.5 * radius,
radius,
radius);
NSMutableArray *values = [NSMutableArray array];
for (int i = 0; i < nrSteps + 1; i++)
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointZero];
[path addLineToPoint:CGPointMake(radius * cosf(startAngle),
radius * sinf(startAngle))];
[path addArcWithCenter:...
endAngle:startAngle + i * (endAngle - startAngle) / nrSteps
...];
[path closePath];
[values addObject:(__bridge id)path.CGPath];
}
Basic animation is good for scalars/vectors. But do you want it to interpolate your paths?
Quick copy/paste Swift translation of this excellent Objective-C answer.
class ProgressView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
genericInit()
}
private func genericInit() {
self.opaque = false;
self.layer.contentsScale = UIScreen.mainScreen().scale
self.layer.setNeedsDisplay()
}
var progress : CGFloat = 0 {
didSet {
(self.layer as! ProgressLayer).progress = progress
}
}
override class func layerClass() -> AnyClass {
return ProgressLayer.self
}
func updateWith(progress : CGFloat) {
self.progress = progress
}
}
class ProgressLayer: CALayer {
#NSManaged var progress : CGFloat
override class func needsDisplayForKey(key: String!) -> Bool{
return key == "progress" || super.needsDisplayForKey(key);
}
override func actionForKey(event: String!) -> CAAction! {
if event == "progress" {
let animation = CABasicAnimation(keyPath: event)
animation.duration = 0.2
animation.fromValue = self.presentationLayer().valueForKey(event)
return animation
}
return super.actionForKey(event)
}
override func drawInContext(ctx: CGContext!) {
if progress != 0 {
let circleRect = CGRectInset(self.bounds, 1, 1)
let borderColor = UIColor.whiteColor().CGColor
let backgroundColor = UIColor.clearColor().CGColor
CGContextSetFillColorWithColor(ctx, backgroundColor)
CGContextSetStrokeColorWithColor(ctx, borderColor)
CGContextSetLineWidth(ctx, 2)
CGContextFillEllipseInRect(ctx, circleRect)
CGContextStrokeEllipseInRect(ctx, circleRect)
let radius = min(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect))
let center = CGPointMake(radius, CGRectGetMidY(circleRect))
let startAngle = CGFloat(-(M_PI/2))
let endAngle = CGFloat(startAngle + 2 * CGFloat(M_PI * Double(progress)))
CGContextSetFillColorWithColor(ctx, borderColor)
CGContextMoveToPoint(ctx, center.x, center.y)
CGContextAddArc(ctx, center.x, center.y, radius, startAngle, endAngle, 0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
}
}
In Swift 3
class ProgressView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
genericInit()
}
private func genericInit() {
self.isOpaque = false;
self.layer.contentsScale = UIScreen.main.scale
self.layer.setNeedsDisplay()
}
var progress : CGFloat = 0 {
didSet {
(self.layer as! ProgressLayer).progress = progress
}
}
override class var layerClass: AnyClass {
return ProgressLayer.self
}
func updateWith(progress : CGFloat) {
self.progress = progress
}
}
class ProgressLayer: CALayer {
#NSManaged var progress : CGFloat
override class func needsDisplay(forKey key: String) -> Bool{
return key == "progress" || super.needsDisplay(forKey: key);
}
override func action(forKey event: String) -> CAAction? {
if event == "progress" {
let animation = CABasicAnimation(keyPath: event)
animation.duration = 0.2
animation.fromValue = self.presentation()?.value(forKey: event) ?? 0
return animation
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
if progress != 0 {
let circleRect = self.bounds.insetBy(dx: 1, dy: 1)
let borderColor = UIColor.white.cgColor
let backgroundColor = UIColor.red.cgColor
ctx.setFillColor(backgroundColor)
ctx.setStrokeColor(borderColor)
ctx.setLineWidth(2)
ctx.fillEllipse(in: circleRect)
ctx.strokeEllipse(in: circleRect)
let radius = min(circleRect.midX, circleRect.midY)
let center = CGPoint(x: radius, y: circleRect.midY)
let startAngle = CGFloat(-(Double.pi/2))
let endAngle = CGFloat(startAngle + 2 * CGFloat(Double.pi * Double(progress)))
ctx.setFillColor(borderColor)
ctx.move(to: CGPoint(x:center.x , y: center.y))
ctx.addArc(center: CGPoint(x:center.x, y: center.y), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
ctx.closePath()
ctx.fillPath()
}
}
}
Related
I am currently making an application that uses a custom View Controller container. Multiple views are on the screen at one time and when one is tapped, the selected view controller animates to full screen. In doing so, the selected view controllers subviews scale as well (frame, font size, etc.) Though, UILabel's font property is not animatable leading to issues. I have tried multiple solutions but all flat out suck.
The solutions I have tried are:
Take a screenshot of the larger view and animating the change (similar to how Flipboard does)
Animate by using the transform property
Zooming out a UIScrollView and zooming it in when brought to full screen.
Setting adjustsFontSizeToFitWidth to YES and setting the fontSize prior to animation
One has been the best solution so far but I am not satisfied with it.
I'm looking for other suggestions if anyone has any or a UILabel substitue that animates smoothly using [UIView animate..].
Here is a good example that is similar to what I would like my UILabel to do:
http://www.cocoawithlove.com/2010/09/zoomingviewcontroller-to-animate-uiview.html
EDIT: This code works
// Load View
self.label = [[UILabel alloc] init];
self.label.text = #"TEXT";
self.label.font = [UIFont boldSystemFontOfSize:20.0];
self.label.backgroundColor = [UIColor clearColor];
[self.label sizeToFit];
[self.view addSubview:self.label];
// Animation
self.label.font = [UIFont boldSystemFontOfSize:80.0];
self.label.transform = CGAffineTransformScale(self.label.transform, .25, .25);
[self.label sizeToFit];
[UIView animateWithDuration:1.0 animations:^{
self.label.transform = CGAffineTransformScale(self.label.transform, 4.0, 4.0);
self.label.center = self.view.center;
} completion:^(BOOL finished) {
self.label.font = [UIFont boldSystemFontOfSize:80.0];
self.label.transform = CGAffineTransformScale(self.label.transform, 1.0, 1.0);
[self.label sizeToFit];
}];
You can change the size and font of your UILabel with animation like below .. here I just put the example of how to change the font of UILabel with transform Animation ..
yourLabel.font = [UIFont boldSystemFontOfSize:35]; // set font size which you want instead of 35
yourLabel.transform = CGAffineTransformScale(yourLabel.transform, 0.35, 0.35);
[UIView animateWithDuration:1.0 animations:^{
yourLabel.transform = CGAffineTransformScale(yourLabel.transform, 5, 5);
}];
For 2017 onwards....
Swift 3.0, 4.0
UIView.animate(withDuration: 0.5) {
label.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) //Scale label area
}
Critical:
The critical point to avoid blurring is you must begin with the biggest size, and shrink it. Then expand to "1" when needed.
For quick "pops" (like a highlight animation) it's OK to expand beyond 1 but if you are transitioning between two sizes, make the larger size the "correct" normal one.
I've created UILabel extension in Swift.
import UIKit
extension UILabel {
func animate(font: UIFont, duration: TimeInterval) {
// let oldFrame = frame
let labelScale = self.font.pointSize / font.pointSize
self.font = font
let oldTransform = transform
transform = transform.scaledBy(x: labelScale, y: labelScale)
// let newOrigin = frame.origin
// frame.origin = oldFrame.origin // only for left aligned text
// frame.origin = CGPoint(x: oldFrame.origin.x + oldFrame.width - frame.width, y: oldFrame.origin.y) // only for right aligned text
setNeedsUpdateConstraints()
UIView.animate(withDuration: duration) {
//L self.frame.origin = newOrigin
self.transform = oldTransform
self.layoutIfNeeded()
}
}
}
Uncomment lines if the label text is left or right aligned.
You could also use CATextLayer which has fontSize as an animatable property.
let startFontSize: CGFloat = 20
let endFontSize: CGFloat = 80
let textLayer = CATextLayer()
textLayer.string = "yourText"
textLayer.font = yourLabel.font.fontName as CFTypeRef?
textLayer.fontSize = startFontSize
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.contentsScale = UIScreen.main.scale //for some reason CATextLayer by default only works for 1x screen resolution and needs this line to work properly on 2x, 3x, etc. ...
textLayer.frame = parentView.bounds
parentView.layer.addSublayer(textLayer)
//animation:
let duration: TimeInterval = 1
textLayer.fontSize = endFontSize //because upon completion of the animation CABasicAnimation resets the animated CALayer to its original state (as opposed to changing its properties to the end state of the animation), setting fontSize to endFontSize right BEFORE the animation starts ensures the fontSize doesn't jump back right after the animation.
let fontSizeAnimation = CABasicAnimation(keyPath: "fontSize")
fontSizeAnimation.fromValue = startFontSize
fontSizeAnimation.toValue = endFontSize
fontSizeAnimation.duration = duration
fontSizeAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
textLayer.add(fontSizeAnimation, forKey: nil)
I used it in my project: https://github.com/yinanq/AngelListJobs
This animation keeps the font top left aligned (unlike CGAffineTransformScale scaling the label from center), pro or con depending on your needs. A disadvantage of CATextLayer is that CALayers don't work with autolayout constraint animation (which I happened to need and solved it by making a UIView containing just the CATextLayer and animating its constraints).
For those not looking for a transform, but actual value change:
UIView.transition(with: label, duration: 0.25, options: .transitionCrossDissolve, animations: {
self.label.font = UIFont.systemFont(ofSize: 15)
}) { isFinished in }
For someone who wants to adjust direction of animation
I have created an extension for UILabel to animate font size change
extension UILabel {
func animate(fontSize: CGFloat, duration: TimeInterval) {
let startTransform = transform
let oldFrame = frame
var newFrame = oldFrame
let scaleRatio = fontSize / font.pointSize
newFrame.size.width *= scaleRatio
newFrame.size.height *= scaleRatio
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * 0.5
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * 0.5
frame = newFrame
font = font.withSize(fontSize)
transform = CGAffineTransform.init(scaleX: 1 / scaleRatio, y: 1 / scaleRatio);
layoutIfNeeded()
UIView.animate(withDuration: duration, animations: {
self.transform = startTransform
newFrame = self.frame
}) { (Bool) in
self.frame = newFrame
}
}
If you want to adjust direction of animation, use below method and put a suitable anchor point.
SWIFT
struct LabelAnimateAnchorPoint {
// You can add more suitable archon point for your needs
static let leadingCenterY = CGPoint.init(x: 0, y: 0.5)
static let trailingCenterY = CGPoint.init(x: 1, y: 0.5)
static let centerXCenterY = CGPoint.init(x: 0.5, y: 0.5)
static let leadingTop = CGPoint.init(x: 0, y: 0)
}
extension UILabel {
func animate(fontSize: CGFloat, duration: TimeInterval, animateAnchorPoint: CGPoint) {
let startTransform = transform
let oldFrame = frame
var newFrame = oldFrame
let archorPoint = layer.anchorPoint
let scaleRatio = fontSize / font.pointSize
layer.anchorPoint = animateAnchorPoint
newFrame.size.width *= scaleRatio
newFrame.size.height *= scaleRatio
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * animateAnchorPoint.x
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * animateAnchorPoint.y
frame = newFrame
font = font.withSize(fontSize)
transform = CGAffineTransform.init(scaleX: 1 / scaleRatio, y: 1 / scaleRatio);
layoutIfNeeded()
UIView.animate(withDuration: duration, animations: {
self.transform = startTransform
newFrame = self.frame
}) { (Bool) in
self.layer.anchorPoint = archorPoint
self.frame = newFrame
}
}
}
OBJECTIVE-C
// You can add more suitable archon point for your needs
#define kLeadingCenterYAnchorPoint CGPointMake(0.f, .5f)
#define kTrailingCenterYAnchorPoint CGPointMake(1.f, .5f)
#define kCenterXCenterYAnchorPoint CGPointMake(.5f, .5f)
#define kLeadingTopAnchorPoint CGPointMake(0.f, 0.f)
#implementation UILabel (FontSizeAnimating)
- (void)animateWithFontSize:(CGFloat)fontSize duration:(NSTimeInterval)duration animateAnchorPoint:(CGPoint)animateAnchorPoint {
CGAffineTransform startTransform = self.transform;
CGRect oldFrame = self.frame;
__block CGRect newFrame = oldFrame;
CGPoint archorPoint = self.layer.anchorPoint;
CGFloat scaleRatio = fontSize / self.font.pointSize;
self.layer.anchorPoint = animateAnchorPoint;
newFrame.size.width *= scaleRatio;
newFrame.size.height *= scaleRatio;
newFrame.origin.x = oldFrame.origin.x - (newFrame.size.width - oldFrame.size.width) * animateAnchorPoint.x;
newFrame.origin.y = oldFrame.origin.y - (newFrame.size.height - oldFrame.size.height) * animateAnchorPoint.y;
self.frame = newFrame;
self.font = [self.font fontWithSize:fontSize];
self.transform = CGAffineTransformScale(self.transform, 1.f / scaleRatio, 1.f / scaleRatio);
[self layoutIfNeeded];
[UIView animateWithDuration:duration animations:^{
self.transform = startTransform;
newFrame = self.frame;
} completion:^(BOOL finished) {
self.layer.anchorPoint = archorPoint;
self.frame = newFrame;
}];
}
#end
For example, to animate changing label font size to 30, duration 1s from center and scale bigger. Simply call
SWIFT
YOUR_LABEL.animate(fontSize: 30, duration: 1, animateAnchorPoint: LabelAnimateAnchorPoint.centerXCenterY)
OBJECTIVE-C
[YOUR_LABEL animateWithFontSize:30
duration:1
animateAnchorPoint:kCenterXCenterYAnchorPoint];
Swift 3.0 & Swift 4.0
UIView.animate(withDuration: 0.5, delay: 0.1, options: .curveLinear, animations: {
label.transform = label.transform.scaledBy(x:4,y:4) //Change x,y to get your desired effect.
} ) { (completed) in
//Animation Completed
}
I found each of the suggestions here inadequate for these reasons:
They don't actually change the font size.
They don't play well with frame sizing & auto layout.
Their interface is non-trivial and/or doesn't play nice inside animation blocks.
In order to retain all of these features & still get a smooth animation transition I've combined the transform approach and the font approach.
The interface is simple. Just update the fontSize property and you'll update the font's size. Do this inside an animation block and it'll animate.
#interface UILabel(MPFontSize)
#property(nonatomic) CGFloat fontSize;
#end
As for the implementation, there's the simple way, and there's the better way.
Simple:
#implementation UILabel(MPFontSize)
- (void)setFontSize:(CGFloat)fontSize {
CGAffineTransform originalTransform = self.transform;
UIFont *targetFont = [self.font fontWithSize:fontSize];
[UIView animateWithDuration:0 delay:0 options:0 animations:^{
self.transform = CGAffineTransformScale( originalTransform,
fontSize / self.fontSize, fontSize / self.fontSize );
} completion:^(BOOL finished) {
self.transform = originalTransform;
if (finished)
self.font = targetFont;
}];
}
- (CGFloat)fontSize {
return self.font.pointSize;
};
#end
Now, the problem with this is that the layout can stutter upon completion, because the view's frame is sized based on the original font all the way until the animation completion, at which point the frame updates to accommodate the target font without animation.
Fixing this problem is a little harder because we need to override intrinsicContentSize. You can do this either by subclassing UILabel or by swizzling the method. I personally swizzle the method, because it lets me keep a generic fontSize property available to all UILabels, but that depends on some library code I can't share here. Here is how you would go about this using subclassing.
Interface:
#interface AnimatableLabel : UILabel
#property(nonatomic) CGFloat fontSize;
#end
Implementation:
#interface AnimatableLabel()
#property(nonatomic) UIFont *targetFont;
#property(nonatomic) UIFont *originalFont;
#end
#implementation AnimatableLabel
- (void)setFontSize:(CGFloat)fontSize {
CGAffineTransform originalTransform = self.transform;
self.originalFont = self.font;
self.targetFont = [self.font fontWithSize:fontSize];
[self invalidateIntrinsicContentSize];
[UIView animateWithDuration:0 delay:0 options:0 animations:^{
self.transform = CGAffineTransformScale( originalTransform,
fontSize / self.fontSize, fontSize / self.fontSize );
} completion:^(BOOL finished) {
self.transform = originalTransform;
if (self.targetFont) {
if (finished)
self.font = self.targetFont;
self.targetFont = self.originalFont = nil;
[self invalidateIntrinsicContentSize];
}
}];
}
- (CGFloat)fontSize {
return self.font.pointSize;
};
- (CGSize)intrinsicContentSize {
#try {
if (self.targetFont)
self.font = self.targetFont;
return self.intrinsicContentSize;
}
#finally {
if (self.originalFont)
self.font = self.originalFont;
}
}
#end
If you want to animate the text size from another anchor point, here is the Swift 5 solution:
How to apply:
yourLabel.setAnimatedFont(.systemFont(ofSize: 48), duration: 0.2, anchorPointX: 0, anchorPointY: 1)
Extensions:
extension UILabel {
/// Animate font size from a given anchor point of the label.
/// - Parameters:
/// - duration: Animation measured in seconds
/// - anchorPointX: 0 = left, 0.5 = center, 1 = right
/// - anchorPointY: 0 = top, 0.5 = center, 1 = bottom
func setAnimatedFont(_ font: UIFont, duration: TimeInterval, anchorPointX: CGFloat, anchorPointY: CGFloat) {
guard let oldFont = self.font else { return }
setAnchorPoint(CGPoint(x: anchorPointX, y: anchorPointY))
self.font = font
let scaleFactor = oldFont.pointSize / font.pointSize
let oldTransform = transform
transform = transform.scaledBy(x: scaleFactor, y: scaleFactor)
setNeedsUpdateConstraints()
UIView.animate(withDuration: duration) {
self.transform = oldTransform
self.layoutIfNeeded()
}
}
}
extension UIView {
/// Change the anchor point without moving the view's position.
/// - Parameters:
/// - point: The layer's bounds rectangle.
func setAnchorPoint(_ point: CGPoint) {
let oldOrigin = frame.origin
layer.anchorPoint = point
let newOrigin = frame.origin
let translation = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)
translatesAutoresizingMaskIntoConstraints = true
center = CGPoint(x: center.x - translation.x, y: center.y - translation.y)
}
}
On UIView you can change the backgroundColour animated. And on a UISlideView you can change the value animated.
Can you add a custom property to your own UIView subclass so that it can be animated?
If I have a CGPath within my UIView then I can animate the drawing of it by changing the percentage drawn of the path.
Can I encapsulate that animation into the subclass.
i.e. I have a UIView with a CGPath that creates a circle.
If the circle is not there it represents 0%. If the circle is full it represents 100%. I can draw any value by changing the percentage drawn of the path. I can also animate the change (within the UIView subclass) by animating the percentage of the CGPath and redrawing the path.
Can I set some property (i.e. percentage) on the UIView so that I can stick the change into a UIView animateWithDuration block and it animate the change of the percentage of the path?
I hope I have explained what I would like to do well.
Essentially, all I want to do is something like...
[UIView animateWithDuration:1.0
animations:^{
myCircleView.percentage = 0.7;
}
completion:nil];
and the circle path animate to the given percentage.
If you extend CALayer and implement your custom
- (void) drawInContext:(CGContextRef) context
You can make an animatable property by overriding needsDisplayForKey (in your custom CALayer class) like this:
+ (BOOL) needsDisplayForKey:(NSString *) key {
if ([key isEqualToString:#"percentage"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
Of course, you also need to have a #property called percentage. From now on you can animate the percentage property using core animation. I did not check whether it works using the [UIView animateWithDuration...] call as well. It might work. But this worked for me:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percentage"];
animation.duration = 1.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:100];
[myCustomLayer addAnimation:animation forKey:#"animatePercentage"];
Oh and to use yourCustomLayer with myCircleView, do this:
[myCircleView.layer addSublayer:myCustomLayer];
Complete Swift 3 example:
public class CircularProgressView: UIView {
public dynamic var progress: CGFloat = 0 {
didSet {
progressLayer.progress = progress
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress),
let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation,
let animation: CABasicAnimation = (action.copy() as? CABasicAnimation) {
animation.keyPath = #keyPath(CircularProgressLayer.progress)
animation.fromValue = progressLayer.progress
animation.toValue = progress
self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
return animation
}
return super.action(for: layer, forKey: event)
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Grey
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
//Red
UIColor.red.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: nil)
There is a great objc article here, which goes into details about how this works
As well as a objc project that uses the same concepts here:
Essentially action(for layer:) will be called when an object is being animated from an animation block, we can start our own animations with the same properties (stolen from the backgroundColor property) and animate the changes.
For the ones who needs more details on that like I did:
there is a cool example from Apple covering this question.
E.g. thanks to it I found that you don't actually need to add your custom layer as sublayer (as #Tom van Zummeren suggests). Instead it's enough to add a class method to your View class:
+ (Class)layerClass
{
return [CustomLayer class];
}
Hope it helps somebody.
you will have to implement the percentage part yourself. you can override layer drawing code to draw your cgpath accroding to the set percentage value. checkout the core animation programming guide and animation types and timing guide
#David Rees answer get me on the right track, but there is one issue. In my case
completion of animation always returns false, right after animation has began.
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: { finished in
// finished - always false
})
This is the way it've worked for me - action of animation is handled inside of CALayer.
I have also included small example how to make layer with additional properties like "color".
In this case, without initializer that copies the values, changing the color would take affect only on non-animating view. During animation it would be visble with "default setting".
public class CircularProgressView: UIView {
#objc public dynamic var progress: CGFloat {
get {
return progressLayer.progress
}
set {
progressLayer.progress = newValue
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
var color: UIColor = .red
// preserve layer properties
// without this specyfic init, if color was changed to sth else
// animation would still use .red
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? CircularProgressLayer {
self.color = layer.color
self.progress = layer.progress
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress) {
guard let animation = action(forKey: #keyPath(backgroundColor)) as? CABasicAnimation else {
setNeedsDisplay()
return nil
}
if let presentation = presentation() {
animation.keyPath = event
animation.fromValue = presentation.value(forKeyPath: event)
animation.toValue = nil
} else {
return nil
}
return animation
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Gray
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
// Red - default
self.color.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
The way to handle animations differently and copy layer properties I have found in this article:
https://medium.com/better-programming/make-apis-like-apple-animatable-view-properties-in-swift-4349b2244cea
I'm trying to draw a shadow under the bottom edge of a UIView in Cocoa Touch. I understand that I should use CGContextSetShadow() to draw the shadow, but the Quartz 2D programming guide is a little vague:
Save the graphics state.
Call the function CGContextSetShadow, passing the appropriate values.
Perform all the drawing to which you want to apply shadows.
Restore the graphics state
I've tried the following in a UIView subclass:
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
CGContextRestoreGState(currentContext);
[super drawRect: rect];
}
..but this doesn't work for me and I'm a bit stuck about (a) where to go next and (b) if there's anything I need to do to my UIView to make this work?
A by far easier approach is to set some layer attributes of the view on initialization:
self.layer.masksToBounds = NO;
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;
You need to import QuartzCore.
#import <QuartzCore/QuartzCore.h>
self.layer.masksToBounds = NO;
self.layer.cornerRadius = 8; // if you like rounded corners
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;
This will slow down the application.
Adding the following line can improve performance as long as your view is visibly rectangular:
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
Same solution, but just to remind you: You can define the shadow directly in the storyboard.
Ex:
In your current code, you save the GState of the current context, configure it to draw a shadow .. and the restore it to what it was before you configured it to draw a shadow. Then, finally, you invoke the superclass's implementation of drawRect: .
Any drawing that should be affected by the shadow setting needs to happen after
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
but before
CGContextRestoreGState(currentContext);
So if you want the superclass's drawRect: to be 'wrapped' in a shadow, then how about if you rearrange your code like this?
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
[super drawRect: rect];
CGContextRestoreGState(currentContext);
}
You can try this .... you can play with the values.
The shadowRadius dictates the amount of blur. shadowOffset dictates where the shadow goes.
Swift 2.0
let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4) //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds = false
demoView.layer.shadowPath = shadowPath.CGPath
Swift 3.0
let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.black.cgColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4) //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds = false
demoView.layer.shadowPath = shadowPath.cgPath
Example with spread
To create a basic shadow
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSizeMake(0.5, 4.0); //Here your control your spread
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
Basic Shadow example in Swift 2.0
Simple and clean solution using Interface Builder
Add a file named UIView.swift in your project (or just paste this in any file) :
import UIKit
#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
}
}
}
Then this will be available in Interface Builder for every view in the Utilities Panel > Attributes Inspector :
You can easily set the shadow now.
Notes:
- The shadow won't appear in IB, only at runtime.
- As Mazen Kasser said
To those who failed in getting this to work [...] make sure Clip Subviews (clipsToBounds) is not enabled
I use this as part of my utils. With this we can not only set shadow but also can get a rounded corner for any UIView. Also you could set what color shadow you prefer. Normally black is preferred but sometimes, when the background is non-white you might want something else. Here's what I use -
in utils.m
+ (void)roundedLayer:(CALayer *)viewLayer
radius:(float)r
shadow:(BOOL)s
{
[viewLayer setMasksToBounds:YES];
[viewLayer setCornerRadius:r];
[viewLayer setBorderColor:[RGB(180, 180, 180) CGColor]];
[viewLayer setBorderWidth:1.0f];
if(s)
{
[viewLayer setShadowColor:[RGB(0, 0, 0) CGColor]];
[viewLayer setShadowOffset:CGSizeMake(0, 0)];
[viewLayer setShadowOpacity:1];
[viewLayer setShadowRadius:2.0];
}
return;
}
To use this we need to call this - [utils roundedLayer:yourview.layer radius:5.0f shadow:YES];
Swift 3
extension UIView {
func installShadow() {
layer.cornerRadius = 2
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 1)
layer.shadowOpacity = 0.45
layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shadowRadius = 1.0
}
}
If you would like to use StoryBoard and wouldnt like to keep typing in runtime attributes, you can easily create an extension to views and make them usable in storyboard.
Step 1. create extension
extension UIView {
#IBInspectable var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
#IBInspectable var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
#IBInspectable var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
#IBInspectable var maskToBound: Bool {
get {
return layer.masksToBounds
}
set {
layer.masksToBounds = newValue
}
}
}
step 2. you can now use these attributes in storyboard
To those who failed in getting this to work (As myself!) after trying all the answers here, just make sure Clip Subviews is not enabled at the Attributes inspector...
Sketch Shadow Using IBDesignable and IBInspectable in Swift 4
HOW TO USE IT
SKETCH AND XCODE SIDE BY SIDE
CODE
#IBDesignable class ShadowView: UIView {
#IBInspectable var shadowColor: UIColor? {
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
return nil
}
set {
if let color = newValue {
layer.shadowColor = color.cgColor
} else {
layer.shadowColor = nil
}
}
}
#IBInspectable var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
#IBInspectable var shadowOffset: CGPoint {
get {
return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
}
set {
layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
}
}
#IBInspectable var shadowBlur: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue / 2.0
}
}
#IBInspectable var shadowSpread: CGFloat = 0 {
didSet {
if shadowSpread == 0 {
layer.shadowPath = nil
} else {
let dx = -shadowSpread
let rect = bounds.insetBy(dx: dx, dy: dx)
layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
}
}
}
OUTPUT
You can use my utility function created for shadow and corner radius as below:
- (void)addShadowWithRadius:(CGFloat)shadowRadius withShadowOpacity:(CGFloat)shadowOpacity withShadowOffset:(CGSize)shadowOffset withShadowColor:(UIColor *)shadowColor withCornerRadius:(CGFloat)cornerRadius withBorderColor:(UIColor *)borderColor withBorderWidth:(CGFloat)borderWidth forView:(UIView *)view{
// drop shadow
[view.layer setShadowRadius:shadowRadius];
[view.layer setShadowOpacity:shadowOpacity];
[view.layer setShadowOffset:shadowOffset];
[view.layer setShadowColor:shadowColor.CGColor];
// border radius
[view.layer setCornerRadius:cornerRadius];
// border
[view.layer setBorderColor:borderColor.CGColor];
[view.layer setBorderWidth:borderWidth];
}
Hope it will help you!!!
Swift 3
self.paddingView.layer.masksToBounds = false
self.paddingView.layer.shadowOffset = CGSize(width: -15, height: 10)
self.paddingView.layer.shadowRadius = 5
self.paddingView.layer.shadowOpacity = 0.5
All Answer all well but I want to add one more point
If you encounter a problem when you have table cells, Deque a new cell there is a mismatch in shadow so in this case, you need to place your shadow code in a layoutSubviews method so that it will behave nicely in all conditions.
-(void)layoutSubviews{
[super layoutSubviews];
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
[VPShadow applyShadowView:self];
}
or in ViewControllers for specific view place shadow code inside the following method so that it's work well
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self.viewShadow layoutIfNeeded];
[VPShadow applyShadowView:self.viewShadow];
}
I have modified my shadow implementation for new devs for more generalized form ex:
/*!
#brief Add shadow to a view.
#param layer CALayer of the view.
*/
+(void)applyShadowOnView:(CALayer *)layer OffsetX:(CGFloat)x OffsetY:(CGFloat)y blur:(CGFloat)radius opacity:(CGFloat)alpha RoundingCorners:(CGFloat)cornerRadius{
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:cornerRadius];
layer.masksToBounds = NO;
layer.shadowColor = [UIColor blackColor].CGColor;
layer.shadowOffset = CGSizeMake(x,y);// shadow x and y
layer.shadowOpacity = alpha;
layer.shadowRadius = radius;// blur effect
layer.shadowPath = shadowPath.CGPath;
}
For fellow Xamarians, the Xamarin.iOS/C# version of the answer would look like the following:
public override void DrawRect(CGRect area, UIViewPrintFormatter formatter)
{
CGContext currentContext = UIGraphics.GetCurrentContext();
currentContext.SaveState();
currentContext.SetShadow(new CGSize(-15, 20), 5);
base.DrawRect(area, formatter);
currentContext.RestoreState();
}
The main difference is that you acquire an instance of CGContext on which you directly call the appropriate methods.
You can use this Extension to add shadow
extension UIView {
func addShadow(offset: CGSize, color: UIColor, radius: CGFloat, opacity: Float)
{
layer.masksToBounds = false
layer.shadowOffset = offset
layer.shadowColor = color.cgColor
layer.shadowRadius = radius
layer.shadowOpacity = opacity
let backgroundCGColor = backgroundColor?.cgColor
backgroundColor = nil
layer.backgroundColor = backgroundCGColor
}
}
you can call it like
your_Custom_View.addShadow(offset: CGSize(width: 0, height: 1), color: UIColor.black, radius: 2.0, opacity: 1.0)
I am developing a commerce application. When I add an item to the shopping cart, I want to create an effect where an image of the item follows a curved path and ends up at the cart tab.
How can I create an animation of an image along a curve like this?
To expand upon what Nikolai said, the best way to handle this is to use Core Animation to animate the motion of the image or view along a Bezier path. This is accomplished using a CAKeyframeAnimation. For example, I've used the following code to animate an image of a view into an icon to indicate saving (as can be seen in the video for this application):
First of all import QuartzCore header file
#import <QuartzCore/QuartzCore.h>
UIImageView *imageViewForAnimation = [[UIImageView alloc] initWithImage:imageToAnimate];
imageViewForAnimation.alpha = 1.0f;
CGRect imageFrame = imageViewForAnimation.frame;
//Your image frame.origin from where the animation need to get start
CGPoint viewOrigin = imageViewForAnimation.frame.origin;
viewOrigin.y = viewOrigin.y + imageFrame.size.height / 2.0f;
viewOrigin.x = viewOrigin.x + imageFrame.size.width / 2.0f;
imageViewForAnimation.frame = imageFrame;
imageViewForAnimation.layer.position = viewOrigin;
[self.view addSubview:imageViewForAnimation];
// Set up fade out effect
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
[fadeOutAnimation setToValue:[NSNumber numberWithFloat:0.3]];
fadeOutAnimation.fillMode = kCAFillModeForwards;
fadeOutAnimation.removedOnCompletion = NO;
// Set up scaling
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(40.0f, imageFrame.size.height * (40.0f / imageFrame.size.width))]];
resizeAnimation.fillMode = kCAFillModeForwards;
resizeAnimation.removedOnCompletion = NO;
// Set up path movement
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
//Setting Endpoint of the animation
CGPoint endPoint = CGPointMake(480.0f - 30.0f, 40.0f);
//to end animation in last tab use
//CGPoint endPoint = CGPointMake( 320-40.0f, 480.0f);
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, viewOrigin.x, viewOrigin.y);
CGPathAddCurveToPoint(curvedPath, NULL, endPoint.x, viewOrigin.y, endPoint.x, viewOrigin.y, endPoint.x, endPoint.y);
pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:fadeOutAnimation, pathAnimation, resizeAnimation, nil]];
group.duration = 0.7f;
group.delegate = self;
[group setValue:imageViewForAnimation forKey:#"imageViewBeingAnimated"];
[imageViewForAnimation.layer addAnimation:group forKey:#"savingAnimation"];
[imageViewForAnimation release];
The way to animate along CGPath using UIView.animateKeyframes (Swift 4)
private func animateNew() {
let alphaFrom: CGFloat = 1
let alphaTo: CGFloat = 0.3
let sizeFrom = CGSize(width: 40, height: 20)
let sizeTo = CGSize(width: 80, height: 60)
let originFrom = CGPoint(x: 40, y: 40)
let originTo = CGPoint(x: 240, y: 480)
let deltaWidth = sizeTo.width - sizeFrom.width
let deltaHeight = sizeTo.height - sizeFrom.height
let deltaAlpha = alphaTo - alphaFrom
// Setting default values
imageViewNew.alpha = alphaFrom
imageViewNew.frame = CGRect(origin: originFrom, size: sizeFrom)
// CGPath setup for calculating points on curve.
let curvedPath = CGMutablePath()
curvedPath.move(to: originFrom)
curvedPath.addQuadCurve(to: originTo, control: CGPoint(x: originFrom.x, y: originTo.y))
let path = Math.BezierPath(cgPath: curvedPath, approximationIterations: 10)
// Calculating timing parameters
let duration: TimeInterval = 0.7
let numberOfKeyFrames = 16
let curvePoints = Math.Easing.timing(numberOfSteps: numberOfKeyFrames, .easeOutQuad)
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeCubic], animations: {
// Iterating curve points and adding key frames
for point in curvePoints {
let origin = path.point(atPercentOfLength: point.end)
let size = CGSize(width: sizeFrom.width + deltaWidth * point.end,
height: sizeFrom.height + deltaHeight * point.end)
let alpha = alphaFrom + deltaAlpha * point.end
UIView.addKeyframe(withRelativeStartTime: TimeInterval(point.start), relativeDuration: TimeInterval(point.duration)) {
self.imageViewNew.frame = CGRect(origin: origin, size: size)
self.imageViewNew.alpha = alpha
}
}
}, completion: nil)
}
File: Math.Easing.swift
// Inspired by: RBBAnimation/RBBEasingFunction.m: https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBEasingFunction.m
extension Math { public struct Easing { } }
extension Math.Easing {
public enum Algorithm: Int {
case linear, easeInQuad, easeOutQuad, easeInOutQuad
}
#inline(__always)
public static func linear(_ t: CGFloat) -> CGFloat {
return t
}
#inline(__always)
public static func easeInQuad(_ t: CGFloat) -> CGFloat {
return t * t
}
#inline(__always)
public static func easeOutQuad(_ t: CGFloat) -> CGFloat {
return t * (2 - t)
}
#inline(__always)
public static func easeInOutQuad(_ t: CGFloat) -> CGFloat {
if t < 0.5 {
return 2 * t * t
} else {
return -1 + (4 - 2 * t) * t
}
}
}
extension Math.Easing {
public struct Timing {
public let start: CGFloat
public let end: CGFloat
public let duration: CGFloat
init(start: CGFloat, end: CGFloat) {
self.start = start
self.end = end
self.duration = end - start
}
public func multiplying(by: CGFloat) -> Timing {
return Timing(start: start * by, end: end * by)
}
}
public static func process(_ t: CGFloat, _ algorithm: Algorithm) -> CGFloat {
switch algorithm {
case .linear:
return linear(t)
case .easeInQuad:
return easeInQuad(t)
case .easeOutQuad:
return easeOutQuad(t)
case .easeInOutQuad:
return easeInOutQuad(t)
}
}
public static func timing(numberOfSteps: Int, _ algorithm: Algorithm) -> [Timing] {
var result: [Timing] = []
let linearStepSize = 1 / CGFloat(numberOfSteps)
for step in (0 ..< numberOfSteps).reversed() {
let linearValue = CGFloat(step) * linearStepSize
let processedValue = process(linearValue, algorithm) // Always in range 0 ... 1
let lastValue = result.last?.start ?? 1
result.append(Timing(start: processedValue, end: lastValue))
}
result = result.reversed()
return result
}
}
File: Math.BezierPath.swift. Look on this SO answer: https://stackoverflow.com/a/50782971/1418981
You can animate a UIView's center property using a CAKeyframeAnimation. See the CoreAnimation programming guide.
Swift 4 version similar to ObjC example from original response.
class KeyFrameAnimationsViewController: ViewController {
let sampleImage = ImageFactory.image(size: CGSize(width: 160, height: 120), fillColor: .blue)
private lazy var imageView = ImageView(image: sampleImage)
private lazy var actionButton = Button(title: "Animate").autolayoutView()
override func setupUI() {
view.addSubviews(imageView, actionButton)
view.backgroundColor = .gray
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]", actionButton).activate()
LayoutConstraint.withFormat("V:|-[*]", actionButton).activate()
}
override func setupHandlers() {
actionButton.setTouchUpInsideHandler { [weak self] in
self?.animate()
}
}
private func animate() {
imageView.alpha = 1
let isRemovedOnCompletion = false
let sizeFrom = CGSize(width: 40, height: 20)
let sizeTo = CGSize(width: 80, height: 60)
let originFrom = CGPoint(x: 40, y: 40)
let originTo = CGPoint(x: 240, y: 480)
imageView.frame = CGRect(origin: originFrom, size: sizeFrom)
imageView.layer.position = originFrom
// Set up fade out effect
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.toValue = 0.3
fadeOutAnimation.fillMode = kCAFillModeForwards
fadeOutAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Set up scaling
let resizeAnimation = CABasicAnimation(keyPath: "bounds.size")
resizeAnimation.toValue = sizeTo
resizeAnimation.fillMode = kCAFillModeForwards
resizeAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Set up path movement
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.isRemovedOnCompletion = isRemovedOnCompletion
// Setting Endpoint of the animation to end animation in last tab use
let curvedPath = CGMutablePath()
curvedPath.move(to: originFrom)
// About curves: https://www.bignerdranch.com/blog/core-graphics-part-4-a-path-a-path/
curvedPath.addQuadCurve(to: originTo, control: CGPoint(x: originFrom.x, y: originTo.y))
pathAnimation.path = curvedPath
let group = CAAnimationGroup()
group.fillMode = kCAFillModeForwards
group.isRemovedOnCompletion = isRemovedOnCompletion
group.animations = [fadeOutAnimation, pathAnimation, resizeAnimation]
group.duration = 0.7
group.setValue(imageView, forKey: "imageViewBeingAnimated")
imageView.layer.add(group, forKey: "savingAnimation")
}
}
I'm trying to draw a shadow under the bottom edge of a UIView in Cocoa Touch. I understand that I should use CGContextSetShadow() to draw the shadow, but the Quartz 2D programming guide is a little vague:
Save the graphics state.
Call the function CGContextSetShadow, passing the appropriate values.
Perform all the drawing to which you want to apply shadows.
Restore the graphics state
I've tried the following in a UIView subclass:
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
CGContextRestoreGState(currentContext);
[super drawRect: rect];
}
..but this doesn't work for me and I'm a bit stuck about (a) where to go next and (b) if there's anything I need to do to my UIView to make this work?
A by far easier approach is to set some layer attributes of the view on initialization:
self.layer.masksToBounds = NO;
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;
You need to import QuartzCore.
#import <QuartzCore/QuartzCore.h>
self.layer.masksToBounds = NO;
self.layer.cornerRadius = 8; // if you like rounded corners
self.layer.shadowOffset = CGSizeMake(-15, 20);
self.layer.shadowRadius = 5;
self.layer.shadowOpacity = 0.5;
This will slow down the application.
Adding the following line can improve performance as long as your view is visibly rectangular:
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
Same solution, but just to remind you: You can define the shadow directly in the storyboard.
Ex:
In your current code, you save the GState of the current context, configure it to draw a shadow .. and the restore it to what it was before you configured it to draw a shadow. Then, finally, you invoke the superclass's implementation of drawRect: .
Any drawing that should be affected by the shadow setting needs to happen after
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
but before
CGContextRestoreGState(currentContext);
So if you want the superclass's drawRect: to be 'wrapped' in a shadow, then how about if you rearrange your code like this?
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetShadow(currentContext, CGSizeMake(-15, 20), 5);
[super drawRect: rect];
CGContextRestoreGState(currentContext);
}
You can try this .... you can play with the values.
The shadowRadius dictates the amount of blur. shadowOffset dictates where the shadow goes.
Swift 2.0
let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4) //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds = false
demoView.layer.shadowPath = shadowPath.CGPath
Swift 3.0
let radius: CGFloat = demoView.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: demoView.frame.height))
//Change 2.1 to amount of spread you need and for height replace the code for height
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.black.cgColor
demoView.layer.shadowOffset = CGSize(width: 0.5, height: 0.4) //Here you control x and y
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
demoView.layer.masksToBounds = false
demoView.layer.shadowPath = shadowPath.cgPath
Example with spread
To create a basic shadow
demoView.layer.cornerRadius = 2
demoView.layer.shadowColor = UIColor.blackColor().CGColor
demoView.layer.shadowOffset = CGSizeMake(0.5, 4.0); //Here your control your spread
demoView.layer.shadowOpacity = 0.5
demoView.layer.shadowRadius = 5.0 //Here your control your blur
Basic Shadow example in Swift 2.0
Simple and clean solution using Interface Builder
Add a file named UIView.swift in your project (or just paste this in any file) :
import UIKit
#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
}
}
}
Then this will be available in Interface Builder for every view in the Utilities Panel > Attributes Inspector :
You can easily set the shadow now.
Notes:
- The shadow won't appear in IB, only at runtime.
- As Mazen Kasser said
To those who failed in getting this to work [...] make sure Clip Subviews (clipsToBounds) is not enabled
I use this as part of my utils. With this we can not only set shadow but also can get a rounded corner for any UIView. Also you could set what color shadow you prefer. Normally black is preferred but sometimes, when the background is non-white you might want something else. Here's what I use -
in utils.m
+ (void)roundedLayer:(CALayer *)viewLayer
radius:(float)r
shadow:(BOOL)s
{
[viewLayer setMasksToBounds:YES];
[viewLayer setCornerRadius:r];
[viewLayer setBorderColor:[RGB(180, 180, 180) CGColor]];
[viewLayer setBorderWidth:1.0f];
if(s)
{
[viewLayer setShadowColor:[RGB(0, 0, 0) CGColor]];
[viewLayer setShadowOffset:CGSizeMake(0, 0)];
[viewLayer setShadowOpacity:1];
[viewLayer setShadowRadius:2.0];
}
return;
}
To use this we need to call this - [utils roundedLayer:yourview.layer radius:5.0f shadow:YES];
Swift 3
extension UIView {
func installShadow() {
layer.cornerRadius = 2
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: 1)
layer.shadowOpacity = 0.45
layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shadowRadius = 1.0
}
}
If you would like to use StoryBoard and wouldnt like to keep typing in runtime attributes, you can easily create an extension to views and make them usable in storyboard.
Step 1. create extension
extension UIView {
#IBInspectable var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
#IBInspectable var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
#IBInspectable var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
#IBInspectable var maskToBound: Bool {
get {
return layer.masksToBounds
}
set {
layer.masksToBounds = newValue
}
}
}
step 2. you can now use these attributes in storyboard
To those who failed in getting this to work (As myself!) after trying all the answers here, just make sure Clip Subviews is not enabled at the Attributes inspector...
Sketch Shadow Using IBDesignable and IBInspectable in Swift 4
HOW TO USE IT
SKETCH AND XCODE SIDE BY SIDE
CODE
#IBDesignable class ShadowView: UIView {
#IBInspectable var shadowColor: UIColor? {
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
return nil
}
set {
if let color = newValue {
layer.shadowColor = color.cgColor
} else {
layer.shadowColor = nil
}
}
}
#IBInspectable var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
#IBInspectable var shadowOffset: CGPoint {
get {
return CGPoint(x: layer.shadowOffset.width, y:layer.shadowOffset.height)
}
set {
layer.shadowOffset = CGSize(width: newValue.x, height: newValue.y)
}
}
#IBInspectable var shadowBlur: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue / 2.0
}
}
#IBInspectable var shadowSpread: CGFloat = 0 {
didSet {
if shadowSpread == 0 {
layer.shadowPath = nil
} else {
let dx = -shadowSpread
let rect = bounds.insetBy(dx: dx, dy: dx)
layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
}
}
}
OUTPUT
You can use my utility function created for shadow and corner radius as below:
- (void)addShadowWithRadius:(CGFloat)shadowRadius withShadowOpacity:(CGFloat)shadowOpacity withShadowOffset:(CGSize)shadowOffset withShadowColor:(UIColor *)shadowColor withCornerRadius:(CGFloat)cornerRadius withBorderColor:(UIColor *)borderColor withBorderWidth:(CGFloat)borderWidth forView:(UIView *)view{
// drop shadow
[view.layer setShadowRadius:shadowRadius];
[view.layer setShadowOpacity:shadowOpacity];
[view.layer setShadowOffset:shadowOffset];
[view.layer setShadowColor:shadowColor.CGColor];
// border radius
[view.layer setCornerRadius:cornerRadius];
// border
[view.layer setBorderColor:borderColor.CGColor];
[view.layer setBorderWidth:borderWidth];
}
Hope it will help you!!!
Swift 3
self.paddingView.layer.masksToBounds = false
self.paddingView.layer.shadowOffset = CGSize(width: -15, height: 10)
self.paddingView.layer.shadowRadius = 5
self.paddingView.layer.shadowOpacity = 0.5
All Answer all well but I want to add one more point
If you encounter a problem when you have table cells, Deque a new cell there is a mismatch in shadow so in this case, you need to place your shadow code in a layoutSubviews method so that it will behave nicely in all conditions.
-(void)layoutSubviews{
[super layoutSubviews];
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
[VPShadow applyShadowView:self];
}
or in ViewControllers for specific view place shadow code inside the following method so that it's work well
-(void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self.viewShadow layoutIfNeeded];
[VPShadow applyShadowView:self.viewShadow];
}
I have modified my shadow implementation for new devs for more generalized form ex:
/*!
#brief Add shadow to a view.
#param layer CALayer of the view.
*/
+(void)applyShadowOnView:(CALayer *)layer OffsetX:(CGFloat)x OffsetY:(CGFloat)y blur:(CGFloat)radius opacity:(CGFloat)alpha RoundingCorners:(CGFloat)cornerRadius{
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:cornerRadius];
layer.masksToBounds = NO;
layer.shadowColor = [UIColor blackColor].CGColor;
layer.shadowOffset = CGSizeMake(x,y);// shadow x and y
layer.shadowOpacity = alpha;
layer.shadowRadius = radius;// blur effect
layer.shadowPath = shadowPath.CGPath;
}
For fellow Xamarians, the Xamarin.iOS/C# version of the answer would look like the following:
public override void DrawRect(CGRect area, UIViewPrintFormatter formatter)
{
CGContext currentContext = UIGraphics.GetCurrentContext();
currentContext.SaveState();
currentContext.SetShadow(new CGSize(-15, 20), 5);
base.DrawRect(area, formatter);
currentContext.RestoreState();
}
The main difference is that you acquire an instance of CGContext on which you directly call the appropriate methods.
You can use this Extension to add shadow
extension UIView {
func addShadow(offset: CGSize, color: UIColor, radius: CGFloat, opacity: Float)
{
layer.masksToBounds = false
layer.shadowOffset = offset
layer.shadowColor = color.cgColor
layer.shadowRadius = radius
layer.shadowOpacity = opacity
let backgroundCGColor = backgroundColor?.cgColor
backgroundColor = nil
layer.backgroundColor = backgroundCGColor
}
}
you can call it like
your_Custom_View.addShadow(offset: CGSize(width: 0, height: 1), color: UIColor.black, radius: 2.0, opacity: 1.0)