Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 182 additions & 62 deletions DetoxSync/DetoxSync/Spies/CAAnimation+DTXSpy.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,105 +12,225 @@
@import ObjectiveC;

static const void* _DTXCAAnimationIsTrackingKey = &_DTXCAAnimationIsTrackingKey;
static const void* _DTXCAAnimationProxyKey = &_DTXCAAnimationProxyKey;

@interface _DTXCAAnimationDelegateHelper : NSObject @end
@implementation _DTXCAAnimationDelegateHelper
#pragma mark - Weak Reference Proxy

- (void)__detox_sync_animationDidStart:(CAAnimation *)anim
@interface _DTXAnimationDelegateProxy : NSProxy <CAAnimationDelegate>
@property (nonatomic, weak) id<CAAnimationDelegate> originalDelegate;
@property (nonatomic, strong) Class originalClass;
@end

@implementation _DTXAnimationDelegateProxy

- (instancetype)initWithDelegate:(id<CAAnimationDelegate>)delegate
{
[anim __detox_sync_trackAnimation];
[self __detox_sync_animationDidStart:anim];
_originalDelegate = delegate;
_originalClass = [delegate class];
return self;
}

- (void)__detox_sync_animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
#pragma mark - CAAnimationDelegate methods (intercepted)

- (void)animationDidStart:(CAAnimation *)anim
{
[self __detox_sync_animationDidStop:anim finished:flag];

[anim __detox_sync_untrackAnimation];
[anim __detox_sync_trackAnimation];

id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate && [delegate respondsToSelector:@selector(animationDidStart:)]) {
[delegate animationDidStart:anim];
}
}
Comment thread
markdevocht marked this conversation as resolved.

@end
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate && [delegate respondsToSelector:@selector(animationDidStop:finished:)]) {
[delegate animationDidStop:anim finished:flag];
}

[anim __detox_sync_untrackAnimation];
}

#pragma mark - Transparent proxy methods (mimic original delegate identity)

- (Class)class
{
return self.originalClass ?: [super class];
}

- (BOOL)isKindOfClass:(Class)aClass
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [delegate isKindOfClass:aClass];
}
return self.originalClass ? [self.originalClass isSubclassOfClass:aClass] : NO;
}

- (BOOL)isMemberOfClass:(Class)aClass
{
return self.originalClass == aClass;
}

- (BOOL)isEqual:(id)object
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [delegate isEqual:object];
}
return self == object;
}

- (NSUInteger)hash
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [delegate hash];
}
return (NSUInteger)self;
}

- (BOOL)conformsToProtocol:(Protocol *)aProtocol
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [delegate conformsToProtocol:aProtocol];
}
return self.originalClass ? class_conformsToProtocol(self.originalClass, aProtocol) : NO;
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
if (aSelector == @selector(animationDidStart:) ||
aSelector == @selector(animationDidStop:finished:)) {
return YES;
}

id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [delegate respondsToSelector:aSelector];
}
return NO;
}

#pragma mark - NSProxy forwarding

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [(NSObject *)delegate methodSignatureForSelector:sel];
}
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

@interface CAAnimation ()
- (void)forwardInvocation:(NSInvocation *)invocation
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate && [delegate respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:delegate];
}
}

- (BOOL)_setCARenderAnimation:(void*)arg1 layer:(id)arg2;
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// Don't forward the methods we intercept
if (aSelector == @selector(animationDidStart:) ||
aSelector == @selector(animationDidStop:finished:)) {
return nil;
}

id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate && [delegate respondsToSelector:aSelector]) {
return delegate;
}
return nil;
}

- (NSString *)description
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [(NSObject *)delegate description];
}
NSString *className = self.originalClass ? NSStringFromClass(self.originalClass) : @"_DTXAnimationDelegateProxy";
return [NSString stringWithFormat:@"<%@: %p (delegate deallocated)>", className, self];
}

- (NSString *)debugDescription
{
id<CAAnimationDelegate> delegate = self.originalDelegate;
if (delegate) {
return [(NSObject *)delegate debugDescription];
}
return [self description];
}

@end

#pragma mark - CAAnimation Extension

@implementation CAAnimation (DTXSpy)

- (BOOL)__detox_sync_isTracking
{
return [objc_getAssociatedObject(self, _DTXCAAnimationIsTrackingKey) boolValue];
return [objc_getAssociatedObject(self, _DTXCAAnimationIsTrackingKey) boolValue];
}

- (void)__detox_sync_setTracking:(BOOL)tracking
{
objc_setAssociatedObject(self, _DTXCAAnimationIsTrackingKey, @(tracking), OBJC_ASSOCIATION_RETAIN);
objc_setAssociatedObject(self, _DTXCAAnimationIsTrackingKey, @(tracking), OBJC_ASSOCIATION_RETAIN);
}

- (void)__detox_sync_trackAnimation
{
[self __detox_sync_untrackAnimation];
[DTXUISyncResource.sharedInstance trackCAAnimation:self];
[self __detox_sync_setTracking:YES];
[self __detox_sync_untrackAnimation];
[DTXUISyncResource.sharedInstance trackCAAnimation:self];
[self __detox_sync_setTracking:YES];
}

- (void)__detox_sync_untrackAnimation
{
if(self.__detox_sync_isTracking == YES)
{
[DTXUISyncResource.sharedInstance untrackCAAnimation:self];
[self __detox_sync_setTracking:NO];
}
if(self.__detox_sync_isTracking == YES)
{
[DTXUISyncResource.sharedInstance untrackCAAnimation:self];
[self __detox_sync_setTracking:NO];
}
}

+ (void)load
{
@autoreleasepool
{
DTXSwizzleMethod(CAAnimation.class, @selector(setDelegate:), @selector(__detox_sync_setDelegate:), NULL);
}
}

- (void)__detox_sync_prepareDelegateIfNeeded:(id<CAAnimationDelegate>)delegate
{
Method mmm = class_getInstanceMethod(delegate.class, NSSelectorFromString(@"__detox_sync_canary"));
if(mmm != NULL)
{
return;
}

NSError* error;

Method m2_helper = class_getInstanceMethod(_DTXCAAnimationDelegateHelper.class, @selector(__detox_sync_animationDidStart:));
if(class_getInstanceMethod(delegate.class, @selector(animationDidStart:)) == NULL)
{
class_addMethod(delegate.class, @selector(animationDidStart:), imp_implementationWithBlock(^(id _self, id anim) { }), method_getTypeEncoding(m2_helper));
}
class_addMethod(delegate.class, @selector(__detox_sync_animationDidStart:), method_getImplementation(m2_helper), method_getTypeEncoding(m2_helper));

DTXSwizzleMethod(delegate.class, @selector(animationDidStart:), @selector(__detox_sync_animationDidStart:), &error);

m2_helper = class_getInstanceMethod(_DTXCAAnimationDelegateHelper.class, @selector(__detox_sync_animationDidStop:finished:));
if(class_getInstanceMethod(delegate.class, @selector(animationDidStop:finished:)) == NULL)
{
class_addMethod(delegate.class, @selector(animationDidStop:finished:), imp_implementationWithBlock(^(id _self, id anim) { }), method_getTypeEncoding(m2_helper));
}
class_addMethod(delegate.class, @selector(__detox_sync_animationDidStop:finished:), method_getImplementation(m2_helper), method_getTypeEncoding(m2_helper));

DTXSwizzleMethod(delegate.class, @selector(animationDidStop:finished:), @selector(__detox_sync_animationDidStop:finished:), &error);

class_addMethod(delegate.class, NSSelectorFromString(@"__detox_sync_canary"), imp_implementationWithBlock(^ (id _self) { }), "v8@0:4");
@autoreleasepool
{
DTXSwizzleMethod(CAAnimation.class, @selector(setDelegate:), @selector(__detox_sync_setDelegate:), NULL);
}
}

- (void)__detox_sync_setDelegate:(id<CAAnimationDelegate>)delegate
{
[self __detox_sync_prepareDelegateIfNeeded:delegate];

[self __detox_sync_setDelegate:delegate];
if (delegate == nil) {
// Clear the proxy reference
objc_setAssociatedObject(self, _DTXCAAnimationProxyKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self __detox_sync_setDelegate:nil];
return;
}

// Don't wrap if already a proxy
if ([object_getClass(delegate) isSubclassOfClass:[_DTXAnimationDelegateProxy class]]) {
[self __detox_sync_setDelegate:delegate];
return;
}

// Create proxy with weak reference to original delegate
_DTXAnimationDelegateProxy *proxy = [[_DTXAnimationDelegateProxy alloc] initWithDelegate:delegate];

// Store proxy with strong reference on the animation (so it stays alive)
objc_setAssociatedObject(self, _DTXCAAnimationProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// Set proxy as the delegate
[self __detox_sync_setDelegate:proxy];
}

@end