Bronze Challenge (solutions)


#1

I didn’t see that anyone put up their solutions to the Bronze Challenge on page 551, so I thought I’d start this thread. Here’s mine.

In RSSChannel.m I first defined a constant:

I then created a new method:

- (void)pruneOldEntriesFromItems
{
    if ([items count] > MAX_CACHE_SIZE) {
        
        [items sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
            return [[obj2 publicationDate] compare:[obj1 publicationDate]];
        }];
        
        NSRange excessItems = NSMakeRange(MAX_CACHE_SIZE, [items count] - MAX_CACHE_SIZE);
        NSIndexSet *indices = [NSIndexSet indexSetWithIndexesInRange:excessItems];

        [items removeObjectsAtIndexes:indices];
    }
}

I then called that method when we write to the file system:

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [self pruneOldEntriesFromItems];

    [aCoder encodeObject:items forKey:@"items"];
    [aCoder encodeObject:title forKey:@"title"];
    [aCoder encodeObject:infoString forKey:@"infoString"];
}

I reset MAX_CACHE_SIZE to 5 to test it, and it seemed to work. Anyone do it differently?


#2

Sorry! There is a big bug in my code that I am trying to get to the bottom of. My Bronze Challenge solution is not working!


#3

Okay, I think I’ve got it now. My bug had to do with the way the app’s logic executes: specifically, this particular snippet from BNRFeedStore’s fetchRSSFeedWithCompletion method:

if (!err) { // Note how we preserve state by enclosing cachedChannel! [channelCopy addItemsFromChannel:obj]; [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath]; }

The above snippet comes from where we pass a block to BNRConnection’s setCompletionBlock method. In that snippet, my pruneOldEntriesFromItems method was going to (eventually) get called, which means that the channel’s items were getting pruned. But take a look at the block in full:

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
        // This is the store's callback code
        if (!err) {
            // Note how we preserve state by enclosing cachedChannel!
            [channelCopy addItemsFromChannel:obj];
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        }
        // This is the controller's callback code (which was passed to the method)
        block(channelCopy, nil);
}];

After pruning channelCopy’s list of items, I was then passing channelCopy to the controller! The items list at that point is supposed to be a merged version of the cache and any new items. Instead, with the bug, it was a pruned version.

I fixed the bug by copying the items list in pruneOldEntriesFromItems…

[code]- (NSMutableArray *)pruneOldEntriesFromItems
{
if ([items count] > MAX_CACHE_SIZE) {

    [items sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        return [[obj2 publicationDate] compare:[obj1 publicationDate]];
    }];
    
    NSMutableArray *array = [items mutableCopy];
    
    NSRange range = NSMakeRange(MAX_CACHE_SIZE, [array count] - MAX_CACHE_SIZE);
    NSIndexSet *indices = [NSIndexSet indexSetWithIndexesInRange:range];
    [array removeObjectsAtIndexes:indices];
    
    return array;
}
else {
    return items;
}

}[/code]

…and then writing that copy to the disk.

[code]- (void)encodeWithCoder:(NSCoder *)aCoder
{
NSMutableArray *array = [self pruneOldEntriesFromItems];

[aCoder encodeObject:array forKey:@"items"];
[aCoder encodeObject:title forKey:@"title"];
[aCoder encodeObject:infoString forKey:@"infoString"];

}[/code]

I believe it’s now fixed. Again, anybody do something different?


#4

hmmm… I did something much simpler…

in addItemsFromChannel: I added trimming code:

[code]-(void)addItemsFromChannel:(RSSChannel*)otherChannel {
for(RSSItem *item in [otherChannel items]) {
if(![[self items] containsObject:item]) {
[[self items] addObject:item];
}
}

[[self items] sortUsingComparator:^NSComparisonResult(id obj1, id obj2) {
	return [[obj2 publicationDate] compare:[obj1 publicationDate]];
}];

// This is the trimming bit...
while([[self items] count] > 100) {
	[[self items] removeLastObject];
}

}[/code]


#5

Hi, I commented out my code and inserted yours in my project. I hope I put your code in correctly. Assuming I did, I’m not sure your solution solves the problem being posed by the challenge. I’m hoping you could double check and let me know.

The challenge is to limit the cache size to 100. I think right now, as I write this, there are something like 5 posts in the feed; so the easiest way to test this is to limit the cache size to something really small, like 2. Then, put in a logging statement that displays how many objects are in the “items” array right before you write them to disk.

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    // How many objects are in the "items" array?
    NSLog(@"## %@ %@", self, NSStringFromSelector(_cmd));
    NSLog(@"## items count: %d", [items count]);
    // Write it to disk
    [aCoder encodeObject:items forKey:@"items"];
    [aCoder encodeObject:title forKey:@"title"];
    [aCoder encodeObject:infoString forKey:@"infoString"];
}

You might also want to put in another logging statement when you unarchive the cache. (I left that out here.)

When I tested this, there were 5 items in the feed when I’m ready to cache it. I set the cache size to 2, so only 2 posts should be cached. Unless I misunderstood something, or made some other mistake, what I found was that all 5 were being cached.

What do you see?


#6

Hey, yes, I misread the challenge. I was going for limiting the channel to 100 altogether. But if its just the cache, then it should be even easier. Just before the single point where the store caches the channel in fetchRSSFeedWithCompletion: we need to trim the items…

[code] if(!cachedChannel) {
cachedChannel = [[RSSChannel alloc] init];
}

RSSChannel *channelCopy = [cachedChannel copy];

[connection setCompletionBlock:^(id obj, NSError *err) {	
	if(!err) {
		[channelCopy addItemsFromChannel:obj];
		
		// Trim the cache just before saving...
		while ([[channelCopy items] count] > 100) {
			[[channelCopy items] removeLastObject];
		}
		
		[NSKeyedArchiver archiveRootObject:channelCopy toFile:cachedPath];
	}
	
	block(channelCopy, err);
}];[/code]

#7

Change 100 to 2, like I did. That’s the kind of thing I always do when I’m testing. (You may even want to change it to 1.) I know the challenge says that we’re limiting it to 100, but a value that high masks what’s really going on. We don’t really see if our logic is correct unless we set the cache size to a size smaller than the number of posts in the day’s feed.

When I take your code, at this point, and set the cache size limit to 2, only 2 items will make it into the app’s GUI. I would say that’s a bug. See if you can confirm that. To me, the challenge is a little tricky, which is why I eventually coded up the solution the way I did and didn’t get it right the first time.


#8

I see what you are saying… I have been testing with 3 as the max… I guess it’s a matter of interpretation of the requirement. If we want an unlimited number of items on the UI and only 100 on the cache, then the trimming should be moved to the RSSChannel’s encodeWithCoder: method… like so:

[code]-(void)encodeWithCoder:(NSCoder*)coder {

while ([[self items] count] > 100) {
	[[self items] removeLastObject];
}

[coder encodeObject:items forKey:@"items"];
[coder encodeObject:title forKey:@"title"];
[coder encodeObject:infoString forKey:@"infoString"];

}[/code]

Haven’t tested this… however, this looks ugly to me, and not really in the semantics of encodeWithCoder: I am not sure about it. another option would be to make yet another copy of the channel, limit it to 100 and archive that third copy…


#9

I think this works as you suggest:

if(!cachedChannel) { cachedChannel = [[RSSChannel alloc] init]; } RSSChannel *channelCopy = [cachedChannel copy]; [connection setCompletionBlock:^(id obj, NSError *err) { if(!err) { [channelCopy addItemsFromChannel:obj]; RSSChannel *archiveChannel = [channelCopy copy]; while ([[archiveChannel items] count] > 100) { [[archiveChannel items] removeLastObject]; } [NSKeyedArchiver archiveRootObject:archiveChannel toFile:cachedPath]; } block(channelCopy, err); }];


#10

I’ll take a look at that. I agree with you, after thinking about it, that putting it in encodeWithCoder: seems a little awkward. Actually, in thinking about it further, I’m now uncomfortable with putting it in RSSChannel at all, since I don’t know why that object should be worried about how many items to cache. It does seem like something for either the controller or store to worry about, which is how you’ve handled it. Thanks.