Artikel getaggt mit Objective C

NSCachedURLResponse / NSKeyedUnarchiver pain

as the iPhone SDK comes with a rather dysfunctional NSURLCache — Apple suggests to implement it from scratch yourself in the code examples about caching — I went for just this.

Until I came across the [NSKeyedUnarchiver unarchiveObjectWithData:...] not restoring userInfo, storagePolicy and data of NSCachedURLResponse.

Couldn’t believe it and spent almost the whole day verifying that the error is not within my code but really the unarchiver treats those fields transient.

See yourself:

-(void)testiPhoneSDK312NSKeyedArchiver
{
//  prepare a NSCachedURLResponse
    NSCachedURLResponse *src = nil;
    {
        NSURL *i_url = [NSURL URLWithString:@"http://www.url.example/path/file.html"];
        NSString *i_mime = @"text/html";
        NSInteger expectedContentLength = 112;
        NSString *i_encoding = @"iso-8859-2";
 
        NSURLResponse *i_response = [[NSURLResponse alloc] initWithURL:i_url
            MIMEType:i_mime
            expectedContentLength:expectedContentLength
            textEncodingName:i_encoding];
        NSData *i_data = [@"Hello, world!" dataUsingEncoding:NSISOLatin2StringEncoding];
        NSURLCacheStoragePolicy i_storage =  NSURLCacheStorageAllowed;
        NSDictionary *i_userInfo = [NSDictionary dictionaryWithObject:
        [NSDate dateWithTimeIntervalSince1970:13] forKey:@"era"];
 
        NSCachedURLResponse *src = [[NSCachedURLResponse alloc] initWithResponse:i_response
            data:i_data
            userInfo:i_userInfo
            storagePolicy:i_storage];
 
//      ensure it's all in place:
        STAssertEqualObjects(i_url, src.response.URL, @"");
        STAssertEqualObjects(i_mime, src.response.MIMEType, @"");
        STAssertEquals((int)expectedContentLength, (int)src.response.expectedContentLength, @"");
        STAssertEqualObjects(@"file.html", src.response.suggestedFilename, @"");
        STAssertEqualObjects(i_encoding, src.response.textEncodingName, @"");
 
        STAssertEqualObjects(i_data, src.data, @"");
        STAssertEqualObjects(i_userInfo, src.userInfo, @"");
        STAssertEquals( 0u, src.storagePolicy, @"");
    }
 
//  archive + unarchive:
    NSCachedURLResponse *dst = [NSKeyedUnarchiver unarchiveObjectWithData:
      [NSKeyedArchiver archivedDataWithRootObject:src]];
 
//  check whether src == dst
    STAssertEqualObjects(src.response.URL, dst.response.URL, @"");
    STAssertEqualObjects(src.response.MIMEType, dst.response.MIMEType, @"");
    STAssertEquals(src.response.expectedContentLength, dst.response.expectedContentLength, @"");
    STAssertEqualObjects(src.response.suggestedFilename, dst.response.suggestedFilename, @"");
    STAssertEqualObjects(src.response.textEncodingName, dst.response.textEncodingName, @"");
 
//  !!!!!!!!!!
//  sad information loss after unarchiving:
    STAssertNil( dst.data, @"" );
    STAssertNil( dst.userInfo, @"" );
    STAssertEquals( 2u , dst.storagePolicy, @"" );
}

When my NSURLCache replacement is ready I think about publishing it a github — interested anyone?

Tags: , , , , , , ,

Binary Search NSArray

Though CFArray comes with binary search capability, NSArray does not – at least not within the iPhone SDK. The indexOfObject:inSortedRange:options:usingComparator: can’t be found.

Plus the CFArrayBSearchValues doesn’t tell you whether the key actually is part of the list or not. That’s what the Java JDK does, so let’s implement some category methods

-(NSInteger)binarySearch:(id)key;
-(NSInteger)binarySearch:(id)key usingSelector:(SEL)comparator;
-(NSInteger)binarySearch:(id)key usingSelector:(SEL)comparator inRange:(NSRange)range;
-(NSInteger)binarySearch:(id)key usingFunction:(NSInteger (*)(id, id, void *))comparator context:(void *)context;
-(NSInteger)binarySearch:(id)key usingFunction:(NSInteger (*)(id, id, void *))comparator context:(void *)context inRange:(NSRange)range;
-(NSInteger)binarySearch:(id)key usingDescriptors:(NSArray *)sortDescriptors;
-(NSInteger)binarySearch:(id)key usingDescriptors:(NSArray *)sortDescriptors inRange:(NSRange)range;

ourselves, like

-(NSInteger)binarySearch:(id)key usingFunction:(NSInteger (*)(id, id, void *))comparator context:(void *)context inRange:(NSRange)range
{
    NSLogD(@"[NSArray(MroBinarySearch) binarySearch:%@ usingFunction:]", key);
    if(self.count == 0 || key == nil || comparator == NULL)
        return [self binarySearch:key usingSelector:nil inRange:range];
 
//	check overflow?
    NSInteger min = range.location;
    NSInteger max = range.location + range.length - 1;
 
    while (min < = max)
    {
        // http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html
        const NSInteger mid = min + (max - min) / 2;
        switch (comparator(key, [self objectAtIndex:mid], context))
        {
            case NSOrderedSame:
                return mid;
            case NSOrderedDescending:
                min = mid + 1;
                break;
            case NSOrderedAscending:
                max = mid - 1;
                break;
        }
    }
    return -(min + 1);
}

See the full interface + implementation + testcase without html encoding dirt at github.

Tags: , , , , , , , , ,

CoreData generic findManyByKey

The base for many of my SELECT-ish queries when querying by exact match is one generic method I created in some category methods on NSManagedObjectContext:

-(NSArray*)entityName:(NSString*)entityName findManyByRelation:(NSDictionary*)dict
{
//  TODO handle dict nil and emptyness
    NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:dict.count];
    for(NSString *key in dict)
    {
        NSExpression *left = [NSExpression expressionForKeyPath:key];
        NSExpression *right = [NSExpression expressionForConstantValue:[dict objectForKey:key]];
        NSComparisonPredicate *cp = [[NSComparisonPredicate alloc] initWithLeftExpression:left
            rightExpression:right modifier:NSDirectPredicateModifier type:NSEqualToPredicateOperatorType options:0];
        [arr addObject:cp];
        [cp release];
    }
    NSPredicate *pred = nil;
    if(arr.count == 1)
        pred = [[arr objectAtIndex:0] retain];    // why do I have to retain here?
    else
        pred = [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:arr];
 
    NSFetchRequest *fr = [[NSFetchRequest alloc] init];
    fr.entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:self];
    fr.predicate = pred;
    [arr release];
    [pred release];
//	NSLog(@"predicate effective: %@", fr.predicate);
 
    NSError *err = nil;
    NSArray *ps = [self executeFetchRequest:fr error:&err];
    if(err != nil)
        [NSException raise:@"DB Error" format:@"Fetch problem %@", fr.predicate.predicateFormat];
    [fr release];
    return ps;
}

For example it serves under the hood of it’s sibling helpers

-(NSManagedObject*)entityName:(NSString*)entityName findByPrimaryKey:(NSDictionary*)dict
{
    NSArray *ps = [self entityName:entityName findManyByRelation:dict];
    if(ps == nil || ps.count == 0)
        return nil;
    if (ps.count == 1)
        return [ps objectAtIndex:0];
    [NSException raise:@"DB Error" format:@"Multiple hits for %@", dict];
    return (NSManagedObject*)1/0;
}

and

-(NSManagedObject*)entityName:(NSString*)entityName selectOrInsertWithKey:(NSDictionary*)dict
{
    NSManagedObject *o = [self entityName:entityName findByPrimaryKey:dict];
    if(o == nil)
    {
        NSEntityDescription *ed = [NSEntityDescription entityForName:entityName inManagedObjectContext:self];
        o = [[NSManagedObject alloc] initWithEntity:ed insertIntoManagedObjectContext:self];
        for(NSString *key in dict)
            [o setValue:[dict objectForKey:key] forKey:key];
        [o autorelease];
    }
    return o;
}

Those methods again are called by custom convenience wrappers in the model classes loosely following the ActiveRecord Pattern.

Tags: , , , , , , ,

NSDateFormatter case sensitive trap

Though NSDateFormatter behaves slightly different than documented, the following might even be correct, as strange as it might look (mind the last two lines):

-(void)testNSDateFormatterTrap
{
    NSDateFormatter *lower = [[[NSDateFormatter alloc] init] autorelease];
    lower.dateFormat = @"yyyy-MM-dd HH:mm:SS ZZZ";
 
    NSDateFormatter *upper = [[[NSDateFormatter alloc] init] autorelease];
    upper.dateFormat = @"YYYY-MM-dd HH:mm:SS ZZZ";
 
    lower.timeZone = upper.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
 
    NSDate *d = [lower dateFromString:@"1970-01-01 00:00:00 +0000"];
    STAssertEqualObjects(@"1970-01-01 00:00:00 +0000", [lower stringFromDate:d], @"lower iso wrong");
    STAssertEqualObjects(@"1970-01-01 00:00:00 +0000", [upper stringFromDate:d], @"upper iso wrong");
 
    d = [d addTimeInterval:(-60*60)];
 
    STAssertEqualObjects(@"1969-12-31 23:00:00 +0000", [lower stringFromDate:d], @"lower iso wrong");
    STAssertEqualObjects(@"1970-12-31 23:00:00 +0000", [upper stringFromDate:d], @"upper iso wrong");
}

The Unicode Format Pattern Documentation explains the difference of the upper- and lowercase year format – but frankly I don’t get the “Year of week of year” idea.

But that subtracting one hour in fact adds almost a whole year – that’s odd to me.

So I rather stay away from the uppercase form – be it correct or buggy.

Seen with iPhone SDK 3.1.2 and XCode 3.2.1 on Snow Leopard.

Update:

I think I got it! Uppercase YYYY makes sense only in combination with a calendar week – and not months or quarters.

Look at January 1st 2010. It belongs to calendar week 53 of 2009. Week 1/2010 starts on Jan 4th.

Tags: , , , , , ,

CocoaTouch, CoreData and binary String Search

The query optimiser for NSPredicate queries ontop CoreData/SQLite on the iPhone is a bit rudimentary (cough) and so I had to optimise myself to get binary-search enabled quick results:

Den Rest des Eintrags lesen. »

Tags: , , , , , ,

Cocoa wrapped regex.h

Strange enough there’s no regular expression class in the iPhone SDK. My simple wrapper around the regex.h C API is not safe for unicode matching patterns but does the job e.g. for parsing URLs. If you need more, have a look at RegexKitLite. My simple wrapper has the interface:

Den Rest des Eintrags lesen. »

Tags: , , , ,

NSDateFormatter & Http Header

Ever tried to get e.g. the “Last-Modified” HTTP response header field into a NSDate object? That’s no real fun, because

  1. this standard formatting isn’t digested by default,
  2. the required format string doesn’t quite work as documented.

Den Rest des Eintrags lesen. »

Tags: , , , , , , ,

Unit Testing / iPhone

Having Eclipse & JUnit in mind I missed unit testing quite a bit while developing with XCode for iPhone.

As a first shot, I set SenTestingKit up as explained by it’s author and it works really nicely. One thing I still miss is Step’n'Trace debugging the tests.

Other intros to the topic from Apple about OCUnit (2005) and SenTestingKit (2008) are ok but not as good as from the author of SenTestingKit.

Didn’t examine other Unit Test frameworks like e.g. the one from Google yet.

Tags: , , , , ,

NSURLCache Joke / iPhone

Did you ever wonder why Apple’s own Demo App URLCache doesn’t use the NSURLCache class, but rather reimplements disk caching instead? Well, it looks like NSURLCache promises disk-caching, but doesn’t keep this promise.

Tags: , , , ,

NSURLConnection gzip magic

For quite some time I ranted about not being able to use compressed network communcation out-of-the-box on the iPhone.

Despite being undocumented (or I just overlooked the hint), NSURLConnection does gzip decompression transparently!

That’s how to use it:

Den Rest des Eintrags lesen. »

Tags: , , , , , , ,