Skip to content

Instantly share code, notes, and snippets.

@mikeger
Created October 29, 2015 10:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeger/6824240ddb5ebdf3fc14 to your computer and use it in GitHub Desktop.
Save mikeger/6824240ddb5ebdf3fc14 to your computer and use it in GitHub Desktop.
WireExtendedBlockAnimation
//
// UIView+WR_ExtendedBlockAnimations.h
// Wire
//
// Created by Jacob Persson on 29/07/15.
// Copyright (c) 2015 Wire Swiss GmbH. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <RBBAnimation/RBBEasingFunction.h>
typedef NS_OPTIONS(NSUInteger, WRExtendedBlockAnimationsOptions) {
WRExtendedBlockAnimationsOptionsNone = 0,
WRExtendedBlockAnimationsOptionsBeginFromCurrentState = 1 << 0
};
@interface UIView (WR_ExtendedBlockAnimations)
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing
duration:(NSTimeInterval)duration
animations:(void (^)(void))animations;
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing
duration:(NSTimeInterval)duration
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing
duration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing
duration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(void))animations
options:(WRExtendedBlockAnimationsOptions)options
completion:(void (^)(BOOL finished))completion;
+ (void)wr_animateWithBasicAnimation:(CABasicAnimation *)animation
duration:(NSTimeInterval)duration
animations:(void (^)(void))animations
options:(WRExtendedBlockAnimationsOptions)options
completion:(void (^)(BOOL finished))completion;
@end
//
// UIView+WR_ExtendedBlockAnimations.m
// Wire
//
// Created by Jacob Persson on 29/07/15.
// Copyright (c) 2015 Wire Swiss GmbH. All rights reserved.
//
@import ObjectiveC;
#import <RBBAnimation/RBBTweenAnimation.h>
#import "UIView+WR_ExtendedBlockAnimations.h"
@interface WRAnimationBlockDelegate : NSObject
@property (nonatomic, copy) void(^start)(void);
@property (nonatomic, copy) void(^stop)(BOOL);
+(instancetype)animationDelegateWithBeginning:(void(^)(void))beginning
completion:(void(^)(BOOL finished))completion;
@end
@implementation WRAnimationBlockDelegate
+ (instancetype)animationDelegateWithBeginning:(void (^)(void))beginning
completion:(void (^)(BOOL))completion
{
WRAnimationBlockDelegate *result = [WRAnimationBlockDelegate new];
result.start = beginning;
result.stop = completion;
return result;
}
- (void)animationDidStart:(CAAnimation *)anim
{
if (self.start) {
self.start();
}
self.start = nil;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (self.stop) {
self.stop(flag);
}
self.stop = nil;
}
@end
@interface WRSavedAnimationState : NSObject
@property (nonatomic) CALayer *layer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic) id oldValue;
+ (instancetype)savedStateWithLayer:(CALayer *)layer
keyPath:(NSString *)keyPath;
@end
@implementation WRSavedAnimationState
+ (instancetype)savedStateWithLayer:(CALayer *)layer
keyPath:(NSString *)keyPath
{
WRSavedAnimationState *savedState = [WRSavedAnimationState new];
savedState.layer = layer;
savedState.keyPath = keyPath;
savedState.oldValue = [layer valueForKeyPath:keyPath];
return savedState;
}
@end
@implementation UIView (WR_ExtendedBlockAnimations)
+ (void)load
{
SEL originalSelector = @selector(actionForLayer:forKey:);
SEL extendedSelector = @selector(WR_actionForLayer:forKey:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method extendedMethod = class_getInstanceMethod(self, extendedSelector);
NSAssert(originalMethod, @"original method should exist");
NSAssert(extendedMethod, @"exchanged method should exist");
if(class_addMethod(self, originalSelector, method_getImplementation(extendedMethod), method_getTypeEncoding(extendedMethod))) {
class_replaceMethod(self, extendedSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, extendedMethod);
}
}
+ (NSMutableArray *)WR_savedAnimationStates
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
WR_savedAnimationStates = [NSMutableArray array];
});
return WR_savedAnimationStates;
}
static void *WR_currentAnimationContext = NULL;
static void *WR_extendedBlockAnimationsContext = &WR_extendedBlockAnimationsContext;
static NSArray *supportedAnimatableProperties = nil;
static NSArray *supportedAdditiveAnimatableProperties = nil;
static NSMutableArray *WR_savedAnimationStates = nil;
- (id<CAAction>)WR_actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
supportedAnimatableProperties = @[@"position", @"bounds", @"opacity", @"transform"];
supportedAdditiveAnimatableProperties = @[@"position", @"bounds"];
});
if (WR_currentAnimationContext == WR_extendedBlockAnimationsContext && [supportedAnimatableProperties containsObject:event]) {
// Bounds can't be animated additive so we must split it into two animations
// see https://gist.github.com/d-ronnqvist/137ce6b4c852d8ebd96d
if ([event isEqualToString:@"bounds"]) {
[[UIView WR_savedAnimationStates] addObject:[WRSavedAnimationState savedStateWithLayer:layer
keyPath:@"bounds.origin"]];
[[UIView WR_savedAnimationStates] addObject:[WRSavedAnimationState savedStateWithLayer:layer
keyPath:@"bounds.size"]];
} else {
[[UIView WR_savedAnimationStates] addObject:[WRSavedAnimationState savedStateWithLayer:layer
keyPath:event]];
}
// no implicit animation (it will be added later)
return (id<CAAction>)[NSNull null];
}
// call the original implementation
return [self WR_actionForLayer:layer forKey:event]; // yes, they are swizzled
}
+ (void)wr_animateWithBasicAnimation:(CABasicAnimation *)animation
duration:(NSTimeInterval)duration
animations:(void (^)(void))animations
options:(WRExtendedBlockAnimationsOptions)options
completion:(void (^)(BOOL finished))completion
{
WR_currentAnimationContext = WR_extendedBlockAnimationsContext;
animations();
NSUInteger savedAnimationStateCount = [[self WR_savedAnimationStates] count];
BOOL beginFromCurrentState = (options & WRExtendedBlockAnimationsOptionsBeginFromCurrentState) == WRExtendedBlockAnimationsOptionsBeginFromCurrentState;
if (beginFromCurrentState) {
animation.additive = NO;
}
__block NSUInteger animationCount = 0;
[[UIView WR_savedAnimationStates] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
WRSavedAnimationState *savedState = (WRSavedAnimationState *)obj;
CALayer *layer = savedState.layer;
NSString *keyPath = savedState.keyPath;
id oldValue = savedState.oldValue;
id newValue = [layer valueForKeyPath:keyPath];
CABasicAnimation *anim = [animation copy];
anim.keyPath = keyPath;
anim.duration = duration;
anim.beginTime += [layer convertTime:0 fromLayer:nil]; // Compensate for layer.timeOffset
if (anim.isAdditive && ! [supportedAdditiveAnimatableProperties containsObject:keyPath]) {
anim.additive = NO;
}
if (anim.isAdditive) {
anim.fromValue = [self differenceFromValue:oldValue toValue:newValue];
anim.toValue = [self zeroValueForValue:newValue];
} else if (beginFromCurrentState) {
anim.fromValue = layer.presentationLayer ? [layer.presentationLayer valueForKeyPath:keyPath] : oldValue;
anim.toValue = newValue;
} else {
anim.fromValue = oldValue;
anim.toValue = newValue;
}
anim.delegate = [WRAnimationBlockDelegate animationDelegateWithBeginning:^{
animationCount++;
} completion:^(BOOL finished) {
animationCount--;
if (animationCount == 0) {
if (completion != nil) completion(finished);
}
}];
// Additive animations need a unique or nil animation key
NSString *animationKey = anim.isAdditive ? nil : keyPath;
[layer addAnimation:anim forKey:animationKey];
}];
// clean up (remove all the stored state)
[[self WR_savedAnimationStates] removeAllObjects];
WR_currentAnimationContext = NULL;
if (savedAnimationStateCount == 0) {
// No animations were created
if (completion != nil) completion(YES);
}
}
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing duration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[self wr_animateWithEasing:easing duration:duration delay:0 animations:animations options:WRExtendedBlockAnimationsOptionsNone completion:nil];
}
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
[self wr_animateWithEasing:easing duration:duration delay:0 animations:animations options:WRExtendedBlockAnimationsOptionsNone completion:completion];
}
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
[self wr_animateWithEasing:easing duration:duration delay:delay animations:animations options:WRExtendedBlockAnimationsOptionsNone completion:completion];
}
+ (void)wr_animateWithEasing:(RBBEasingFunction)easing
duration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
animations:(void (^)(void))animations
options:(WRExtendedBlockAnimationsOptions)options
completion:(void (^)(BOOL finished))completion
{
RBBTweenAnimation *animation = [[RBBTweenAnimation alloc] init];
animation.easing = easing;
animation.beginTime = CACurrentMediaTime() + delay;
animation.fillMode = kCAFillModeBoth;
animation.additive = YES;
// RBBTweenAnimation has the same interface as CABasicAnimation so this should be safe
[self wr_animateWithBasicAnimation:(CABasicAnimation *)animation duration:duration animations:animations options:options completion:completion];
}
#pragma mark NSValue helper methods
+ (NSValue *)differenceFromValue:(NSValue *)fromValue toValue:(NSValue *)toValue
{
NSValue *differenceValue = nil;
if (strcmp(fromValue.objCType, @encode(CGPoint)) == 0) {
CGPoint fromPoint = [fromValue CGPointValue];
CGPoint toPoint = [toValue CGPointValue];
CGPoint difference = CGPointMake(fromPoint.x - toPoint.x, fromPoint.y - toPoint.y);
differenceValue = [NSValue valueWithCGPoint:difference];
}
else if (strcmp(fromValue.objCType, @encode(CGSize)) == 0) {
CGSize fromSize = [fromValue CGSizeValue];
CGSize toSize = [toValue CGSizeValue];
CGSize difference = CGSizeMake(fromSize.width - toSize.width, fromSize.height - toSize.height);
differenceValue = [NSValue valueWithCGSize:difference];
}
else if (strcmp(fromValue.objCType, @encode(CGRect)) == 0) {
differenceValue = [NSValue valueWithCGRect:CGRectMake([fromValue CGRectValue].origin.x - [toValue CGRectValue].origin.x,
[fromValue CGRectValue].origin.y - [toValue CGRectValue].origin.y,
[fromValue CGRectValue].size.width - [toValue CGRectValue].size.width,
[fromValue CGRectValue].size.height - [toValue CGRectValue].size.height)];
}
else if ([fromValue isKindOfClass:[NSNumber class]]) {
NSNumber *fromNumber = (NSNumber *)fromValue;
NSNumber *toNumber = (NSNumber *)toValue;
differenceValue = @(fromNumber.doubleValue - toNumber.doubleValue);
}
else {
NSAssert(NO, @"Unsupported difference calculation from value: %@ to value: %@", fromValue, toValue);
}
return differenceValue;
}
+ (NSValue *)zeroValueForValue:(NSValue *)value
{
NSValue *zeroValue = nil;
if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
zeroValue = [NSValue valueWithCGPoint:CGPointZero];
}
else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
zeroValue = [NSValue valueWithCGSize:CGSizeZero];
}
else if (strcmp(value.objCType, @encode(CGRect)) == 0) {
zeroValue = [NSValue valueWithCGRect:CGRectZero];
}
else if ([value isKindOfClass:[NSNumber class]]) {
zeroValue = @(0);
}
else {
NSAssert(NO, @"Undefined zero value for value: %@", value);
}
return zeroValue;
}
@end
@mikeger
Copy link
Author

mikeger commented Oct 29, 2015

This requires RBBAnimation library for the timing functions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment