├── .gitignore ├── Example ├── Animation Examples │ ├── Balls.h │ ├── Balls.m │ ├── CIMenuCloseAnimationView.h │ ├── CIMenuCloseAnimationView.m │ ├── Extra Examples │ │ ├── SACircleFillAnimationView.h │ │ ├── SACircleFillAnimationView.m │ │ ├── SAColorAnimationView.h │ │ ├── SAColorAnimationView.m │ │ ├── SATwoStepAnimationView.h │ │ └── SATwoStepAnimationView.m │ ├── SAPlayPauseAnimationView.h │ ├── SAPlayPauseAnimationView.m │ ├── SATransformAnimationView.h │ └── SATransformAnimationView.m ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── first.imageset │ │ ├── Contents.json │ │ └── first.pdf │ └── second.imageset │ │ ├── Contents.json │ │ └── second.pdf ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── Utils │ ├── CGMath.h │ └── CGMath.m └── main.m ├── License ├── README.md ├── SAAnimationView.podspec ├── SAAnimationView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Example.xcscheme │ └── SAAnimationView.xcscheme └── SAAnimationView ├── Info.plist ├── SAAnimationView.h └── SAAnimationView.m /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ##### 3 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 4 | # 5 | # This is complicated: 6 | # 7 | # SOMETIMES you need to put this file in version control. 8 | # Apple designed it poorly - if you use "custom executables", they are 9 | # saved in this file. 10 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 11 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 12 | 13 | *.pbxuser 14 | *.mode1v3 15 | *.mode2v3 16 | *.perspectivev3 17 | # NB: also, whitelist the default ones, some projects need to use these 18 | !default.pbxuser 19 | !default.mode1v3 20 | !default.mode2v3 21 | !default.perspectivev3 22 | 23 | 24 | #### 25 | # Xcode 4 - semi-personal settings, often included in workspaces 26 | # 27 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them 28 | # 29 | 30 | xcuserdata 31 | !xcschemes 32 | *.xccheckout 33 | 34 | #### 35 | # XCode 4 workspaces - more detailed 36 | # 37 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 38 | # 39 | # Workspace layout is quite spammy. For reference: 40 | # 41 | # (root)/ 42 | # (project-name).xcodeproj/ 43 | # project.pbxproj 44 | # project.xcworkspace/ 45 | # contents.xcworkspacedata 46 | # xcuserdata/ 47 | # (your name)/xcuserdatad/ 48 | # xcuserdata/ 49 | # (your name)/xcuserdatad/ 50 | # 51 | # 52 | # 53 | # Xcode 4 workspaces - SHARED 54 | # 55 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results 56 | # But if you're going to kill personal workspaces, at least keep the shared ones... 57 | # 58 | # 59 | !xcshareddata 60 | 61 | 62 | #### 63 | # Xcode 4 - Deprecated classes 64 | # 65 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 66 | # 67 | # We're using source-control, so this is a "feature" that we do not want! 68 | 69 | *.moved-aside 70 | 71 | 72 | #### 73 | # UNKNOWN: recommended by others, but I can't discover what these files are 74 | # 75 | # ...none. Everything is now explained. 76 | .DS* 77 | .DS_Store 78 | -------------------------------------------------------------------------------- /Example/Animation Examples/Balls.h: -------------------------------------------------------------------------------- 1 | // 2 | // Balls.h 3 | // AnimationExamples 4 | // 5 | // Created by Savvy Apps on 5/3/16. 6 | // Copyright © 2016 Emilio Peláez. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface Balls : SAAnimationView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/Animation Examples/Balls.m: -------------------------------------------------------------------------------- 1 | // 2 | // Balls.m 3 | // AnimationExamples 4 | // 5 | // Created by Savvy Apps on 5/3/16. 6 | // Copyright © 2016 Emilio Peláez. All rights reserved. 7 | // 8 | 9 | #import "Balls.h" 10 | 11 | @implementation Balls 12 | 13 | /* 14 | // Only override drawRect: if you perform custom drawing. 15 | // An empty implementation adversely affects performance during animation. 16 | - (void)drawRect:(CGRect)rect { 17 | // Drawing code 18 | } 19 | */ 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Example/Animation Examples/CIMenuCloseAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CIMenuCloseAnimationView.h 3 | // AnimationTest 4 | // 5 | // Created by Emilio Peláez on 3/2/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface CIMenuCloseAnimationView : SAAnimationView 12 | 13 | +(CGSize)preferredSize; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Example/Animation Examples/CIMenuCloseAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CIMenuCloseAnimationView.m 3 | // AnimationTest 4 | // 5 | // Created by Emilio Peláez on 3/2/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "CIMenuCloseAnimationView.h" 10 | 11 | typedef CGFloat (^EasingFunction)(CGFloat); 12 | 13 | EasingFunction EaseInBack = ^CGFloat (CGFloat p) { 14 | return p * p * p - p * sin(p * M_PI); 15 | }; 16 | 17 | EasingFunction EaseOutCircular = ^CGFloat (CGFloat p) { 18 | return sqrt((2 - p) * p); 19 | }; 20 | 21 | @implementation CIMenuCloseAnimationView 22 | 23 | +(CGSize)preferredSize{ 24 | return CGSizeMake(30, 30); 25 | } 26 | 27 | -(void)initializeView{ 28 | self.behavior = SAAnimationBehaviorPingPong; 29 | self.duration = 1; 30 | 31 | self.playing = YES; 32 | 33 | self.behaviorDelay = .5; 34 | 35 | self.backgroundColor = [UIColor clearColor]; 36 | 37 | // self.transform = CGAffineTransformMakeScale(2, 2); 38 | } 39 | 40 | -(void)drawRect:(CGRect)rect { 41 | CGContextRef c = UIGraphicsGetCurrentContext(); 42 | 43 | UIColor *color = [UIColor colorWithWhite:.25 alpha:1]; 44 | [color setFill]; 45 | [color setStroke]; 46 | CGContextSetLineWidth(c, 4); 47 | 48 | if(self.progress < .5){ 49 | CGFloat localT = [self normalizeProgressWithSliceOffset:0 andSliceDuration:.5]; 50 | localT = EaseInBack(localT); 51 | 52 | CGFloat barOffsetX = 6; 53 | CGFloat barHeight = 4; 54 | CGFloat barWidth = rect.size.width - 2 * barOffsetX; 55 | 56 | UIColor *fillColor = [UIColor colorWithRed:localT green:0 blue:0 alpha:1]; 57 | [fillColor setFill]; 58 | 59 | CGRect middleBarRect = CGRectMake(barOffsetX, (rect.size.height - barHeight)/2, barWidth, barHeight); 60 | [[UIBezierPath bezierPathWithRoundedRect:middleBarRect cornerRadius:2] fill]; 61 | 62 | CGFloat maxDistance = barHeight + 3; 63 | CGFloat currentDistance = (1 - localT) * maxDistance; 64 | 65 | CGRect topBarRect = CGRectMake(middleBarRect.origin.x, middleBarRect.origin.y - currentDistance, 66 | CGRectGetWidth(middleBarRect), CGRectGetHeight(middleBarRect)); 67 | [[UIBezierPath bezierPathWithRoundedRect:topBarRect cornerRadius:2] fill]; 68 | CGRect bottomBarRect = CGRectMake(middleBarRect.origin.x, middleBarRect.origin.y + currentDistance, 69 | CGRectGetWidth(middleBarRect), CGRectGetHeight(middleBarRect)); 70 | [[UIBezierPath bezierPathWithRoundedRect:bottomBarRect cornerRadius:2] fill]; 71 | 72 | CGContextAddEllipseInRect(c, CGRectMake(1, middleBarRect.origin.y, barHeight, barHeight)); 73 | CGContextAddEllipseInRect(c, CGRectMake(1, topBarRect.origin.y, barHeight, barHeight)); 74 | CGContextAddEllipseInRect(c, CGRectMake(1, bottomBarRect.origin.y, barHeight, barHeight)); 75 | 76 | CGContextFillPath(c); 77 | }else if(self.progress <= 1){ 78 | CGFloat localT = [self normalizeProgressWithSliceOffset:.5 andSliceDuration:.5]; 79 | localT = EaseOutCircular(localT); 80 | 81 | CGFloat barHeight = 4; 82 | CGFloat barOffsetX = 6; 83 | CGFloat barWidth = rect.size.width - 2 * barOffsetX; 84 | 85 | [[UIColor darkGrayColor] setFill]; 86 | 87 | CGRect backgroundRect = CGRectInset(CGRectInset(rect, 1, 1), 88 | (1 - localT) * rect.size.width / 2, 89 | (1 - localT) * rect.size.height / 2); 90 | UIBezierPath *backgroundPath = [UIBezierPath bezierPathWithOvalInRect:backgroundRect]; 91 | [backgroundPath fill]; 92 | 93 | [[UIColor redColor] setFill]; 94 | [[UIColor redColor] setStroke]; 95 | 96 | CGRect middleBarRect = CGRectMake(barOffsetX, (rect.size.height - barHeight)/2, barWidth, barHeight); 97 | CGPoint rectCenter = CGPointMake(CGRectGetMidX(middleBarRect), CGRectGetMidY(middleBarRect)); 98 | 99 | UIBezierPath *barPath = [UIBezierPath bezierPathWithRoundedRect:middleBarRect cornerRadius:2]; 100 | 101 | CGAffineTransform transform = CGAffineTransformMakeTranslation(rectCenter.x, rectCenter.y); 102 | transform = CGAffineTransformRotate(transform, M_PI_4 * localT); 103 | transform = CGAffineTransformTranslate(transform, -rectCenter.x, -rectCenter.y); 104 | 105 | [barPath applyTransform:transform]; 106 | [barPath fill]; 107 | 108 | barPath = [UIBezierPath bezierPathWithRoundedRect:middleBarRect cornerRadius:2]; 109 | 110 | transform = CGAffineTransformMakeTranslation(rectCenter.x, rectCenter.y); 111 | transform = CGAffineTransformRotate(transform, -M_PI_4 * localT); 112 | transform = CGAffineTransformTranslate(transform, -rectCenter.x, -rectCenter.y); 113 | 114 | [barPath applyTransform:transform]; 115 | [barPath fill]; 116 | 117 | if(localT == 0) return; 118 | rectCenter = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); 119 | UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rectCenter.x, rectCenter.y) 120 | radius:CGRectGetWidth(rect) / 2 - 3 121 | startAngle:0 endAngle:(1 - MIN(localT, .999)) * 2 * M_PI clockwise:NO]; 122 | circlePath.lineWidth = 4; 123 | circlePath.lineCapStyle = kCGLineCapRound; 124 | transform = CGAffineTransformMakeTranslation(rectCenter.x, rectCenter.y); 125 | transform = CGAffineTransformScale(transform, -1, 1); 126 | transform = CGAffineTransformTranslate(transform, -rectCenter.x, -rectCenter.y); 127 | [circlePath applyTransform:transform]; 128 | [circlePath stroke]; 129 | } 130 | } 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SACircleFillAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SACircleFillAnimationView.h 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 5/20/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface SACircleFillAnimationView : SAAnimationView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SACircleFillAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SACircleFillAnimationView.m 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 5/20/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SACircleFillAnimationView.h" 10 | 11 | @implementation SACircleFillAnimationView 12 | 13 | -(void)initializeView{ 14 | self.behavior = SAAnimationBehaviorPingPong; 15 | self.duration = 1; 16 | 17 | self.playing = YES; 18 | } 19 | 20 | -(void)drawRect:(CGRect)rect { 21 | [[UIColor blueColor] setStroke]; 22 | UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)) 23 | radius:CGRectGetWidth(rect)/2 - 4 24 | startAngle:0 25 | endAngle:2*M_PI*self.progress 26 | clockwise:YES]; 27 | circlePath.lineWidth = 4; 28 | [circlePath stroke]; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SAColorAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SAColorAnimationView.h 3 | // 4 | // Created by Emilio Peláez on 3/2/15. 5 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 6 | // 7 | 8 | #import "SAAnimationView.h" 9 | 10 | @interface SAColorAnimationView : SAAnimationView 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SAColorAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SAColorAnimationView.m 3 | // 4 | // Created by Emilio Peláez on 3/2/15. 5 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 6 | // 7 | 8 | #import "SAColorAnimationView.h" 9 | 10 | @implementation SAColorAnimationView 11 | 12 | -(void)initializeView{ 13 | self.behavior = SAAnimationBehaviorPingPong; 14 | self.duration = 1; 15 | 16 | self.playing = YES; 17 | } 18 | 19 | -(void)drawRect:(CGRect)rect { 20 | CGContextRef c = UIGraphicsGetCurrentContext(); 21 | 22 | UIColor *color = [UIColor colorWithWhite:self.progress alpha:1]; 23 | [color setFill]; 24 | 25 | CGContextFillRect(c, rect); 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SATwoStepAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SATwoStepAnimationView.h 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 5/27/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface SATwoStepAnimationView : SAAnimationView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/Animation Examples/Extra Examples/SATwoStepAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SATwoStepAnimationView.m 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 5/27/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SATwoStepAnimationView.h" 10 | 11 | CGFloat lerp(CGFloat a, CGFloat b, CGFloat t){ 12 | return a + (t * (b - a)); 13 | } 14 | 15 | @implementation SATwoStepAnimationView 16 | 17 | -(void)initializeView{ 18 | self.behavior = SAAnimationBehaviorPingPong; 19 | self.duration = 2; 20 | 21 | self.playing = YES; 22 | } 23 | 24 | -(void)drawRect:(CGRect)rect{ 25 | CGFloat localP; 26 | 27 | CGFloat thirdY = CGRectGetHeight(rect) / 3; 28 | CGFloat thirdMin = MIN(CGRectGetWidth(rect), CGRectGetHeight(rect)) / 3; 29 | CGFloat fourthMin = MIN(CGRectGetWidth(rect), CGRectGetHeight(rect)) / 4; 30 | 31 | [[UIColor blueColor] setFill]; 32 | 33 | if(self.progress < .5){ 34 | localP = [self normalizeProgressWithSliceOffset:0 andSliceDuration:.5]; 35 | UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetWidth(rect)/2, lerp(thirdY * 2, thirdY, localP)) 36 | radius:fourthMin 37 | startAngle:0 38 | endAngle:2 * M_PI 39 | clockwise:YES]; 40 | 41 | [path fill]; 42 | }else{ 43 | localP = [self normalizeProgressWithSliceOffset:.5 andSliceDuration:.5]; 44 | UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetWidth(rect)/2, thirdY) 45 | radius:lerp(fourthMin, thirdMin, localP) 46 | startAngle:0 47 | endAngle:2 * M_PI 48 | clockwise:YES]; 49 | 50 | [path fill]; 51 | } 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Example/Animation Examples/SAPlayPauseAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SAPlayPauseAnimationView.h 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 8/12/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface SAPlayPauseAnimationView : SAAnimationView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/Animation Examples/SAPlayPauseAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SAPlayPauseAnimationView.m 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 8/12/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAPlayPauseAnimationView.h" 10 | #import "CGMath.h" 11 | 12 | @implementation SAPlayPauseAnimationView 13 | 14 | -(void)initializeView{ 15 | self.behavior = SAAnimationBehaviorPingPong; 16 | self.duration = 0.5; 17 | 18 | self.playing = YES; 19 | 20 | self.backgroundColor = [UIColor clearColor]; 21 | 22 | self.behaviorDelay = .25; 23 | } 24 | 25 | -(void)drawRect:(CGRect)rect{ 26 | CGFloat yDifference = CGRectGetHeight(rect)/2 * self.inverseProgress; 27 | 28 | UIBezierPath *path = [UIBezierPath new]; 29 | [path moveToPoint:rect.origin]; 30 | [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinX(rect) + yDifference)]; 31 | [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect) - yDifference)]; 32 | [path addLineToPoint:CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect))]; 33 | [path closePath]; 34 | 35 | [path fill]; 36 | 37 | if(self.progress > .1){ 38 | UIBezierPath *clearPath = [UIBezierPath bezierPathWithRect:CGRectMakeWithCenterAndSize(CGRectGetCenter(rect), CGSizeMake(self.progress * .2 * CGRectGetWidth(rect), CGRectGetHeight(rect)))]; 39 | [clearPath fillWithBlendMode:kCGBlendModeClear alpha:1]; 40 | } 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Example/Animation Examples/SATransformAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SATransformAnimationView.h 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 4/29/16. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface SATransformAnimationView : SAAnimationView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Example/Animation Examples/SATransformAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SATransformAnimationView.m 3 | // AnimationExamples 4 | // 5 | // Created by Emilio Peláez on 4/29/16. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SATransformAnimationView.h" 10 | #import "CGMath.h" 11 | 12 | @interface SATransformAnimationView () { 13 | CGSize lastSize; 14 | } 15 | 16 | @property(nonatomic, strong) NSArray *circleViews; 17 | 18 | @end 19 | 20 | @implementation SATransformAnimationView 21 | 22 | static int numberOfCircles = 5; 23 | 24 | -(void)initializeView{ 25 | NSMutableArray *temp = [NSMutableArray arrayWithCapacity:numberOfCircles]; 26 | for(int i = 0; i < numberOfCircles; i++){ 27 | UIView *circle = [[UIView alloc] initWithFrame:CGRectZero]; 28 | circle.backgroundColor = [UIColor blueColor]; 29 | [self addSubview:circle]; 30 | circle.clipsToBounds = YES; 31 | [temp addObject:circle]; 32 | } 33 | self.circleViews = [temp copy]; 34 | 35 | self.behavior = SAAnimationBehaviorLoop; 36 | self.duration = 1.5; 37 | [self play]; 38 | 39 | lastSize = CGSizeZero; 40 | } 41 | 42 | -(void)setNeedsLayout{ 43 | [super setNeedsLayout]; 44 | } 45 | 46 | -(void)layoutIfNeeded{ 47 | [super layoutIfNeeded]; 48 | } 49 | 50 | -(void)layoutSubviews{ 51 | [super layoutSubviews]; 52 | 53 | if(CGSizeEqualToSize(lastSize, self.bounds.size)) 54 | return; 55 | lastSize = self.bounds.size; 56 | 57 | CGPoint center = CGRectGetCenter(self.bounds); 58 | CGFloat circleSide = MIN(self.bounds.size.width, self.bounds.size.height) / 4; 59 | CGSize circleSize = CGSizeMake(circleSide, circleSide); 60 | CGRect circleFrame = CGRectMakeWithCenterAndSize(center, circleSize); 61 | 62 | [_circleViews enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 63 | obj.frame = circleFrame; 64 | obj.layer.cornerRadius = circleSide / 2; 65 | }]; 66 | 67 | [self update:0]; 68 | } 69 | 70 | -(void)update:(CGFloat)delta{ 71 | CGFloat circleSide = MIN(self.bounds.size.width, self.bounds.size.height) / 4; 72 | CGFloat angleDifference = M_PI * 2 / numberOfCircles; 73 | CGFloat translation = (MIN(self.bounds.size.width, self.bounds.size.height) - circleSide) / 2; 74 | [_circleViews enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 75 | CGFloat angle = (idx * angleDifference) + (self.progress * M_PI * 2); 76 | CGAffineTransform transform = CGAffineTransformMakeTranslation(cos(angle) * translation, sin(angle) * translation); 77 | obj.transform = transform; 78 | }]; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /Example/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Example 4 | // 5 | // Created by Arnold Plakolli on 4.5.21. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow * window; 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /Example/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Example 4 | // 5 | // Created by Arnold Plakolli on 4.5.21. 6 | // 7 | 8 | #import "AppDelegate.h" 9 | 10 | @interface AppDelegate () 11 | 12 | @end 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 17 | return YES; 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "first.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savvyapps/SAAnimationView/bf17e9109a85893b7328484d49a3a0e3f122d49c/Example/Assets.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "second.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/savvyapps/SAAnimationView/bf17e9109a85893b7328484d49a3a0e3f122d49c/Example/Assets.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/Utils/CGMath.h: -------------------------------------------------------------------------------- 1 | // 2 | // CGMath.h 3 | // 4 | // Created by Emilio Peláez on 7/30/11. 5 | // Copyright © 2011 Emilio Peláez. All rights reserved. 6 | // 7 | 8 | #ifndef CGMath_h 9 | #define CGMath_h 10 | 11 | #import 12 | #import "UIKit/UIKit.h" 13 | 14 | // t is the interpolation value. It should be in the range [0, 1], but it's your responsability to make sure it's in that range 15 | // In some cases you might want to use values outside that range, if you don't, you can use clamp 16 | 17 | // Precise method which guarantees v = v1 when t = 1. 18 | // https://en.wikipedia.org/wiki/Linear_interpolation 19 | 20 | NS_INLINE CGFloat lerp(CGFloat v0, CGFloat v1, CGFloat t){ return (1 - t) * v0 + t * v1; } 21 | CGFloat lerpInRange(NSRange r, CGFloat t); 22 | 23 | NS_INLINE CGFloat inverseLerp(CGFloat v0, CGFloat v1, CGFloat v){ return (v - v0) / (v1 - v0); } 24 | CGFloat inverseLerpInRange(NSRange r, CGFloat v); 25 | 26 | NS_INLINE CGFloat clamp(CGFloat min, CGFloat max, CGFloat v){ return MAX(min, MIN(max, v)); } 27 | NS_INLINE CGFloat clamp01(CGFloat v){ return clamp(0, 1, v); } 28 | CGFloat clampToRange(NSRange r, CGFloat v); 29 | 30 | CGFloat remap(CGFloat r00, CGFloat r01, CGFloat r10, CGFloat r11, CGFloat v); 31 | CGFloat remapFromRangeToRange(CGFloat v, NSRange r0, NSRange r1); 32 | 33 | // CGPoint 34 | NS_INLINE CGPoint CGPointAdd(CGPoint point1, CGPoint point2){ return CGPointMake(point1.x + point2.x, point1.y + point2.y); } 35 | 36 | CGPoint CGPointAddBatch(CGPoint *points, NSInteger count); 37 | 38 | NS_INLINE CGPoint CGPointSubstract(CGPoint point1, CGPoint point2){ return CGPointMake(point1.x - point2.x, point1.y - point2.y); } 39 | 40 | NS_INLINE CGPoint CGPointMultiply(CGPoint point, CGFloat scalar){ return CGPointMake(point.x * scalar, point.y * scalar); } 41 | 42 | 43 | NS_INLINE CGFloat CGPointDotProduct(CGPoint point1, CGPoint point2){ return point1.x * point2.x + point1.y * point2.y; } 44 | 45 | NS_INLINE CGFloat CGPointGetMagnitude(CGPoint point){ return sqrtf((point.x * point.x) + (point.y * point.y)); } 46 | 47 | CGPoint CGPointGetNormalized(CGPoint point); 48 | 49 | CGPoint CGPointLerp(CGPoint v0, CGPoint v1, CGFloat t); 50 | 51 | // CGSize 52 | NS_INLINE CGSize CGSizeMin(CGSize size1, CGSize size2){ return CGSizeMake(MIN(size1.width, size2.width), MIN(size1.height, size2.height)); } 53 | NS_INLINE CGSize CGSizeMax(CGSize size1, CGSize size2){ return CGSizeMake(MAX(size1.width, size2.width), MAX(size1.height, size2.height)); } 54 | 55 | NS_INLINE CGSize CGSizeAdd(CGSize size1, CGSize size2){ return CGSizeMake(MAX(0, size1.width + size2.width), MAX(0, size1.height + size2.height)); } 56 | 57 | CGSize CGSizeMakeWithAspectRatioThatFitsSize(CGFloat ratio, CGSize size); 58 | 59 | // Aspect ratio is calculated as the number of times the witdh can fit in the height (ar = width/height) 60 | // A CGSize {200, 100} would have an aspect ratio of 2 61 | NS_INLINE CGFloat CGSizeGetAspectRatio(CGSize size){ return size.width / size.height; } 62 | 63 | CGSize CGSizeLerp(CGSize v0, CGSize v1, CGFloat t); 64 | 65 | NS_INLINE BOOL CGSizeCanFitSize(CGSize size1, CGSize size2){ return size1.width >= size2.height && size1.height >= size2.height; } 66 | 67 | // CGRect 68 | NS_INLINE CGRect CGRectMakeWithOriginAndSize(CGPoint origin, CGSize size){ return CGRectMake(origin.x, origin.y, size.width, size.height); } 69 | 70 | NS_INLINE CGRect CGRectMakeWithCenterAndSize(CGPoint center, CGSize size){ return CGRectMake(center.x - size.width/2, center.y - size.height/2, size.width, size.height); } 71 | 72 | CGRect CGRectMakeWithRectAndAspectRatio(CGRect rect, CGFloat ratio); 73 | 74 | CGRect CGRectMakeWithRectAndAspectRatioFromRect(CGRect rect, CGRect rectForRatio); 75 | 76 | CGRect CGRectLerp(CGRect v0, CGRect v1, CGFloat t); 77 | 78 | CGRect CGRectEdgeInset(CGRect rect, UIEdgeInsets insets); 79 | 80 | NS_INLINE CGPoint CGRectGetCenter(CGRect rect){ return CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); } 81 | 82 | #endif /* CGMath_h */ 83 | -------------------------------------------------------------------------------- /Example/Utils/CGMath.m: -------------------------------------------------------------------------------- 1 | // 2 | // CGMath.m 3 | // 4 | // Created by Emilio Peláez on 7/30/11. 5 | // Copyright © 2011 Emilio Peláez. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "CGMath.h" 10 | 11 | CGFloat lerpInRange(NSRange r, CGFloat t){ 12 | return lerp(r.location, NSMaxRange(r), t); 13 | } 14 | 15 | CGFloat inverseLerpInRange(NSRange r, CGFloat v){ 16 | return inverseLerp(r.location, NSMaxRange(r), v); 17 | } 18 | 19 | CGFloat clampToRange(NSRange r, CGFloat v){ 20 | return clamp(r.location, NSMaxRange(r), v); 21 | } 22 | 23 | CGFloat remap(CGFloat r00, CGFloat r01, CGFloat r10, CGFloat r11, CGFloat v){ 24 | CGFloat t = inverseLerp(r00, r01, v); 25 | return lerp(r10, r11, t); 26 | } 27 | 28 | CGFloat remapFromRangeToRange(CGFloat v, NSRange r0, NSRange r1){ 29 | CGFloat t = inverseLerpInRange(r0, v); 30 | return lerpInRange(r1, t); 31 | } 32 | 33 | #pragma mark - CGPoint 34 | 35 | CGPoint CGPointAddBatch(CGPoint *points, NSInteger count){ 36 | CGFloat x = 0; 37 | CGFloat y = 0; 38 | for(NSInteger i = 0; i < count; i++){ 39 | x += points[i].x; 40 | y += points[i].y; 41 | } 42 | return CGPointMake(x, y); 43 | } 44 | 45 | CGPoint CGPointGetNormalized(CGPoint point){ 46 | CGFloat magnitude = CGPointGetMagnitude(point); 47 | CGFloat x = point.x/magnitude; 48 | CGFloat y = point.y/magnitude; 49 | return CGPointMake(x, y); 50 | } 51 | 52 | CGPoint CGPointLerp(CGPoint v0, CGPoint v1, CGFloat t){ 53 | CGFloat x = lerp(v0.x, v1.x, t); 54 | CGFloat y = lerp(v0.y, v1.y, t); 55 | 56 | return CGPointMake(x, y); 57 | } 58 | 59 | #pragma mark - CGSize 60 | 61 | CGSize CGSizeMakeWithAspectRatioThatFitsSize(CGFloat ratio, CGSize size){ 62 | CGFloat sizeRatio = CGSizeGetAspectRatio(size); 63 | if(ratio > sizeRatio){ 64 | return CGSizeMake(size.width, size.width / ratio); 65 | }else{ 66 | return CGSizeMake(size.height * ratio, size.height); 67 | } 68 | } 69 | 70 | CGSize CGSizeLerp(CGSize v0, CGSize v1, CGFloat t){ 71 | CGFloat width = lerp(v0.width, v1.width, t); 72 | CGFloat height = lerp(v0.height, v1.height, t); 73 | 74 | return CGSizeMake(width, height); 75 | } 76 | 77 | #pragma mark - CGRect 78 | 79 | CGRect CGRectMakeWithRectAndAspectRatio(CGRect rect, CGFloat ratio){ 80 | CGPoint center = CGRectGetCenter(rect); 81 | CGSize size = CGSizeMakeWithAspectRatioThatFitsSize(ratio, rect.size); 82 | 83 | return CGRectMakeWithCenterAndSize(center, size); 84 | } 85 | 86 | CGRect CGRectMakeWithRectAndAspectRatioFromRect(CGRect rect, CGRect rectForRatio){ 87 | CGFloat ratio = CGSizeGetAspectRatio(rectForRatio.size); 88 | return CGRectMakeWithRectAndAspectRatio(rect, ratio); 89 | } 90 | 91 | CGRect CGRectLerp(CGRect v0, CGRect v1, CGFloat t){ 92 | CGPoint origin = CGPointLerp(v0.origin, v1.origin, t); 93 | CGSize size = CGSizeLerp(v0.size, v1.size, t); 94 | 95 | return CGRectMakeWithOriginAndSize(origin, size); 96 | } 97 | 98 | CGRect CGRectEdgeInset(CGRect rect, UIEdgeInsets insets) { 99 | return CGRectMake(CGRectGetMinX(rect) + insets.left, 100 | CGRectGetMinY(rect) + insets.top, 101 | CGRectGetWidth(rect) - insets.left - insets.right, 102 | CGRectGetHeight(rect) - insets.top - insets.bottom); 103 | } 104 | -------------------------------------------------------------------------------- /Example/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Example 4 | // 5 | // Created by Arnold Plakolli on 4.5.21. 6 | // 7 | 8 | #import 9 | #import "AppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | NSString * appDelegateClassName; 13 | @autoreleasepool { 14 | // Setup code that might create autoreleased objects goes here. 15 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 16 | } 17 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 18 | } 19 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Savvy Apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SAAnimationView 2 | 3 | [![Pod Version](https://img.shields.io/cocoapods/v/SAAnimationView.svg?style=flat)](http://cocoapods.org/pods/SAAnimationView) 4 | [![License](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 5 | 6 | `SAAnimationView` is a framework that allows you to create an animation programatically. It uses a `CADisplayLink` to update every frame whenever the animation is not paused. Savvy Apps was initially inspired to create this framework by our work on an animation in [a podcasting app for The Cato Institute](http://savvyapps.com/work/cato-institute). This framework doesn't rely on images, making it easy to change and lessening the burden on the application size. Once the basic code is in place, adding and modifying the animation curve and timing is simple. 7 | 8 | ## Features 9 | 10 | * Pause, resume and reverse the animation 11 | * Total control of the animation 12 | * The duration of the animation can be adjusted without causing the animation to stutter 13 | * Doesn't use images, reducing the application size 14 | * Iterating on the animation is fast 15 | * No need to re-export animation when changes are made (or ever) 16 | 17 | ### Considerations for use 18 | 19 | * Using multiple `SAAnimationViews` in your application at the same time might negatively affect the performance of you app 20 | * `SAAnimationView` is not an out-of-the-box solution, it requires you to implement the actual animation 21 | 22 | ## Examples of use 23 | 24 | ![Cato Audio Animation](https://dl.dropboxusercontent.com/s/ka7avtyyxija30d/cato_saanimationview.gif?dl=0 "Cato Audio Animation") 25 | 26 | ## Installation 27 | 28 | SAAnimationView is available through [CocoaPods](http://cocoapods.org (http://cocoapods.org/)). To install 29 | it, simply add the following line to your Podfile: 30 | 31 | ```ruby 32 | pod 'SAAnimationView' 33 | ``` 34 | 35 | ## How to use 36 | 37 | ### Subclass SAAnimationView 38 | To start using `SAAnimationView`, create a subclass of it and implement your animation logic. Below you will find two examples of simple animations. 39 | ```objc 40 | @interface AnimationView : SAAnimationView 41 | ``` 42 | 43 | This example is a simple animation of an empty circle that is drawn gradually as the animation is executed. 44 | ```objc 45 | - (void)drawRect:(CGRect)rect { 46 | UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)) 47 | radius:CGRectGetWidth(rect) / 2 - 3 48 | startAngle:0 endAngle:(self.progress * 2 * M_PI) clockwise:NO]; 49 | [circlePath stroke]; 50 | } 51 | ``` 52 | 53 | If you don't want to draw your content using CoreGraphics, you can also use views to create an animation. 54 | `SAAnimationView` has a method called `update:` that gets called every frame when the animation is not paused. 55 | 56 | ```objc 57 | - (void)update:(CGFloat)delta { 58 | CGFloat scale = MAX(0.01, self.progress); 59 | self.imageView.transform = CGAffineTransformMakeScale(scale, scale); 60 | } 61 | ``` 62 | 63 | ### Instantiate 64 | You can instantiate your animation view with code like you would any other view, or through Interface Builder by adding a `UIView` element to your scene and setting the custom class to yours. 65 | 66 | ### Initialize 67 | The best way to initialize the properties of your view is to override `initializeView` and use it to set the properties, like `behavior`, `duration`, etc. If you want your application to start playing right away you can also do it here. 68 | 69 | ## Authors / Contributions 70 | 71 | SAAnimationView was authored by Emilio Peláez at [Savvy Apps](http://savvyapps.com). We [encourage feedback](http://savvyapps.com/contact) or pull requests. 72 | 73 | ## License 74 | 75 | SAAnimationView is available under the MIT license. See the LICENSE file for more info. 76 | -------------------------------------------------------------------------------- /SAAnimationView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint SAAnimationView.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | 13 | s.name = "SAAnimationView" 14 | s.version = "1.0.1" 15 | s.summary = "The SAAnimationView framework provides a simple interface to programmatically create an animation." 16 | 17 | s.description = <<-DESC 18 | The SAAnimationView framework provides a simple interface to programmatically create an animation. It doesn't rely on images, giving it a few advantages over other animation frameworks. Because the framework draws the content with code instead of using images, it makes it simple to iterate on, doesn't require additional exports, and decreases the animation's burden on the size of the app. 19 | Read more about SAAnimationView at http://savvyapps.com/blog/saanimationview-framework-programmatically-create-ios-animations 20 | DESC 21 | 22 | s.homepage = "https://github.com/savvyapps/SAAnimationView" 23 | 24 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 25 | 26 | s.license = "MIT" 27 | 28 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 29 | 30 | s.author = { "Emilio Peláez" => "emilio.pelaez@savvyapps.com" } 31 | 32 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 33 | 34 | s.platform = :ios, "7.0" 35 | 36 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 37 | 38 | s.source = { :git => "https://github.com/savvyapps/SAAnimationView.git", :tag => s.version.to_s } 39 | 40 | 41 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 42 | 43 | s.source_files = "SAAnimationView", "SAAnimationView/**/*.{h,m}" 44 | 45 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 46 | 47 | s.framework = "UIKit" 48 | 49 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 50 | 51 | s.requires_arc = true 52 | 53 | end 54 | -------------------------------------------------------------------------------- /SAAnimationView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 98F7170C264165010009DD2C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F7170B264165010009DD2C /* AppDelegate.m */; }; 11 | 98F71715264165010009DD2C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98F71713264165010009DD2C /* Main.storyboard */; }; 12 | 98F71717264165020009DD2C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98F71716264165020009DD2C /* Assets.xcassets */; }; 13 | 98F7171A264165020009DD2C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98F71718264165020009DD2C /* LaunchScreen.storyboard */; }; 14 | 98F7171D264165020009DD2C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F7171C264165020009DD2C /* main.m */; }; 15 | 98F7172E264165B30009DD2C /* CGMath.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F7172C264165B30009DD2C /* CGMath.m */; }; 16 | 98F7175C264166310009DD2C /* SATwoStepAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F7174E264166310009DD2C /* SATwoStepAnimationView.m */; }; 17 | 98F7175D264166310009DD2C /* SAColorAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F71752264166310009DD2C /* SAColorAnimationView.m */; }; 18 | 98F7175E264166310009DD2C /* SACircleFillAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F71753264166310009DD2C /* SACircleFillAnimationView.m */; }; 19 | 98F7175F264166310009DD2C /* Balls.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F71754264166310009DD2C /* Balls.m */; }; 20 | 98F71760264166310009DD2C /* SAPlayPauseAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F71755264166310009DD2C /* SAPlayPauseAnimationView.m */; }; 21 | 98F71761264166310009DD2C /* CIMenuCloseAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F71757264166310009DD2C /* CIMenuCloseAnimationView.m */; }; 22 | 98F71762264166310009DD2C /* SATransformAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F7175A264166310009DD2C /* SATransformAnimationView.m */; }; 23 | 98F71778264168B80009DD2C /* SAAnimationView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98F7169B264160E30009DD2C /* SAAnimationView.framework */; }; 24 | 98F71779264168B80009DD2C /* SAAnimationView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 98F7169B264160E30009DD2C /* SAAnimationView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 25 | 98F717A526416E7F0009DD2C /* SAAnimationView.h in Headers */ = {isa = PBXBuildFile; fileRef = 98F717A326416E7F0009DD2C /* SAAnimationView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 26 | 98F717A626416E7F0009DD2C /* SAAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 98F717A426416E7F0009DD2C /* SAAnimationView.m */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 98F7177A264168B80009DD2C /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 98F71692264160E30009DD2C /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 98F7169A264160E30009DD2C; 35 | remoteInfo = SAAnimationView; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXCopyFilesBuildPhase section */ 40 | 98F7177C264168B80009DD2C /* Embed Frameworks */ = { 41 | isa = PBXCopyFilesBuildPhase; 42 | buildActionMask = 2147483647; 43 | dstPath = ""; 44 | dstSubfolderSpec = 10; 45 | files = ( 46 | 98F71779264168B80009DD2C /* SAAnimationView.framework in Embed Frameworks */, 47 | ); 48 | name = "Embed Frameworks"; 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXCopyFilesBuildPhase section */ 52 | 53 | /* Begin PBXFileReference section */ 54 | 98F7169B264160E30009DD2C /* SAAnimationView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SAAnimationView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 98F7169F264160E30009DD2C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 98F71708264165010009DD2C /* SAAnimationView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SAAnimationView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 98F7170A264165010009DD2C /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 58 | 98F7170B264165010009DD2C /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 59 | 98F71714264165010009DD2C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 60 | 98F71716264165020009DD2C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61 | 98F71719264165020009DD2C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 62 | 98F7171B264165020009DD2C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | 98F7171C264165020009DD2C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 64 | 98F7172C264165B30009DD2C /* CGMath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CGMath.m; sourceTree = ""; }; 65 | 98F7172D264165B30009DD2C /* CGMath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CGMath.h; sourceTree = ""; }; 66 | 98F7174E264166310009DD2C /* SATwoStepAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SATwoStepAnimationView.m; sourceTree = ""; }; 67 | 98F7174F264166310009DD2C /* SAColorAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAColorAnimationView.h; sourceTree = ""; }; 68 | 98F71750264166310009DD2C /* SACircleFillAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SACircleFillAnimationView.h; sourceTree = ""; }; 69 | 98F71751264166310009DD2C /* SATwoStepAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SATwoStepAnimationView.h; sourceTree = ""; }; 70 | 98F71752264166310009DD2C /* SAColorAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAColorAnimationView.m; sourceTree = ""; }; 71 | 98F71753264166310009DD2C /* SACircleFillAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SACircleFillAnimationView.m; sourceTree = ""; }; 72 | 98F71754264166310009DD2C /* Balls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Balls.m; sourceTree = ""; }; 73 | 98F71755264166310009DD2C /* SAPlayPauseAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAPlayPauseAnimationView.m; sourceTree = ""; }; 74 | 98F71756264166310009DD2C /* SATransformAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SATransformAnimationView.h; sourceTree = ""; }; 75 | 98F71757264166310009DD2C /* CIMenuCloseAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CIMenuCloseAnimationView.m; sourceTree = ""; }; 76 | 98F71758264166310009DD2C /* Balls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Balls.h; sourceTree = ""; }; 77 | 98F71759264166310009DD2C /* CIMenuCloseAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CIMenuCloseAnimationView.h; sourceTree = ""; }; 78 | 98F7175A264166310009DD2C /* SATransformAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SATransformAnimationView.m; sourceTree = ""; }; 79 | 98F7175B264166310009DD2C /* SAPlayPauseAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAPlayPauseAnimationView.h; sourceTree = ""; }; 80 | 98F717A326416E7F0009DD2C /* SAAnimationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAAnimationView.h; sourceTree = ""; }; 81 | 98F717A426416E7F0009DD2C /* SAAnimationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAAnimationView.m; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 98F71698264160E30009DD2C /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | 98F71705264165010009DD2C /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | 98F71778264168B80009DD2C /* SAAnimationView.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | /* End PBXFrameworksBuildPhase section */ 101 | 102 | /* Begin PBXGroup section */ 103 | 98F71691264160E30009DD2C = { 104 | isa = PBXGroup; 105 | children = ( 106 | 98F7169D264160E30009DD2C /* SAAnimationView */, 107 | 98F71709264165010009DD2C /* Example */, 108 | 98F7169C264160E30009DD2C /* Products */, 109 | 98F71777264168B80009DD2C /* Frameworks */, 110 | ); 111 | sourceTree = ""; 112 | }; 113 | 98F7169C264160E30009DD2C /* Products */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 98F7169B264160E30009DD2C /* SAAnimationView.framework */, 117 | 98F71708264165010009DD2C /* SAAnimationView.app */, 118 | ); 119 | name = Products; 120 | sourceTree = ""; 121 | }; 122 | 98F7169D264160E30009DD2C /* SAAnimationView */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 98F717A326416E7F0009DD2C /* SAAnimationView.h */, 126 | 98F717A426416E7F0009DD2C /* SAAnimationView.m */, 127 | 98F7169F264160E30009DD2C /* Info.plist */, 128 | ); 129 | path = SAAnimationView; 130 | sourceTree = ""; 131 | }; 132 | 98F71709264165010009DD2C /* Example */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 98F7170A264165010009DD2C /* AppDelegate.h */, 136 | 98F7170B264165010009DD2C /* AppDelegate.m */, 137 | 98F71713264165010009DD2C /* Main.storyboard */, 138 | 98F71716264165020009DD2C /* Assets.xcassets */, 139 | 98F71718264165020009DD2C /* LaunchScreen.storyboard */, 140 | 98F7171B264165020009DD2C /* Info.plist */, 141 | 98F7171C264165020009DD2C /* main.m */, 142 | 98F7172B264165B30009DD2C /* Utils */, 143 | 98F7174C264166310009DD2C /* Animation Examples */, 144 | ); 145 | path = Example; 146 | sourceTree = ""; 147 | }; 148 | 98F7172B264165B30009DD2C /* Utils */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 98F7172D264165B30009DD2C /* CGMath.h */, 152 | 98F7172C264165B30009DD2C /* CGMath.m */, 153 | ); 154 | path = Utils; 155 | sourceTree = ""; 156 | }; 157 | 98F7174C264166310009DD2C /* Animation Examples */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 98F71758264166310009DD2C /* Balls.h */, 161 | 98F71754264166310009DD2C /* Balls.m */, 162 | 98F71756264166310009DD2C /* SATransformAnimationView.h */, 163 | 98F71755264166310009DD2C /* SAPlayPauseAnimationView.m */, 164 | 98F71759264166310009DD2C /* CIMenuCloseAnimationView.h */, 165 | 98F71757264166310009DD2C /* CIMenuCloseAnimationView.m */, 166 | 98F7175B264166310009DD2C /* SAPlayPauseAnimationView.h */, 167 | 98F7175A264166310009DD2C /* SATransformAnimationView.m */, 168 | 98F7174D264166310009DD2C /* Extra Examples */, 169 | ); 170 | path = "Animation Examples"; 171 | sourceTree = ""; 172 | }; 173 | 98F7174D264166310009DD2C /* Extra Examples */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 98F7174F264166310009DD2C /* SAColorAnimationView.h */, 177 | 98F71752264166310009DD2C /* SAColorAnimationView.m */, 178 | 98F71750264166310009DD2C /* SACircleFillAnimationView.h */, 179 | 98F71753264166310009DD2C /* SACircleFillAnimationView.m */, 180 | 98F71751264166310009DD2C /* SATwoStepAnimationView.h */, 181 | 98F7174E264166310009DD2C /* SATwoStepAnimationView.m */, 182 | ); 183 | path = "Extra Examples"; 184 | sourceTree = ""; 185 | }; 186 | 98F71777264168B80009DD2C /* Frameworks */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | ); 190 | name = Frameworks; 191 | sourceTree = ""; 192 | }; 193 | /* End PBXGroup section */ 194 | 195 | /* Begin PBXHeadersBuildPhase section */ 196 | 98F71696264160E30009DD2C /* Headers */ = { 197 | isa = PBXHeadersBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | 98F717A526416E7F0009DD2C /* SAAnimationView.h in Headers */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | /* End PBXHeadersBuildPhase section */ 205 | 206 | /* Begin PBXNativeTarget section */ 207 | 98F7169A264160E30009DD2C /* SAAnimationView */ = { 208 | isa = PBXNativeTarget; 209 | buildConfigurationList = 98F716A3264160E30009DD2C /* Build configuration list for PBXNativeTarget "SAAnimationView" */; 210 | buildPhases = ( 211 | 98F71696264160E30009DD2C /* Headers */, 212 | 98F71697264160E30009DD2C /* Sources */, 213 | 98F71698264160E30009DD2C /* Frameworks */, 214 | 98F71699264160E30009DD2C /* Resources */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | ); 220 | name = SAAnimationView; 221 | productName = SAAnimationView; 222 | productReference = 98F7169B264160E30009DD2C /* SAAnimationView.framework */; 223 | productType = "com.apple.product-type.framework"; 224 | }; 225 | 98F71707264165010009DD2C /* Example */ = { 226 | isa = PBXNativeTarget; 227 | buildConfigurationList = 98F7171E264165020009DD2C /* Build configuration list for PBXNativeTarget "Example" */; 228 | buildPhases = ( 229 | 98F71704264165010009DD2C /* Sources */, 230 | 98F71705264165010009DD2C /* Frameworks */, 231 | 98F71706264165010009DD2C /* Resources */, 232 | 98F7177C264168B80009DD2C /* Embed Frameworks */, 233 | ); 234 | buildRules = ( 235 | ); 236 | dependencies = ( 237 | 98F7177B264168B80009DD2C /* PBXTargetDependency */, 238 | ); 239 | name = Example; 240 | productName = Example; 241 | productReference = 98F71708264165010009DD2C /* SAAnimationView.app */; 242 | productType = "com.apple.product-type.application"; 243 | }; 244 | /* End PBXNativeTarget section */ 245 | 246 | /* Begin PBXProject section */ 247 | 98F71692264160E30009DD2C /* Project object */ = { 248 | isa = PBXProject; 249 | attributes = { 250 | LastUpgradeCheck = 1240; 251 | TargetAttributes = { 252 | 98F7169A264160E30009DD2C = { 253 | CreatedOnToolsVersion = 12.4; 254 | }; 255 | 98F71707264165010009DD2C = { 256 | CreatedOnToolsVersion = 12.4; 257 | }; 258 | }; 259 | }; 260 | buildConfigurationList = 98F71695264160E30009DD2C /* Build configuration list for PBXProject "SAAnimationView" */; 261 | compatibilityVersion = "Xcode 9.3"; 262 | developmentRegion = en; 263 | hasScannedForEncodings = 0; 264 | knownRegions = ( 265 | en, 266 | Base, 267 | ); 268 | mainGroup = 98F71691264160E30009DD2C; 269 | productRefGroup = 98F7169C264160E30009DD2C /* Products */; 270 | projectDirPath = ""; 271 | projectRoot = ""; 272 | targets = ( 273 | 98F7169A264160E30009DD2C /* SAAnimationView */, 274 | 98F71707264165010009DD2C /* Example */, 275 | ); 276 | }; 277 | /* End PBXProject section */ 278 | 279 | /* Begin PBXResourcesBuildPhase section */ 280 | 98F71699264160E30009DD2C /* Resources */ = { 281 | isa = PBXResourcesBuildPhase; 282 | buildActionMask = 2147483647; 283 | files = ( 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | 98F71706264165010009DD2C /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | 98F7171A264165020009DD2C /* LaunchScreen.storyboard in Resources */, 292 | 98F71717264165020009DD2C /* Assets.xcassets in Resources */, 293 | 98F71715264165010009DD2C /* Main.storyboard in Resources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | /* End PBXResourcesBuildPhase section */ 298 | 299 | /* Begin PBXSourcesBuildPhase section */ 300 | 98F71697264160E30009DD2C /* Sources */ = { 301 | isa = PBXSourcesBuildPhase; 302 | buildActionMask = 2147483647; 303 | files = ( 304 | 98F717A626416E7F0009DD2C /* SAAnimationView.m in Sources */, 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | 98F71704264165010009DD2C /* Sources */ = { 309 | isa = PBXSourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | 98F7175F264166310009DD2C /* Balls.m in Sources */, 313 | 98F7175C264166310009DD2C /* SATwoStepAnimationView.m in Sources */, 314 | 98F71761264166310009DD2C /* CIMenuCloseAnimationView.m in Sources */, 315 | 98F7170C264165010009DD2C /* AppDelegate.m in Sources */, 316 | 98F7175D264166310009DD2C /* SAColorAnimationView.m in Sources */, 317 | 98F7171D264165020009DD2C /* main.m in Sources */, 318 | 98F71762264166310009DD2C /* SATransformAnimationView.m in Sources */, 319 | 98F7175E264166310009DD2C /* SACircleFillAnimationView.m in Sources */, 320 | 98F71760264166310009DD2C /* SAPlayPauseAnimationView.m in Sources */, 321 | 98F7172E264165B30009DD2C /* CGMath.m in Sources */, 322 | ); 323 | runOnlyForDeploymentPostprocessing = 0; 324 | }; 325 | /* End PBXSourcesBuildPhase section */ 326 | 327 | /* Begin PBXTargetDependency section */ 328 | 98F7177B264168B80009DD2C /* PBXTargetDependency */ = { 329 | isa = PBXTargetDependency; 330 | target = 98F7169A264160E30009DD2C /* SAAnimationView */; 331 | targetProxy = 98F7177A264168B80009DD2C /* PBXContainerItemProxy */; 332 | }; 333 | /* End PBXTargetDependency section */ 334 | 335 | /* Begin PBXVariantGroup section */ 336 | 98F71713264165010009DD2C /* Main.storyboard */ = { 337 | isa = PBXVariantGroup; 338 | children = ( 339 | 98F71714264165010009DD2C /* Base */, 340 | ); 341 | name = Main.storyboard; 342 | sourceTree = ""; 343 | }; 344 | 98F71718264165020009DD2C /* LaunchScreen.storyboard */ = { 345 | isa = PBXVariantGroup; 346 | children = ( 347 | 98F71719264165020009DD2C /* Base */, 348 | ); 349 | name = LaunchScreen.storyboard; 350 | sourceTree = ""; 351 | }; 352 | /* End PBXVariantGroup section */ 353 | 354 | /* Begin XCBuildConfiguration section */ 355 | 98F716A1264160E30009DD2C /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_ANALYZER_NONNULL = YES; 360 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 361 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 362 | CLANG_CXX_LIBRARY = "libc++"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_ENABLE_OBJC_WEAK = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | COPY_PHASE_STRIP = NO; 389 | CURRENT_PROJECT_VERSION = 1; 390 | DEBUG_INFORMATION_FORMAT = dwarf; 391 | ENABLE_STRICT_OBJC_MSGSEND = YES; 392 | ENABLE_TESTABILITY = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu11; 394 | GCC_DYNAMIC_NO_PIC = NO; 395 | GCC_NO_COMMON_BLOCKS = YES; 396 | GCC_OPTIMIZATION_LEVEL = 0; 397 | GCC_PREPROCESSOR_DEFINITIONS = ( 398 | "DEBUG=1", 399 | "$(inherited)", 400 | ); 401 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 402 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 403 | GCC_WARN_UNDECLARED_SELECTOR = YES; 404 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 405 | GCC_WARN_UNUSED_FUNCTION = YES; 406 | GCC_WARN_UNUSED_VARIABLE = YES; 407 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 408 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 409 | MTL_FAST_MATH = YES; 410 | ONLY_ACTIVE_ARCH = YES; 411 | SDKROOT = iphoneos; 412 | VERSIONING_SYSTEM = "apple-generic"; 413 | VERSION_INFO_PREFIX = ""; 414 | }; 415 | name = Debug; 416 | }; 417 | 98F716A2264160E30009DD2C /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ALWAYS_SEARCH_USER_PATHS = NO; 421 | CLANG_ANALYZER_NONNULL = YES; 422 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 423 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 424 | CLANG_CXX_LIBRARY = "libc++"; 425 | CLANG_ENABLE_MODULES = YES; 426 | CLANG_ENABLE_OBJC_ARC = YES; 427 | CLANG_ENABLE_OBJC_WEAK = YES; 428 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 429 | CLANG_WARN_BOOL_CONVERSION = YES; 430 | CLANG_WARN_COMMA = YES; 431 | CLANG_WARN_CONSTANT_CONVERSION = YES; 432 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 433 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 434 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 435 | CLANG_WARN_EMPTY_BODY = YES; 436 | CLANG_WARN_ENUM_CONVERSION = YES; 437 | CLANG_WARN_INFINITE_RECURSION = YES; 438 | CLANG_WARN_INT_CONVERSION = YES; 439 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 440 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 441 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 442 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 443 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 444 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 445 | CLANG_WARN_STRICT_PROTOTYPES = YES; 446 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 447 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 448 | CLANG_WARN_UNREACHABLE_CODE = YES; 449 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 450 | COPY_PHASE_STRIP = NO; 451 | CURRENT_PROJECT_VERSION = 1; 452 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 453 | ENABLE_NS_ASSERTIONS = NO; 454 | ENABLE_STRICT_OBJC_MSGSEND = YES; 455 | GCC_C_LANGUAGE_STANDARD = gnu11; 456 | GCC_NO_COMMON_BLOCKS = YES; 457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 459 | GCC_WARN_UNDECLARED_SELECTOR = YES; 460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 461 | GCC_WARN_UNUSED_FUNCTION = YES; 462 | GCC_WARN_UNUSED_VARIABLE = YES; 463 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 464 | MTL_ENABLE_DEBUG_INFO = NO; 465 | MTL_FAST_MATH = YES; 466 | SDKROOT = iphoneos; 467 | VALIDATE_PRODUCT = YES; 468 | VERSIONING_SYSTEM = "apple-generic"; 469 | VERSION_INFO_PREFIX = ""; 470 | }; 471 | name = Release; 472 | }; 473 | 98F716A4264160E30009DD2C /* Debug */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | CODE_SIGN_STYLE = Automatic; 477 | DEFINES_MODULE = YES; 478 | DYLIB_COMPATIBILITY_VERSION = 1; 479 | DYLIB_CURRENT_VERSION = 1; 480 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 481 | INFOPLIST_FILE = SAAnimationView/Info.plist; 482 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 483 | LD_RUNPATH_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "@executable_path/Frameworks", 486 | "@loader_path/Frameworks", 487 | ); 488 | MARKETING_VERSION = 1.0.1; 489 | PRODUCT_BUNDLE_IDENTIFIER = com.savvyapps.SAAnimationView; 490 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 491 | SKIP_INSTALL = YES; 492 | TARGETED_DEVICE_FAMILY = "1,2"; 493 | }; 494 | name = Debug; 495 | }; 496 | 98F716A5264160E30009DD2C /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | buildSettings = { 499 | CODE_SIGN_STYLE = Automatic; 500 | DEFINES_MODULE = YES; 501 | DYLIB_COMPATIBILITY_VERSION = 1; 502 | DYLIB_CURRENT_VERSION = 1; 503 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 504 | INFOPLIST_FILE = SAAnimationView/Info.plist; 505 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 506 | LD_RUNPATH_SEARCH_PATHS = ( 507 | "$(inherited)", 508 | "@executable_path/Frameworks", 509 | "@loader_path/Frameworks", 510 | ); 511 | MARKETING_VERSION = 1.0.1; 512 | PRODUCT_BUNDLE_IDENTIFIER = com.savvyapps.SAAnimationView; 513 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 514 | SKIP_INSTALL = YES; 515 | TARGETED_DEVICE_FAMILY = "1,2"; 516 | }; 517 | name = Release; 518 | }; 519 | 98F7171F264165020009DD2C /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 523 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 524 | CODE_SIGN_STYLE = Automatic; 525 | DEVELOPMENT_TEAM = ""; 526 | INFOPLIST_FILE = Example/Info.plist; 527 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 528 | LD_RUNPATH_SEARCH_PATHS = ( 529 | "$(inherited)", 530 | "@executable_path/Frameworks", 531 | ); 532 | PRODUCT_BUNDLE_IDENTIFIER = com.savvyapps.SAAnimationViewExample; 533 | PRODUCT_NAME = SAAnimationView; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | }; 536 | name = Debug; 537 | }; 538 | 98F71720264165020009DD2C /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 543 | CODE_SIGN_STYLE = Automatic; 544 | DEVELOPMENT_TEAM = ""; 545 | INFOPLIST_FILE = Example/Info.plist; 546 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 547 | LD_RUNPATH_SEARCH_PATHS = ( 548 | "$(inherited)", 549 | "@executable_path/Frameworks", 550 | ); 551 | PRODUCT_BUNDLE_IDENTIFIER = com.savvyapps.SAAnimationViewExample; 552 | PRODUCT_NAME = SAAnimationView; 553 | TARGETED_DEVICE_FAMILY = "1,2"; 554 | }; 555 | name = Release; 556 | }; 557 | /* End XCBuildConfiguration section */ 558 | 559 | /* Begin XCConfigurationList section */ 560 | 98F71695264160E30009DD2C /* Build configuration list for PBXProject "SAAnimationView" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | 98F716A1264160E30009DD2C /* Debug */, 564 | 98F716A2264160E30009DD2C /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | 98F716A3264160E30009DD2C /* Build configuration list for PBXNativeTarget "SAAnimationView" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | 98F716A4264160E30009DD2C /* Debug */, 573 | 98F716A5264160E30009DD2C /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | 98F7171E264165020009DD2C /* Build configuration list for PBXNativeTarget "Example" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | 98F7171F264165020009DD2C /* Debug */, 582 | 98F71720264165020009DD2C /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | /* End XCConfigurationList section */ 588 | }; 589 | rootObject = 98F71692264160E30009DD2C /* Project object */; 590 | } 591 | -------------------------------------------------------------------------------- /SAAnimationView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SAAnimationView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SAAnimationView.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SAAnimationView.xcodeproj/xcshareddata/xcschemes/SAAnimationView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /SAAnimationView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /SAAnimationView/SAAnimationView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SAAnimationView.h 3 | // SAAnimationView 4 | // 5 | // Created by Emilio Peláez on 2/26/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /// @brief: The behavior the animation will execute when reaching the end of the animation 12 | typedef NS_ENUM(NSInteger, SAAnimationBehavior){ 13 | SAAnimationBehaviorSingle, /// Stop the animation 14 | SAAnimationBehaviorLoop, /// Reset the progress to 0 15 | SAAnimationBehaviorPingPong /// Set the animation in reverse 16 | }; 17 | 18 | @class SAAnimationView; 19 | 20 | typedef void (^SAAnimationViewBlock)(SAAnimationView *); 21 | 22 | @protocol SAAnimationViewDelegate; 23 | 24 | /*! 25 | SAAnimationView is a class that helps create very dynamic animations. It uses @c CADisplayLink 26 | to create a runloop and update itself every frame. The animation is performed by utilizing 27 | the progress property to manually set the state of the animation, this can be done by setting 28 | the frame/transform of a subview, using the @c drawRect: method to draw custom content, or by 29 | setting any other property that would modify the way the view is rendered 30 | */ 31 | @interface SAAnimationView : UIView 32 | 33 | @property(nonatomic, weak) id delegate; 34 | 35 | /** The current status of the internal @c displayLink */ 36 | @property(nonatomic, getter=isPlaying) BOOL playing; 37 | 38 | /** 39 | The current progress of the animation, it's always in the range of @c[0, 1] 40 | Use this to determine the progress of you animation when drawing or updating any animation values 41 | */ 42 | @property(nonatomic) CGFloat progress; 43 | /** 1 - progress */ 44 | @property(nonatomic, readonly) CGFloat inverseProgress; 45 | 46 | /** The behavior the animation will execute when reaching the end of the animation */ 47 | @property(nonatomic) SAAnimationBehavior behavior; 48 | 49 | /** 50 | Duation has to be greater than 1/60 (1 frame @ 60fps). The actual duration of the animation 51 | will be @code duration/abs(speed) @endcode 52 | */ 53 | @property(nonatomic) NSTimeInterval duration; 54 | /** 55 | Speed can be a negative number. A value -1 will cause the animation to play in reverse. 56 | A value of 0 will pause the animation but not the @c displayLink, which means the animation 57 | will keep refreshing. 58 | */ 59 | @property(nonatomic) CGFloat speed; 60 | 61 | // These delays are in seconds and are not influenced by the speed 62 | /** The delay in seconds to wait before starting the animation */ 63 | @property(nonatomic) NSTimeInterval startDelay; 64 | /** The delay in seconds to wait before executing the behavior at the end of the animation. Won't have any effect if behavior = Single */ 65 | @property(nonatomic) NSTimeInterval behaviorDelay; 66 | 67 | // These blocks will get called when the animation reaches the end, which block gets called 68 | // will depend on the animation behavior 69 | /** The block to be executed when the animation ends and behavior = Single */ 70 | @property(nonatomic, copy) SAAnimationViewBlock completionBlock; 71 | /** The block to be executed when the animation ends and behavior = Loop */ 72 | @property(nonatomic, copy) SAAnimationViewBlock loopBlock; 73 | /** The block to be executed when the animation ends and behavior = PingPong */ 74 | @property(nonatomic, copy) SAAnimationViewBlock pingPongBlock; 75 | 76 | /** 77 | While your drawing code will be usually in drawRect:, you can perform some logic on 'update:' as well. 78 | The default implementation only calls setNeedsDisplay, if you override and don't call super you can control 79 | when to call setNeedsDisplay. It's recommended to only call it when there are changes to be drawn. 80 | @param delta The change in the progress since the last time it was called. 81 | */ 82 | -(void)update:(CGFloat)delta; 83 | 84 | /** Sets @c playing to @c true */ 85 | -(void)play; 86 | /** Sets @c playing to @c false */ 87 | -(void)pause; 88 | /** Sets @c playing to @c false and resets the state of the animation @c progress will be set to 0 if @c speed is greater or equal than 0 and 1 otherwise */ 89 | -(void)stop; 90 | /** Sets the @c speed to the negative of its current value and @c playing to true */ 91 | -(void)reverse; 92 | /** Sets @c playing to @c true and the speed to its absolute value */ 93 | -(void)playForward; 94 | /** Same as @c playForward but also sets @c progress to 0 */ 95 | -(void)playFromBeginning; 96 | /** Sets @c playing @c true and the @c speed to the negative of it's absolute value */ 97 | -(void)playBackwards; 98 | /** Same as playBackwards but sets the progress to 1 */ 99 | -(void)playFromEnd; 100 | 101 | @end 102 | 103 | @interface SAAnimationView (SubclassHelpers) 104 | /** Override this method to set the initial values for your view */ 105 | -(void)initializeView; 106 | 107 | /** 108 | No need to override this, just call it as it is 109 | @see @c normalizeSlice 110 | */ 111 | -(CGFloat)normalizeProgressWithSliceOffset:(CGFloat)sliceOffset andSliceDuration:(CGFloat)sliceDuration; 112 | 113 | @end 114 | 115 | /** 116 | This method is a helper to divide an animation into different "slices". Given a sliceOffset and 117 | a sliceDuration, this method will return a value of where the progress is within that slice, note 118 | that the return value might be outside that slice. 119 | 120 | For example, if you want to divide your animation into three slices or sub-animations, each slice 121 | would have a duration of 0.33; the first slice would have an offset of 0, the second would have 0.33, and 122 | the third one would have 0.66. 123 | 124 | If you want to normalize the total progress in the second slice, you would do 125 | @c normalize(progress,0.33,0.33) when progress is between 0.33 and 0.66, this will normalize the 126 | progress and give you a number between 0 and 1, which is a lot easier to use. 127 | If you use this with a progress lower than 0.33 you'll get a number lower than 0, if you use it with a 128 | progress greater than 0.66 you'll get a number greater than 1 129 | 130 | Example code: 131 | @code 132 | if(self.progress < .33){ 133 | CGFloat normalP = normalizeSlice(self.progress, 0.00, .33); 134 | // Draw first step with normalP in [0,1] 135 | }else if(self.progress < .66){ 136 | CGFloat normalP = normalizeSlice(self.progress, 0.33, .33); 137 | // Draw second step with normalP in [0,1] 138 | }else{ 139 | CGFloat normalP = normalizeSlice(self.progress, 0.66, .33); 140 | // Draw third step with normalP in [0,1] 141 | } 142 | @endcode 143 | 144 | @param progress The current progress to normalize 145 | @param sliceOffset Where the slice will start. Has to be in the @c[0,1] range 146 | @param sliceDuration Duration of slice 147 | */ 148 | CGFloat normalizeSlice(CGFloat progress, CGFloat sliceOffset, CGFloat sliceDuration); 149 | 150 | @protocol SAAnimationViewDelegate 151 | 152 | @optional 153 | 154 | /** Get's called when behavior == SAAnimationBehaviorSingle */ 155 | -(void)animationViewDidFinish:(SAAnimationView *)view; 156 | /** Get's called when behavior == SAAnimationBehaviorLoop */ 157 | -(void)animationViewDidLoop:(SAAnimationView *)view; 158 | /** Get's called when behavior == SAAnimationBehaviorPingPong */ 159 | -(void)animationViewDidPingPong:(SAAnimationView *)view; 160 | 161 | @end 162 | -------------------------------------------------------------------------------- /SAAnimationView/SAAnimationView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SAAnimationView.m 3 | // SAAnimationView 4 | // 5 | // Created by Emilio Peláez on 2/26/15. 6 | // Copyright (c) 2015 Savvy Apps. All rights reserved. 7 | // 8 | 9 | #import "SAAnimationView.h" 10 | 11 | @interface SAAnimationView (){ 12 | NSTimeInterval currentDelay; 13 | } 14 | 15 | @property(nonatomic, strong) CADisplayLink *displayLink; 16 | 17 | -(void)private_InitializeView; 18 | 19 | -(void)runLoop:(CADisplayLink *)displayLink; 20 | 21 | -(void)private_didFinish; 22 | -(void)private_didLoop; 23 | -(void)private_didPingPong; 24 | 25 | @end 26 | 27 | @implementation SAAnimationView 28 | 29 | -(instancetype)initWithFrame:(CGRect)frame{ 30 | self = [super initWithFrame:frame]; 31 | if(self){ 32 | [self private_InitializeView]; 33 | } 34 | return self; 35 | } 36 | 37 | -(instancetype)initWithCoder:(NSCoder *)aDecoder{ 38 | self = [super initWithCoder:aDecoder]; 39 | if(self){ 40 | [self private_InitializeView]; 41 | } 42 | return self; 43 | } 44 | 45 | -(void)private_InitializeView{ 46 | _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(runLoop:)]; 47 | [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 48 | 49 | if(self.duration == 0) self.duration = 1; 50 | if(self.speed == 0) self.speed = 1; 51 | 52 | self.playing = NO; 53 | 54 | [self initializeView]; 55 | 56 | currentDelay = self.startDelay; 57 | } 58 | 59 | -(void)initializeView{} 60 | 61 | -(void)runLoop:(CADisplayLink *)displayLink{ 62 | CGFloat delta = displayLink.duration; 63 | 64 | if(currentDelay > 0){ 65 | currentDelay -= delta; 66 | if(currentDelay > 0) return; 67 | delta = -currentDelay; 68 | } 69 | 70 | if(self.speed == 0) return; 71 | 72 | delta = delta * self.speed / self.duration; 73 | 74 | self.progress += delta; 75 | [self update:delta]; 76 | } 77 | 78 | -(void)update:(CGFloat)delta{ 79 | [self setNeedsDisplay]; 80 | } 81 | 82 | -(void)setPlaying:(BOOL)playing{ 83 | self.displayLink.paused = !playing; 84 | } 85 | 86 | -(BOOL)isPlaying{ 87 | return !self.displayLink.paused; 88 | } 89 | 90 | -(void)setProgress:(CGFloat)progress{ 91 | if(self.speed == 0) return; 92 | 93 | switch (self.behavior) { 94 | case SAAnimationBehaviorSingle: 95 | if((progress > 1 && self.speed > 0) || (progress < 0 && self.speed < 0)){ 96 | self.playing = NO; 97 | [self private_didFinish]; 98 | } 99 | _progress = MAX(0, MIN(1, progress)); 100 | break; 101 | case SAAnimationBehaviorLoop: 102 | if(progress > 1 && self.speed > 0){ 103 | if(self.behaviorDelay > 0){ 104 | currentDelay = self.behaviorDelay; 105 | _progress = 0; 106 | }else{ 107 | _progress = progress - 1; 108 | } 109 | [self private_didLoop]; 110 | }else if(progress < 0 && self.speed < 0){ 111 | if(self.behaviorDelay > 0){ 112 | currentDelay = self.behaviorDelay; 113 | _progress = 1; 114 | }else{ 115 | _progress = progress + 1; 116 | } 117 | [self private_didLoop]; 118 | }else{ 119 | _progress = MAX(0, MIN(1, progress)); 120 | } 121 | break; 122 | case SAAnimationBehaviorPingPong: 123 | if(progress > 1 && self.speed > 0){ 124 | if(self.behaviorDelay > 0){ 125 | currentDelay = self.behaviorDelay; 126 | _progress = 1; 127 | }else{ 128 | _progress = 2 - progress; 129 | } 130 | self.speed = -self.speed; 131 | [self private_didPingPong]; 132 | }else if(progress < 0 && self.speed < 0){ 133 | if(self.behaviorDelay > 0){ 134 | currentDelay = self.behaviorDelay; 135 | _progress = 0; 136 | }else{ 137 | _progress = -progress; 138 | } 139 | self.speed = -self.speed; 140 | [self private_didPingPong]; 141 | }else{ 142 | _progress = MAX(0, MIN(1, progress)); 143 | } 144 | break; 145 | default: 146 | break; 147 | } 148 | } 149 | 150 | -(void)setDuration:(NSTimeInterval)duration{ 151 | _duration = MAX(1.0/60, duration); 152 | } 153 | 154 | -(void)setStartDelay:(NSTimeInterval)startDelay{ 155 | _startDelay = MAX(0, startDelay); 156 | } 157 | 158 | -(void)setBehaviorDelay:(NSTimeInterval)behaviorDelay{ 159 | _behaviorDelay = MAX(0, behaviorDelay); 160 | } 161 | 162 | -(CGFloat)inverseProgress{ 163 | return 1 - _progress; 164 | } 165 | 166 | -(CGFloat)normalizeProgressWithSliceOffset:(CGFloat)sliceOffset andSliceDuration:(CGFloat)sliceDuration{ 167 | return normalizeSlice(self.progress, sliceOffset, sliceDuration); 168 | } 169 | 170 | -(void)play{ 171 | self.playing = YES; 172 | } 173 | 174 | -(void)pause{ 175 | self.playing = NO; 176 | } 177 | 178 | -(void)stop{ 179 | self.playing = NO; 180 | self.progress = self.speed >= 0 ? 0 : 1; 181 | } 182 | 183 | -(void)playForward{ 184 | self.speed = fabs(self.speed); 185 | self.playing = YES; 186 | } 187 | 188 | -(void)playFromBeginning{ 189 | currentDelay = self.startDelay; 190 | self.speed = fabs(self.speed); 191 | self.progress = 0; 192 | self.playing = YES; 193 | } 194 | 195 | -(void)reverse{ 196 | self.speed = -self.speed; 197 | self.playing = YES; 198 | } 199 | 200 | -(void)playBackwards{ 201 | self.speed = -fabs(self.speed); 202 | self.playing = YES; 203 | } 204 | 205 | -(void)playFromEnd{ 206 | self.progress = 1; 207 | self.speed = -fabs(self.speed); 208 | self.playing = YES; 209 | } 210 | 211 | -(void)private_didFinish{ 212 | if([self.delegate respondsToSelector:@selector(animationViewDidFinish:)]) 213 | [self.delegate animationViewDidFinish:self]; 214 | if(self.completionBlock) 215 | self.completionBlock(self); 216 | } 217 | 218 | -(void)private_didLoop{ 219 | if([self.delegate respondsToSelector:@selector(animationViewDidLoop:)]) 220 | [self.delegate animationViewDidLoop:self]; 221 | if(self.loopBlock) 222 | self.loopBlock(self); 223 | } 224 | 225 | -(void)private_didPingPong{ 226 | if([self.delegate respondsToSelector:@selector(animationViewDidPingPong:)]) 227 | [self.delegate animationViewDidPingPong:self]; 228 | if(self.pingPongBlock) 229 | self.pingPongBlock(self); 230 | } 231 | 232 | @end 233 | 234 | CGFloat normalizeSlice(CGFloat progress, CGFloat offset, CGFloat duration){ 235 | return (progress - offset) / duration; 236 | } 237 | --------------------------------------------------------------------------------