This is my solution. Basically, the algorithm traverses the string looking for substring matches and returns those matches in an array.
Since an NSRange is a struct it cannot be added to the array directly. By using NSValue, I can encode the match first and then add it to the array. To retrieve the range, I then decode the NSValue object to an NSRange.
#import <Foundation/Foundation.h>
NSRange makeRangeFromIndex(NSUInteger index, NSUInteger length) {
    return NSMakeRange(index, length - index);
}
NSArray<NSValue *> * allLocationsOfStringMatchingSubstring(NSString *text, NSString *pattern) {
    NSMutableArray *matchingRanges = [NSMutableArray new];
    NSUInteger textLength = text.length;
    NSRange match = makeRangeFromIndex(0, textLength);
    while(match.location != NSNotFound) {
        match = [text rangeOfString:pattern options:0L range:match];
        if (match.location != NSNotFound) {
            NSValue *value = [NSValue value:&match withObjCType:@encode(NSRange)];
            [matchingRanges addObject:value];
            match = makeRangeFromIndex(match.location + 1, textLength);
        }
    }
    return [matchingRanges copy];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *text = @"TATACCATGGGCCATCATCATCATCATCATCATCATCATCATCACAG";
        NSString *pattern = @"CAT";
        NSArray<NSValue *> *matches = allLocationsOfStringMatchingSubstring(text, pattern);
        NSLog(@"Text: %@", text);
        NSLog(@"Pattern: %@", pattern);
        NSLog(@"Number of matches found: %li", matches.count);
        [matches enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop) {
            NSRange match;
            [obj getValue:&match];
            NSLog(@"   Match found at index: %li", match.location);
        }];
    }
    return 0;
}