78

Given an NSArray of NSDictionary objects (containing similar objects and keys) is it possible to write perform a map to an array of specified key? For example, in Ruby it can be done with:

array.map(&:name)
Stussa
  • 3,375
  • 3
  • 24
  • 35
  • 1
    What return value are you hoping to get from the call? – jaminguy May 25 '11 at 16:35
  • Ruby's map is close to the typical functional-programming idea of map. Given a collection, transform each object in the collection, and return the resulting collection. – James Moore Oct 25 '12 at 23:21
  • Incidentally, I've written an Objective-C library that provides Ruby's `map`/`collect` method for `NSArray`: https://github.com/mdippery/collections – mipadi May 07 '13 at 18:15

11 Answers11

136

It only saves a couple lines, but I use a category on NSArray. You need to ensure your block never returns nil, but other than that it's a time saver for cases where -[NSArray valueForKey:] won't work.

@interface NSArray (Map)

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block;

@end

@implementation NSArray (Map)

- (NSArray *)mapObjectsUsingBlock:(id (^)(id obj, NSUInteger idx))block {
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:[self count]];
    [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [result addObject:block(obj, idx)];
    }];
    return result;
}

@end

Usage is much like -[NSArray enumerateObjectsWithBlock:]:

NSArray *people = @[
                     @{ @"name": @"Bob", @"city": @"Boston" },
                     @{ @"name": @"Rob", @"city": @"Cambridge" },
                     @{ @"name": @"Robert", @"city": @"Somerville" }
                  ];
// per the original question
NSArray *names = [people mapObjectsUsingBlock:^(id obj, NSUInteger idx) {
    return obj[@"name"];
}];
// (Bob, Rob, Robert)

// you can do just about anything in a block
NSArray *fancyNames = [people mapObjectsUsingBlock:^(id obj, NSUInteger idx) {
    return [NSString stringWithFormat:@"%@ of %@", obj[@"name"], obj[@"city"]];
}];
// (Bob of Boston, Rob of Cambridge, Robert of Somerville)
Justin Anderson
  • 2,100
  • 3
  • 23
  • 22
  • Don't forget to check for nil on the block you are passing in to `mapObjectsUsingBlock`. It will currently crash if you pass in a nil block. – DHamrick May 15 '12 at 04:07
  • 7
    http://www.mikeash.com/pyblog/friday-qa-2009-08-14-practical-blocks.html contains a better (syntactic) variant of the above, IMHO. – Manav Sep 10 '12 at 12:58
  • @Justin Anderson can you please include in your answer a sample usage of the above? – abbood May 07 '13 at 08:04
  • 2
    You're returning a `MutableArray` back. Is it better practice to do `return [result copy]`? – Mr Rogers Aug 02 '13 at 01:18
  • @MrRogers `result copy` would potentially reduce memory usage, but there's a time cost to copying the array. For large arrays that could be pretty expensive. The other benefit of making the response truly immutable is that you would get runtime exceptions if you tried to mutate it, but the return type of the method is immutable, so you'll get build warnings either way if you try to treat the response as mutable. – Justin Anderson Aug 06 '13 at 13:58
  • @JustinAnderson `copy` returns an immutable array. Isn't that what one would expect if that's the return type of the method? There is a speed hit though. Thanks! – Mr Rogers Aug 08 '13 at 15:44
  • @MrRogers There's no expectation or implication about the actual mutability of the returned object, just that you should treat it as though it is immutable. A return type of `NSArray` is just a promise that a response will be an `NSArray` or an instance of a subclass of it. It's like how you need to treat an `id` response as an `NSObject` until you've tested it with `respondsToSelector` and the like before you can expect to treat it as something more specific. – Justin Anderson Aug 08 '13 at 20:26
  • follow up question [here](http://stackoverflow.com/questions/18360559/how-to-skip-objects-while-iterating-in-a-ruby-like-map-method-for-obj-c/18361973?noredirect=1#18361973): what to do when I would like to skip an object in the internal enumerator – abbood Aug 21 '13 at 16:00
  • 1
    @abbood If you're looking to keep a 1-1 mapping of the indexes from your original array to your mapped array, I'd suggest returning `NSNull`. That's the Obj-C object for representing nil in collection classes. `NSNull` is pretty uncommon and a hassle to use though, since Obj-C doesn't do unboxing, so `NSNull != nil`. But if you want to just filter some items out of the array, you could modify `mapObjectsUsingBlock` to check for nil responses from the block and skip them. – Justin Anderson Aug 28 '13 at 18:27
  • Use `__block` on variables that declared outside the block but need to be accessed inside the block. – uchuugaka Jul 24 '19 at 02:10
  • @uchuugaka __block doesn't apply here. There are no outside variables being captured and no state to persist beyond the block's return value. – Justin Anderson Jul 24 '19 at 12:25
80

I've no idea what that bit of Ruby does but I think you are looking for NSArray's implementation of -valueForKey:. This sends -valueForKey: to every element of the array and returns an array of the results. If the elements in the receiving array are NSDictionaries, -valueForKey: is nearly the same as -objectForKey:. It will work as long as the key doesn't start with an @

JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • It does exactly that! Thanks so much! – Stussa May 25 '11 at 16:52
  • This isn't what map does. Ruby's map takes a block, not just a single method call. This particular case only works because the block is a single call to a single method. Answer from @JustinAnderson is much closer to what you can do in Ruby. – James Moore Oct 25 '12 at 23:24
  • 2
    Actually, this solution is far more flexible than you might suppose, because `valueForKey:` works by calling the corresponding getter. Thus, if you know in advance the various things you might want an object to do, you can inject the needed getters into it (e.g. with a category) and then use NSArray's `valueForKey:` as a way of passing a call to a particular getter through the array to each object and getting an array of the results. – matt Nov 17 '12 at 17:02
  • 3
    +1; this is the cleanest way to handle the particular (fairly common) use case that the question asker described, even if it's not a fully flexible map function as asked for in the question title. For people who genuinely need a `map` method, use the category from Justin Anderson's answer instead. – Mark Amery Jul 29 '13 at 10:38
  • 1
    I keep coming back to this answer to remember that name. I'm so used to "map" in other programming languages that I never can remember "valueForKey" – tothemario Mar 30 '15 at 22:07
43

To summarize all other answers:

Ruby (as in the question):

array.map{|o| o.name}

Obj-C (with valueForKey):

[array valueForKey:@"name"];

Obj-C (with valueForKeyPath, see KVC Collection Operators):

[array valueForKeyPath:@"[collect].name"];

Obj-C (with enumerateObjectsUsingBlock):

NSMutableArray *newArray = [NSMutableArray array];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
     [newArray addObject:[obj name]];
}];

Swift (with map, see closures)

array.map { $0.name }

And, there are a couple of libraries that allow you to handle arrays in a more functional way. CocoaPods is recommended to install other libraries.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
tothemario
  • 5,851
  • 3
  • 44
  • 39
19

Update: If you're using Swift, see map.


BlocksKit is an option:

NSArray *new = [stringArray bk_map:^id(NSString *obj) { 
    return [obj stringByAppendingString:@".png"]; 
}];

Underscore is another option. There is a map function, here is an example from the website:

NSArray *tweets = Underscore.array(results)
    // Let's make sure that we only operate on NSDictionaries, you never
    // know with these APIs ;-)
    .filter(Underscore.isDictionary)
    // Remove all tweets that are in English
    .reject(^BOOL (NSDictionary *tweet) {
        return [tweet[@"iso_language_code"] isEqualToString:@"en"];
    })
    // Create a simple string representation for every tweet
    .map(^NSString *(NSDictionary *tweet) {
        NSString *name = tweet[@"from_user_name"];
        NSString *text = tweet[@"text"];

        return [NSString stringWithFormat:@"%@: %@", name, text];
    })
    .unwrap;
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 9
    IMHO, using dot syntax for Objective-C methods is not a good idea. This is not Ruby. – Rudolf Adamkovič Mar 20 '14 at 11:06
  • 1
    @RudolfAdamkovic While I do agree with you in principle, doing this with bracket notation would be less readable. – 11684 Jun 12 '14 at 20:37
  • 9
    Unless you're doing functional operations all over the place, it's a much better idea to use the NSArray's standard mapObjectsUsingBlock: or valueForKey: methods (suggested in the answers below). Avoiding a few more characters of "ugly" Objective-C syntax doesn't justify adding a Cocoapods dependency. – Gregarious Sep 17 '14 at 15:40
  • 3
    `mapObjectsUsingBlock:` isn't a standard function, but an extension suggested by another answer – mcfedr Mar 16 '16 at 14:56
  • Agreed with @Gregarious, we should never go against coding standards just to make it "prettier" – J2N May 04 '16 at 14:55
  • 1
    Page linked from "Underscore" in answer seems not related to Objective-C any more. – Pang May 31 '18 at 05:12
8

I think valueForKeyPath is a good choice.

Sit below has very cool examples. Hopes it is helpful.

http://kickingbear.com/blog/archives/9

Some example:

NSArray *names = [allEmployees valueForKeyPath: @"[collect].{daysOff<10}.name"];
NSArray *albumCovers = [records valueForKeyPath:@"[collect].{artist like 'Bon Iver'}.<NSUnarchiveFromDataTransformerName>.albumCoverImageData"];
Steven Jiang
  • 1,006
  • 9
  • 21
4

I'm no Ruby expert so I'm not 100% confident I'm answering correctly, but based on the interpretation that 'map' does something to everything in the array and produces a new array with the results, I think what you probably want is something like:

NSMutableArray *replacementArray = [NSMutableArray array];

[existingArray enumerateObjectsUsingBlock:
    ^(NSDictionary *dictionary, NSUInteger idx, BOOL *stop)
    {
         NewObjectType *newObject = [something created from 'dictionary' somehow];
         [replacementArray addObject:newObject];
    }
];

So you're using the new support for 'blocks' (which are closures in more general parlance) in OS X 10.6/iOS 4.0 to perform the stuff in the block on everything in the array. You're choosing to do some operation and then add the result to a separate array.

If you're looking to support 10.5 or iOS 3.x, you probably want to look into putting the relevant code into the object and using makeObjectsPerformSelector: or, at worst, doing a manual iteration of the array using for(NSDictionary *dictionary in existingArray).

Tommy
  • 99,986
  • 12
  • 185
  • 204
2
@implementation NSArray (BlockRockinBeats)

- (NSArray*)mappedWithBlock:(id (^)(id obj, NSUInteger idx))block {
    NSMutableArray* result = [NSMutableArray arrayWithCapacity:self.count];
    [self enumerateObjectsUsingBlock:^(id currentObject, NSUInteger index, BOOL *stop) {
        id mappedCurrentObject = block(currentObject, index);
        if (mappedCurrentObject)
        {
            [result addObject:mappedCurrentObject];
        }
    }];
    return result;
}

@end


A slight improvement upon a couple of the answers posted.

  1. Checks for nil—you can use nil to remove objects as you're mapping
  2. Method name better reflects that the method doesn't modify the array it's called on
  3. This is more a style thing but I've IMO improved the argument names of the block
  4. Dot syntax for count
james_womack
  • 10,028
  • 6
  • 55
  • 74
0

For Objective-C, I would add the ObjectiveSugar library to this list of answers: https://github.com/supermarin/ObjectiveSugar

Plus, its tagline is "ObjectiveC additions for humans. Ruby style." which should suit OP well ;-)

My most common use-case is mapping an dictionary returned by a server call to an array of simpler objects e.g. getting an NSArray of NSString IDs from your NSDictionary posts:

NSArray *postIds = [results map:^NSString*(NSDictionary* post) {
                       return [post objectForKey:@"post_id"];
                   }];
LordParsley
  • 3,808
  • 2
  • 30
  • 41
0

For Objective-C, I would add the Higher-Order-Functions to this list of answers: https://github.com/fanpyi/Higher-Order-Functions;

There is a JSON array studentJSONList like this:

[
    {"number":"100366","name":"Alice","age":14,"score":80,"gender":"female"},
    {"number":"100368","name":"Scarlett","age":15,"score":90,"gender":"female"},
    {"number":"100370","name":"Morgan","age":16,"score":69.5,"gender":"male"},
    {"number":"100359","name":"Taylor","age":14,"score":86,"gender":"female"},
    {"number":"100381","name":"John","age":17,"score":72,"gender":"male"}
]
//studentJSONList map to NSArray<Student *>
NSArray *students = [studentJSONList map:^id(id obj) {
return [[Student alloc]initWithDictionary:obj];
}];

// use reduce to get average score
NSNumber *sum = [students reduce:@0 combine:^id(id accumulator, id item) {
Student *std = (Student *)item;
return @([accumulator floatValue] + std.score);
}];
float averageScore = sum.floatValue/students.count;

// use filter to find all student of score greater than 70
NSArray *greaterthan = [students filter:^BOOL(id obj) {
Student *std = (Student *)obj;
return std.score > 70;
}];

//use contains check students whether contain the student named 'Alice'
BOOL contains = [students contains:^BOOL(id obj) {
Student *std = (Student *)obj;
return [std.name isEqual:@"Alice"];
}];
Bugs
  • 4,491
  • 9
  • 32
  • 41
范陆离
  • 91
  • 1
  • 3
0

There is a special key-path operator for this: @unionOfObjects. Probably it replaced [collect] from previous versions.

Imagine a Transaction class with payee property:

NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];

Apple docs on Array Operators in Key-Value coding.

kelin
  • 11,323
  • 6
  • 67
  • 104
-2

Swift introduces a new map function.

Here is an example from the documentation:

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

let strings = numbers.map {
    (var number) -> String in
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

The map function takes a closure which returns a value of any type and maps the existing values in the array to instances of this new type.

Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 5
    You could use a simpler example, like `[1,2,3].map {$0 * 2} //=> [2,4,6]`. AND, the question is about Obj-C NSArray, not Swift ;) – tothemario Mar 30 '15 at 22:04