3

By Objective-C object I mean something like MyViewController, and a class object MyViewController.superclass.

For example, in this function how would you cast self using targetClass?

// This code doesn't compile
- (void) useOtherClassImplementation :(Class) targetClass :(SEL) targetSelector {
    if ([self isKindOfClass: targetClass]) {
            ((void (*)(id, SEL))[((targetClass *) self) methodForSelector:selector])(self, selector);
        }
}

Is there a way to do something like ((targetClass *) self), which doesn't compile?


Case study

Overview:

When ViewController appears, ViewController.viewDidAppear is called and the swizzled implementation runs. After ViewController.viewDidAppear swizzled implementation runs, the original implementation is called. Good.

When the ViewController.viewDidAppear original implementation runs, UIViewController.viewDidAppear is called by super.viewDidAppear(). The swizzled implementation for UIViewController.viewDidAppear is called and run, and in that swizzled implementation self is used to call the original implementation BUT since self is ViewController and not UIViewController at runtime, ViewController.viewDidAppear swizzled implementation is called again and thus a recursive loop begins.

In other words, the recursive loop starts when a child's method, which has been swizzled, calls its super's method, which has also been swizzled. In the swizzled method self is used to call the original implementation, and since self at runtime is the most child class (in this example ViewController) the super's swizzled method calls the child's original method again, and so the cycle repeats.

Goal:

Find a way to call a swizzled class's original implementation.

When self at runtime could be some child, and both the parent and child have their methods swizzled where the child method calls the parent method, there has to be a way to explicitly choose which class's implementation to run by using the runtime function class_getInstanceMethod

Tried and failed:

Casting self as another class because I cannot find out how to use the Class object to cast. To use this swizzling code in a more generic case, a Class object storing the original class has to be used instead of explicitly writing the class type.

ViewController.swift

// Child class ViewController inherits from parent class UIViewController
class ViewController: UIViewController {

    override func viewDidLoad() {
        _ = ViewController.swizzleViewDidAppearParentAndChild
    }

    override func viewDidAppear(_ animated: Bool) {
        // NOTICE the call to parent's function
        super.viewDidAppear(animated)
        // never reaches here
        print("In viewcontroller viewdidappear")
    }

    // swizzles in the block for both UIViewController and ViewController
    // recursively prints
    //    TestApp.ViewController is about to perform viewDidAppear:
    //
    static var swizzleViewDidAppearParentAndChild: Void = {
        SwizzledObject.createTrampoline(for: UIViewController.self, selector: #selector(UIViewController.viewDidAppear(_:)), with: printBeforePerforming)
        SwizzledObject.createTrampoline(for: ViewController.self, selector: #selector(ViewController.viewDidAppear(_:)), with: printBeforePerforming)
    }()

    // a block to be used before a method call
    static var printBeforePerforming: SelectorTrampolineBlock {
        return { target, selector in
            print("\(NSStringFromClass(type(of: target as AnyObject))) is about to perform \(NSStringFromSelector(selector!))")
        }
    }

}

NSObject+Swizzling.h

#import <Foundation/Foundation.h>

@interface SwizzledObject : NSObject

typedef void (^ SelectorTrampolineBlock)(id target, SEL targetSelector);

+ (SEL) createTrampolineForClass:(Class)targetClass selector:(SEL)targetSelector withBlock:(SelectorTrampolineBlock) block;

@end

NSObject+Swizzling.m

#import "NSObject+Swizzling.h"
#import <objc/runtime.h>

@implementation SwizzledObject

// creates a method at runtime that calls the trampolineBlock, and then performs original method
+ (SEL) createTrampolineForClass:(Class)targetClass selector:(SEL)targetSelector withBlock:(SelectorTrampolineBlock) block {
    SEL trampolineSelector = NSSelectorFromString([NSString stringWithFormat:@"performBefore__%@", NSStringFromSelector(targetSelector)]);

    Method originalMethod = class_getInstanceMethod(targetClass, targetSelector);
    if (originalMethod == nil) {
        return nil;
    }

    IMP dynamicImp = imp_implementationWithBlock(^(id self, bool param) {
        block(self, targetSelector);
        if (!self || ![self respondsToSelector:trampolineSelector]) {return;}
        ((void (*)(id, SEL, bool))[self methodForSelector:trampolineSelector])(self, trampolineSelector, param);
    });

    class_addMethod(targetClass, trampolineSelector, dynamicImp, method_getTypeEncoding(originalMethod));

    Method newMethod = class_getInstanceMethod(targetClass, targetSelector);
    if (newMethod == nil) {
        return nil;
    }

    [SwizzledObject injectSelector:targetClass :trampolineSelector :targetClass :targetSelector];

    return trampolineSelector;
}

// Switches/swizzles method
+ (BOOL) injectSelector:(Class) swizzledClass :(SEL) swizzledSelector :(Class) originalClass :(SEL) orignalSelector {
    NSLog(@"Injecting selector %@ for class %@ with %@", NSStringFromSelector(orignalSelector), NSStringFromClass(originalClass), NSStringFromSelector(swizzledSelector));
    Method newMeth = class_getInstanceMethod(swizzledClass, swizzledSelector);
    IMP imp = method_getImplementation(newMeth);
    const char* methodTypeEncoding = method_getTypeEncoding(newMeth);

    BOOL existing = class_getInstanceMethod(originalClass, orignalSelector) != NULL;

    if (existing) {
        class_addMethod(originalClass, swizzledSelector, imp, methodTypeEncoding);
        newMeth = class_getInstanceMethod(originalClass, swizzledSelector);
        Method orgMeth = class_getInstanceMethod(originalClass, orignalSelector);
        method_exchangeImplementations(orgMeth, newMeth);
    }
    else {
        class_addMethod(originalClass, orignalSelector, imp, methodTypeEncoding);
    }

    return existing;
}

@end

Output

2018-04-04 17:50:43.201458-0700 TestApp[26612:6527489] Injecting selector viewDidAppear: for class UIViewController with performBefore__viewDidAppear:
2018-04-04 17:50:43.202641-0700 TestApp[26612:6527489] Injecting selector viewDidAppear: for class TestApp.ViewController with performBefore__viewDidAppear:
TestApp.ViewController is about to perform viewDidAppear:
TestApp.ViewController is about to perform viewDidAppear:
TestApp.ViewController is about to perform viewDidAppear:
(infinitely prints previous line)

2 Answers2

2

Here is an example of how you might do it:

- (void)useSuperclassImplementation:(Class)targetClass targetSelector:(SEL)targetSelector {
    if ([self isKindOfClass: targetClass] && [targetClass respondsToSelector:targetSelector]) {
        ((void (*)(id, SEL))[targetClass methodForSelector:targetSelector])(self, targetSelector);
    }
}

You could use [targetClass performSelector:targetSelector]; and ignore the warning

There's a detailed explanation of the solution on this answer: https://stackoverflow.com/a/20058585/1755720


edit:

        struct objc_super superInfo = {
            .receiver = [self class],
            .super_class = targetClass
        };

        id (*objc_superAllocTyped)(struct objc_super *, SEL) = (void *)&objc_msgSendSuper;
        (*objc_superAllocTyped)(&superInfo, targetSelector);

^ is also another option to invoke super directly, but it's not too safe as you would really need to be certain the target class is the superclass - and I need to ask, why are you doing this? There might be a simpler solution to the problem.

Oliver Atkinson
  • 7,647
  • 29
  • 41
  • I've used this implementation before, but there's a big issue. If I swizzle both the parent and child class, and in child function() it calls super.function(), then using plain old `self` calls the child function in the child class swizzle, AND in the parent class swizzle the child function is called again (instead of the parent class function) so this becomes recursive. – cuddergambino Apr 04 '18 at 22:19
  • Do you have an example project I could take a look at? – Oliver Atkinson Apr 04 '18 at 22:21
  • I do have an example project, but let me try to give an overview of what's happening first since the project is a little muddled. I'll edit the question in a few minutes and add a real example – cuddergambino Apr 04 '18 at 22:25
1

Phrasing this as casting is just confusing, to the reader and probably yourself. Type casting is a purely static, compile-time thing. targetClass, being a variable, is, of course, a dynamic, run-time thing. At run-time, the static type of the message receiver expression has no bearing on the behavior of the code. That information is more or less gone at that point. Both [self someMethod...] and [(AnyType*)self someMethod...] will have been compiled to the exact same code.

Are you just looking for:

[targetClass instanceMethodForSelector:selector]

where you currently have:

[((targetClass *) self) methodForSelector:selector]

?

Ken Thomases
  • 83,395
  • 7
  • 101
  • 140