Gold Challenge


#1

I have a working implementation of the Gold Challenge. Changes to AssetTypePicker’s tableView:numberOfRowsInSection: and tableView:cellForRowAtIndexPath: (plus a trivial implementation of numberOfSectionsInTableView:) were pretty straight forward, once I was able to make available to tableView:cellForRowAtIndexPath: an array of the assets (BNRItems) of the selected asset type corresponding to rows in the tableView section. To make this happen I implemented a new method allItemsWithTypeLabel: on BNRItemStore. Rather than do another fetch from the data, I chose to filter the allItems array that BNRItemStore already does the work of fetching and maintaining. It wasn’t immediately obvious to me how to do this, but here’s the solution I settled on:

BNRItemStore.m:

- (NSArray *)allItemsWithTypeLabel:(NSString *)typeLabel { NSPredicate *predicate = [NSPredicate predicateWithBlock: ^BOOL(id evaluatedObject, NSDictionary *bindings) { return ([evaluatedObject assetTypeLabel] == typeLabel); }]; return [allItems filteredArrayUsingPredicate:predicate]; }

Since the assetType of a BNRItem points to an NSManagedObject, I implemented a helper method to get the type label as an NSString by refactoring code originally written to apply the label to the button in DetailViewController:

BNRItem.m:

- (NSString *)assetTypeLabel { NSString *typeLabel = [[self assetType] valueForKey:@"label"]; if (!typeLabel) { typeLabel = @"None"; } return typeLabel; }

Now, I haven’t counted, but there’s at least a couple of hoops there. I wonder if there’s a more direct way?


#2

@dmddmd I think that is a pretty direct way. I implemented a similar, although not as elegant!, way for this challenge.

My method to determine and return an array of similar asset types (your allItemsWithTypeLabel: method) is:
In BNRItemStore.m

[code]- (NSArray *)allItemsOfAssetType:(BNRItem *)item
{
// Get the assetType of the BNRItem passed as argument
NSManagedObject *assetTypeToFind = [item assetType];
// Use key-value coding to get the asset type’s label
NSString *assetLabelToFind = [assetTypeToFind valueForKey:@“label”];

// Create empty array to hold BNRItems of the same asset type
NSMutableArray *arrayOfSameAssetType = [[NSMutableArray alloc] init];

for (BNRItem *i in allItems) {
    NSManagedObject *assetType = [i assetType];
    // Use key-value coding to get the asset type's label
    NSString *assetLabel = [assetType valueForKey:@"label"];
    if (assetLabelToFind == assetLabel) {
        [arrayOfSameAssetType addObject:i];
    }
}
return arrayOfSameAssetType;

}[/code]
Although I didn’t need the assetTypeLabel method in BNRItem.m, I much prefer your code.

I’m interested to know your thought process in coming up with the far more elegant NSPredicate solution? Also not sure what the NSDictionary *bindings is? Would this predicate have to be performed in a block as it’s just talking to an array?

I initially toyed with a call to core data to return the items of similar type. Has anyone got a working solution for this?

(@joeconway maybe :blush:) What would be the preferred solution here in terms of speed, efficiency and common practice? My somehow convoluted solution, NSPredicate or a call to core data?

Thanks, Mark


#3

You should really be using Core Data to perform this fetch, it is going to be faster and it is possible that in other situations, you will not have loaded all of your BNRItems. (This application simplifies things by simply fetching allItems at once.)

Since the BNRAssetType has a to-many relationship to the BNRItems it holds, you can just ask the BNRAssetType for its “items” value. That’s just one line of code.


#4

Thank you Joe; but been pulling my hair out with this…

In core data, I’m trying to fetch all the items that have the chosen asset type:

[code] NSManagedObject *assetType = [item assetType];
// Use key-value coding to get the asset type’s label
NSString *assetLabel = [assetType valueForKey:@“label”];

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"BNRAssetType" inManagedObjectContext:context];
[request setEntity:entity];

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"items like[cd] %@", assetLabel];
[request setPredicate:predicate];

NSError *error = nil;
NSArray *result = [context executeFetchRequest:request error:&error];[/code]

I’ve tried many combinations for the line:

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"items like[cd] %@", assetLabel];
But can’t return the list of “items”. I get ‘NSInvalidArgumentException’, reason: ‘to-many key not allowed here’ for the above. If there was an attribute on the BNRItem holding the assetType I could target it with item.assetType maybe.
I know it’s just a one-liner but I can’t get my head around it!

Can anyone help me out?

Thanks in advance, Mark


#5

I finally made it!!

@BananaSkin : You don’t want an array of BNRAssetType, but an array of BNRItem, you have to change it.

Here is my solution :

[code]- (NSArray *)allItemsLike:(BNRItem *)item
{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *e = [NSEntityDescription entityForName:@“BNRItem” inManagedObjectContext:context];
[request setEntity:e];
NSPredicate *p = [NSPredicate predicateWithFormat:@“assetType == %@”, [item assetType]];
[request setPredicate:p];
NSError *error;
NSArray *result = [context executeFetchRequest:request
error:&error];
if (!result) {
[NSException raise:@"Fetch failed"
format:@“Reason : %@”, [error localizedDescription]];
}

return result;

}
[/code]

I really enjoy reading this book despite of my poor English! I really thank you, Joe Conway and Aaron!


#6

@chalusf3 thank you very much for the pointer … works like a dream :smiley:

The problem I had was that within the predicate I used the variable assetLabel derived from:

NSManagedObject *assetType = [item assetType]; // Use key-value coding to get the asset type's label NSString *assetLabel = [assetType valueForKey:@"label"]when I should have been using assetType (i.e. [item assetType]).

BTW, Joe states in his earlier reply [quote]you can just ask the BNRAssetType for its “items” value[/quote]
I’m aware this isn’t what is happening here - this seems to be the inverse - asking BNRItem for its “assetType” value.

Does it make any difference??

Thanks again, Mark


#7

Hi all,

I think what Joe was trying to say is that you don’t need to use predicates at all. You can just retrieve the “items” directly from the BNRAssetType, as an “unordered set”. Something like this:

// Ch 16: Core Data: Gold Challenge: Show all items that belong to the selected asset type
- (NSArray *)allItemsForAssetType:(NSManagedObject *)assetType
{
    if (assetType)
        return [[assetType mutableSetValueForKey:@"items"] allObjects];

    // If our asset type is "None", use a predicate to return all of the items
    // which do not have an asset type.
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"assetType == NIL"];
    return [allItems filteredArrayUsingPredicate:pred];
}

Note that the first return is the one we care about, where we use “mutableSetValueForKey”. That second return value is just me getting fancy, because I also wanted to return all of the items that didn’t have an asset type, and the only way I could do that was with a predicate.

Hope this makes sense!
-Chris


#8

Thanks Chris

That works! That’s so simple!! I need to get this clear in my head…you’ve passed the NSManagedObject as the argument whereas I’ve passed BNRItem, so I’m guessing you’ve used [item assetType] before you call the method. That bit I’m fine with, but the line:

return [[assetType mutableSetValueForKey:@"items"] allObjects]; I can’t see how that makes a core data call with no preceding ‘set-up’’? But I’ve checked and it does.

I have all of this!!

[code] NSManagedObject *assetType = [item assetType];

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"BNRItem" inManagedObjectContext:context];
[request setEntity:entity];

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"assetType == %@", assetType];
[request setPredicate:predicate];

NSError *error = nil;
NSArray *result = [context executeFetchRequest:request error:&error];[/code]

Where did you come up with the mutableSetValueForKey method? I think I’m missing something :open_mouth:

Thanks again for you patience,
Mark


#9

Hi Mark,
It was really Joe’s comment that provided the clue, where he said just ask the BNRAssetType for its “items” value.

Then, I looked at the code in AssetTypePicker…cellForRowAtIndexPath, where we get the label:

// Use key-value coding to get the asset type's label NSString *assetLabel = [assetType valueForKey:@"label"];

I didn’t understand that “valueForKey” so I looked up the docs for NSManagedObject, and stumbled upon all of the other *valueForKey methods, including the mutableSetValueForKey. I think that this is all related to the “faults” section in the Core Data chapter. I’m guessing that there is a ton of code inside that mutableSetValueForKey, which looks to see if the “items” property has been loaded for that BNRAssetType, and if it hasn’t been loaded, then it goes ahead and loads it from the SQL.

The reason we needed to use “valueForKey” and “mutableSetValueForKey” is because we don’t have a “real” BNRAssetType class. Instead, we are using NSManagedObject, which doesn’t know anything about these properties. With BNRItem, we have a real class so we can get/set the properties as if they are owned by the object, instead of coming from the SQL file.

To test this, I created a BNRAssetType class from the CoreData model (using the same procedure for creating the BNRItem class). Once I did this (and changed all of the NSManagedObject’s to BNRAssetType’s), I was able to get and set both the “label” and “items” properties directly.

Hope this all makes sense, and good luck!
-Chris


#10

Thanks for the explanation Chris.

With guidance and much prompting I can pretty much get there - thank you! I’m close to finishing this (excellent!) book and think I’ll return and redo this chapter. This is such a large and vital area to master that I really need to get the fundamentals firmly fixed so my thought process to solving problems is more natural for this topic. Replies and explanation like yours really help.

Thanks again, Mark


#11

I see that some have solved this by using the selection already done in BNRItemStore, however, I didn’t think of that first, and since I wanted to try my hand at creating a new query anyway, this being the Core Data chapter, I solved this by creating a new query within AssetTypePicker.m as follows (with extensive comments)

[code]-(NSArray *)itemsOfCurrentAssetType
{
// First of all, use the Managed Object Context configured already in BNRItemStore
// (added the context as a property in BNRItemStore.h)
NSManagedObjectContext *moc = [BNRItemStore sharedStore].context;

NSFetchRequest *selectWhichItems = [[NSFetchRequest alloc] init];

// Create the selection entity, which is the equivalent of the FROM
// part of an SQL SELECT statement (SELECT FROM tablename...)
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"BNRItem" inManagedObjectContext:moc];
selectWhichItems.entity = entityDescription;

// Create a predicate. The predicate is the equivalent of the part of an
// SQL SELECT statement which defines the criteria.
NSPredicate *selectionPredicate = [NSPredicate predicateWithFormat:
                          @"(assetType = %@)", item.assetType];

// Set the selection criteria against the selection entity, so that
// the entire select statement would look something like this, in a
// pseudo-SQL Query
//    SELECT FROM tablename=BNRItem WHERE assetType=item.assetType
selectWhichItems.predicate = selectionPredicate;
NSError *error = nil;

// Create an array for the results of the query, and then execute the query
NSArray *selectedItems = [moc executeFetchRequest:selectWhichItems error:&error];
if (selectedItems == nil)
    NSLog(@"No items for this asset type.");
return selectedItems;

}
[/code]

I used the results generated by this query in two places: firstly in the code to determine how many rows to display in the section, again this is within AssetTypePicker.m:

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section == 0) { return [[[BNRItemStore sharedStore] allAssetTypes] count]; } else { // if (section == 1), i.e. the section with the list of items of the given Asset Type... return self.itemsOfCurrentAssetType.count; } }
And the second place was in the code which describes what actually goes into the cells of the table, i.e. the tableView:cellForRowAtIndexPath: method. The first part of this method is unchanged, except that it’s wrapped in a “if (ip.section == 0) {}” clause since I want to display the Asset Types in the first part of the table; whereas when the ip.section is not equal to zero, ie. in the secon section of the table, is where I want to display just the items of the given Asset Type. So, again within AssetTypePicker.m:

[code]-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)ip
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@“UITableViewCell”];
}
NSArray *allAssets = [[BNRItemStore sharedStore] allAssetTypes];
NSManagedObject *assetType = [allAssets objectAtIndex:[ip row]];

if (ip.section == 0) {// Use key-value coding to get the asset type's label
    NSString *assetLabel = [assetType valueForKey:@"label"];
    cell.textLabel.text = assetLabel;

    // Checkmark the one that is currently selected
    if (assetType == [item assetType]) {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
    } else {
        cell.accessoryType = UITableViewCellAccessoryNone;
    }
} else { // i.e. for the second section that we have created
    BNRItem *i = [self.itemsOfCurrentAssetType objectAtIndex:ip.row];
    cell.textLabel.text = [NSString stringWithFormat:@"%@",i.itemName];
}
return cell;

}
[/code]
I found the three challenges of this chapter to all be interesting, and very helpful in my learning.

I have also found that this Forum has been exceedingly useful in helping my understanding… so thanks to the authors, and moderators… but also thanks to the rest of you participants on this Forum for contributing too. :slight_smile:


#12

Thanks all of you as well as my hard work. I am almost crazy with happiness. Everything works! All three challenges…


#13

Here is my solution:

Add a item array instance variable to .h:

@interface AssetTypePicker : UITableViewController { NSArray *items; }

Implement accessor for Item setter - use key-value coding to retrieve all items for the pickers item.assetType and add to the items array in .h:

[code]-(void)setItem:(BNRItem *)item
{
if (_item == item)
return;

_item = item;

NSManagedObject *assetType = [_item assetType];
   
items = [[assetType valueForKeyPath:@"items"] allObjects];

}[/code]

Trivial sections implementation:

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; }

Send the …count] message to the items instance variable to initialise the new sections’ no. of rows:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger *rows; switch (section) { case 0: rows = [[[BNRItemStore sharedStore] allAssetTypes] count]; break; case 1: rows = [items count]; break; } return rows; }

Finally, in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath, use the switch statement to check which section is being populated with cells, get the BNRItem for the current row in the new section:

[code]BNRItem *p;

switch ([indexPath section]) {
    case 0:
        [[cell textLabel] setText:assetLabel];
        
        // Checkmark the one that is currently selected
        if (assetType == [_item assetType]) {
            [cell setAccessoryType:UITableViewCellAccessoryCheckmark];
        } else {
            [cell setAccessoryType:UITableViewCellAccessoryNone];
        }
        break;
    case 1:
        
        p = [items objectAtIndex:[indexPath row]];
        
        [[cell textLabel] setText:p.itemName];
        break;
}

[/code]

That’s all there is to it, key-value coding!


#14

Hi,

I have a quick question about your code, what’s this underscore coding you do? i.e.

[quote] -(void)setItem:(BNRItem *)item
{
if (_item == item)
return;

_item = item;

NSManagedObject *assetType = [_item assetType];
   
items = [[assetType valueForKeyPath:@"items"] allObjects];

}
[/quote]

I haven’t seen it before. When I typed this into xcode it doesn’t like it.

Thanks in advance!


#15

Hi Tiberius,

I used a property for the ‘item’ instead of an instance variable like this in AssetTypePicker.h:

[code]#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import “HomepwnerViewController.h”
#import “AssetTypeViewController.h”

@class BNRItem;
@protocol AssetTypePickerDelegate;

@interface AssetTypePicker : UITableViewController
{
NSArray *items;
}

@property (nonatomic, strong) BNRItem *item;
@property (weak) id delegate;

@end

@protocol AssetTypePickerDelegate

@optional

  • (void)didChangeSelection:(NSObject*)id;

@end[/code]

Hence, to access the instance variable that backs the property, I use the underscore syntax.

Hope this helps.

Shaz


#16

Thanks for the reply Shaz. Our AssetTypePicker.h looks similar (with the exception of your @protocal additions).

When I try to type in your code as is, the xcode yells at me (is there some setting I need to turn on?). Hence I was hoping to learn about how this _item works. I haven’t seen anything in either BNR books I’ve read thus far about this type of style. I’ve come across it on stackoverflow.com and now here in the forums.

I’m mainly curious and want to learn about it.

Since I couldn’t get the complied to accept _item, I just modified void to be:

-(void)setItem:(BNRItem *)i
{
  if (item = i)
      return;
  item = i;

  NSManagedObject *assetType = [item assetType];

  items = [[assetType valueForKeyPath:@"items"] allObjects];

Thanks in advance for any further clarification!


#17

Hi Tiberius,

Did you get any further with this?

Are you synthesizing the property in .m? Try omitting synthesizing it if you are.

Hope this helps.

Shaz


#18

Thanks for that! When you don’t synthesize the property the underscore nomenclature works. Thanks I learned something new today. Now some of the other code I’ve been reading makes a little more sense!

Is this more a stylistic type approach or are there benefits not to synthesize variables?


#19

Hi Tiberius,

When you synthesize a property, you don’t explicitly declare a getter or setter for it. Since I had to modify the default implementation of the setter, synthesizing the property was not required and would raise a warning.

Its not really a stylistic approach either. Theoretically, we only use the underscore notation to reference a property’s instance variable when we need to instantiate it, typically in the designated initialiser or in my example, the explicit setter. We should use the following syntax when we want to get the value of the property: self.propName

Hope this helps.

Shaz


#20

dmddmd: What is “assetTypeLabel”?

I get an error there.