In the first part of this article I discussed the basics of NSSet: why you'd want to use one and how you can simply access the contents of an NSSet.
As I wrote at the time, my greatest use of NSSet thus far has come when using Core Data. That's because you get an NSSet whenever you access a one-to-many relationship in Core Data. In this article, I'm going to talk about some of the many ways to use NSSets in that context.
All of my examples will center around the data model that I laid out way back in Core Data, Part Two: The Data Model. You might want to look at the graphics there for clarification, but the basic idea that I'll focus on here is that I have a "CardStack" entitity which includes a one-to-many relationship with a "Card" entity called "cards". In other words, it's an entity that represents a deck of cards.
The Simple Core Data Stuff
The simplest thing you can do with Core Data is directly grab an NSSet by looking up a one-to-many relationship. If you want, you can then turn it into an array with the allObjects method call that I mentioned in the last article:NSArray *theseCards = [fromDeck.cards allObjects];You can similarly set one-to-many relationships using NSSet, as I did when I set up my cards initially:
This example shows the creation of a one-to-many-relationship with a single element, but it's easy enough to use other NSSet messages such as setWithArray: and setWithObjects: to instead add multiple objects to your relationship.[card setImages:[NSSet setWithObject:
[self setupCardPic:(CardPics *)myCardPic]]];
Note that this methodology is also what I was highlighting in Core Data Part Six, There's More Than One Way to Do It. Accessing an NSSet often represents a cleaner, more intuitive way to get to your data. However, it's not just restricted to the simple NSSet lookups described here.
What follows are some more complex NSSet lookups that really highlight the power of the class--and its powerful integration with Core Data. Each of the examples that follows is part of a method directly built into my CardStack class. Thus you'll see references to things like "self.cards" which is just a way to access the cards relationships from inside a CardStack object.
Enumerating an NSSet
Thus far I've talked about how to access an NSSet mainly by turning it into a different sort of object (an NSArray). That's by no means necessary. You can walk through the contents of an NSSet with the help of an NSEnumerator:
-(NSNumber *)worthOfType:(NSString *)thisType {
NSSet *theseCards = [self cardsOfType:thisType];
NSEnumerator *thisEnum = [theseCards objectEnumerator];
Card *thisCard; float totalWorth = 0;
while (thisCard = [thisEnum nextObject]) {
totalWorth += [thisCard.worth floatValue];
}
return [NSNumber numberWithFloat:totalWorth];
}
This examples figures out the total worth of the cards of a specific type. Once you create an NSEnumerator object with the ObjectEnumerator NSSet message, you can step through it by continuously sending the NSEnumerator a nextObject message. I'll offer one warning that's also noted in the Apple docs: don't change an NSMutableSet as you're enumerating it. I've accidentally done that more than once, and sure enough, it doesn't work.
Mind you, there's an alternative way to do this same thing with key paths, but stepping through an NSSet with an NSEnumerator will probably have many uses for you.
Predicates & NSSets
The cardsOfType: message call in the previous example shows another way that you can use an NSSet, by filtering it with an NSPredicate.Here's a complete example, with cardsOfType: actually sending a more generic cardsWithKey:Value: message:
As you can see, a simple three-step process lets you reduce your NSSet to a subset based on a predicate.-(NSSet *)cardsOfType:(NSString *)type {
return [self cardsWithKey:@"type" Value:type];
}
-(NSSet *)cardsWithKey:(NSString *)thisKey Value:(id)thisValue {
NSSet *theseCards = self.cards;
NSPredicate *predicate =
[NSPredicate predicateWithFormat:@"%K=%@", thisKey,thisValue];
NSSet *filteredCards = [theseCards filteredSetUsingPredicate:predicate];
return filteredCards;
}
First, you create an initial NSSet, here with self.cards.
Second, you create a predicate. This example uses the slightly more complex %K=%@ invocation, which allows me to set the key for my predicate dynamically. What I'm really doing in this example is 'type=%@,thisValue'.
Third, you use the NSSet's filteredSetUsingPredicate: message to turn your original NSSet and your NSPredicate in a smaller NSSet subset.
With this example in hand you can now see how it integrates with the previous example, which let me get the worth of just a subset of all my cards.
Key Paths & NSSets
iPhone in Action doesn't discuss key-value coding, but it's a handy way to quickly apply operators to an entire collection of objects. This means that for NSArrays and NSSets you can compute averages, sums, maxes, minimums, counts, and lots more. You should take a look at Apple's Key-Value Coding Guide for more information.The earlier NSEnumerator example could have been done with key-value coding and a @sum operator (but I left it as was so that I could show off NSEnumerator). Here's a very similar example that instead uses a key-value path:
As you can see, you create a key path as an NSString. It must have an operator, which begins with an @ symbol, followed by the property that the operator is working on. Thus, here, we're summing the value of the worth property. You then can apply that to an NSSet with a valueForKeyPath: message. That's surely easier than digging through the entire NSSet as I did the first time through.-(NSNumber *)selectedWorth {
NSSet *filteredCards = [self cardsSelected];
NSString *keyPath = [NSString stringWithFormat:@"@sum.worth"];
NSNumber *worthSum = [filteredCards valueForKeyPath:keyPath];
return worthSum;
}
This example also shows how you can stack up these different NSSet methods, as my original 'filteredSet' was created with an NSPredicate operation (in the cardsSelected method, not shown).
A more complex keyPath might look something like this:
cards.@sum.worthThis shows a different way to invoke a key path. You're putting a property name before the @operator, to dig down through a hierarchy of properties. In this example, the key path wll be operating directly on the self object (the CardStack). It digs down to the cards property, and that's the set that it actually operates on. There it again looks for the @sum of the worth (thus showing a third way to calculate worth using NSSets).
