Search code examples
objective-cswagger-codegen

NSNumber array containsObject fails as types are different but they should be the same


I'm having troubles checking if an array contains an object. The @property(nonatomic) NSArray<NSNumber*>* apiArray; array is returned as a parameter from an API model while the object of which I want to check the presence in the array is returned from another API model @property(nonatomic) NSNumber* localObject;.

When I check for it [apiArray containsObject: localObject] it outputs 0 even though the value is there.. So I checked the types and on localObject the type is effectively an NSNumber, while If i'm not mistaking the array results being an NSString array as by the prints below.

            NSLog(@"$$$$$$$$$$$ [apiArray containsObject: localObject]: %d", [apiArray containsObject: localObject] );
            NSLog(@"apiArray types are: %@", [apiArray valueForKey:@"class"]);
            NSLog(@"localObject type is: %@", [localObject valueForKey:@"class"]);
            NSLog(@"apiArray is: %@", apiArray);
            NSLog(@"localObject is: %@", localObject);



$$$$$$$$$$$ [apiArray containsObject: localObject]: 0
apiArray types are: (
    "__NSCFString"
)
localObject type is: __NSCFNumber
apiArray is: (
    5717271485874176
)
localObject is: 5717271485874176
API NSNumber array types are: (
    "__NSCFString"
)

The models are generated via swagger-codegen.jar but as the parameters are shown they both seem correct.

As a test I did create an array myself and indeed the types are both NSNumber and containsObject actually works as expected

NSMutableArray<NSNumber*>* apiArray = [NSMutableArray<NSNumber*> array];
            NSNumber *localObject = [NSNumber numberWithInt: -1];
            [apiArray addObject: localObject];

            NSLog(@"$$$$$$$$$$$ [apiArray containsObject: localObject]: %d", [apiArray containsObject: localObject] );
            NSLog(@"apiArray types are: %@", [apiArray valueForKey:@"class"]);
            NSLog(@"localObject type is: %@", [localObject valueForKey:@"class"]);
            NSLog(@"apiArray is: %@", apiArray);
            NSLog(@"localObject is: %@", localObject);

$$$$$$$$$$$ [apiArray containsObject: localObject]: 1
apiArray types are: (
    "__NSCFNumber"
)
localObject type is: __NSCFNumber
apiArray is: (
    "-1"
)
localObject is: -1
API NSNumber array types are: (
    "__NSCFNumber"
)

Can you spot what the problem might be?


Solution

  • In Objective-C, it's not because it's declared as such NSArray<NSNumber*>* apiArray that apiArray will contains only NSNumber:

    NSArray<NSNumber*>* apiArray;
    NSArray *t = @[@[], @"", @2];
    apiArray = t;`
    

    This will compile just fine, and the elements of apiArray are an array, a string and a number. It's a common cause of unrecognized selector sent to instance crash error.

    NSArray<NSNumber*>* apiArray is just a small helper: It tells you that apiArray SHOULD only have NSNumber elements (even if it's not the case), so when you populate it, populate it accordingly, and when retrieving elements from it, it would expect them to be NSNumber. So NSArray *first = [apiArray firstObject]; will produce a warning Incompatible pointer types initializing 'NSArray *' with an expression of type 'NSNumber * _Nullable', but that's it, just a warning. And in my case, the first element is really a NSArray, not a NSNumber.

    After a few feedbacks on your end, it seems that the back-end send NSString instead of NSNumber. It's up to you to change it in your JSON parsing or not, or change your local model for localObject be a NSString.

    If you want to keep both API/local as such, since you have NSString and NSNumber to compare, a possible way is to use integerValue.

    It's a little more verbose, but this should do the trick:

    NSInteger index = [self indexOfObjectPassingTest:^BOOL(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        return [obj integerValue] == [localObject integerValue];
    }];
    BOOL contains = index != NSNotFound;
    

    If you want to create a contains(where:) (in a more "Swift" code) you can create a category on NSArray:

    NSArray+Custom.h

    @interface NSArray<ObjectType> (Addon)
    -(BOOL)containsWhere:(BOOL (^)(id obj))predicate;
    @end
    

    NSArray+Custom.m

    @implementation NSArray (Addon)
    
    -(BOOL)containsWhere:(BOOL (^)(id obj))predicate
    {
        NSInteger index = [self indexOfObjectPassingTest:^BOOL(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            return predicate(obj);
        }];
        return index != NSNotFound;
    }
    @end
    

    Then, in use:

    NSNumber *localObject = @2;
    BOOL contains = [array containsWhere:^BOOL(id  _Nonnull obj) {
        if ([obj respondsToSelector:@selector(integerValue)]) {
            return [obj integerValue] == [localObject integerValue];
        } else {
            NSLog(@"Element %@ from array to analyze doesn't respond to integerValue -> Skipped", obj);
            return FALSE;
        }
    }];
    NSLog(@"%@ contains localObject: %d", array, localObject, contains);