LulzCow
LulzCow

Reputation: 1229

How to add a method to a class at runtime in swift

I'm trying to debug a mysterious crash I'm seeing in Crashlytics, but haven't been able to reproduce myself.

The error message looks like this:

Fatal Exception: NSInvalidArgumentException
-[NSNull compare:]: unrecognized selector sent to instance 0x1e911bc30
-[NSOrderedSet initWithSet:copyItems:]

Here is the full stacktrack if interested

Because I haven't been able to pinpoint the origin of the crash, I thought I would add a new method to NSNull in order to further debug it via logging.

However I'm not sure how to do it. I think I'd need to add a compare method to NSNull, but I have limited knowledge of objc. I got the idea from this answer. The proposed solution for a similar problem looks like this

BOOL canPerformAction(id withSender) {
    return false;
} 

- (void)viewDidLoad {
   [super viewDidLoad];

   Class class = NSClassFromString(@"UIThreadSafeNode");
   class_addMethod(class, @selector(canPerformAction:withSender:), (IMP)canPerformAction, "@@:");
}

How could I do this in Swift for adding compare to NSNull?

Upvotes: 1

Views: 793

Answers (1)

Jacob Relkin
Jacob Relkin

Reputation: 163288

You could add a compare method to NSNull like this:

Objective-C:

#import <objc/runtime.h>

static inline NSComparisonResult compareNulls(id self, SEL _cmd, NSNull *other) {
    if([other isKindOfClass:[NSNull class]]) {
        return NSOrderedSame; // Nulls are always the same.
    }

    return NSOrderedDescending;
}

@implementation NSNull (Comparisons)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        const char *encoding = [[NSString stringWithFormat:@"%s@:@", @encode(NSComparisonResult)] UTF8String];
        class_addMethod([self class], @selector(compare:), (IMP)compareNulls, encoding);
    });
}

@end

Swift:

// Add this code to your AppDelegate.swift file:
import ObjectiveC

fileprivate func compareNulls(_ self: AnyObject, _ _cmd: Selector, _ other: AnyObject) -> ComparisonResult {
    if other is NSNull {
        return .orderedSame
    }

    return .orderedDescending
}

fileprivate func addNSNullCompareImplementationIfNecessary() {
    let sel = NSSelectorFromString("compareNulls:")
    guard class_getMethodImplementation(NSNull.self, sel) == nil else {
        return
    }

    let types = "i@:@"
    class_addMethod(NSNull.self, sel, imp_implementationWithBlock(compareNulls), types)
}

// Add this line to your -didFinishLaunching: function:
addNSNullCompareImplementationIfNecessary()

This is only a temporary solution that will stop the crashes.

I would nevertheless encourage you to a) file a bug report, and b) continue investigating why this happened - clearly having an NSNull in this case wasn't expected by Parse...

Upvotes: 1

Related Questions