Matt Connolly's Blog

my brain dumps here…

Tag Archives: iOS

iOS Core Data: Group by and count results

I looked in the usual places and couldn’t find any decent examples of using core data to do a group by and count. So I’m making one here.

Let’s suppose you have a bunch of Records, and each Record has a Status attribute among others, and you want a break down of how many have each Status. The SQL would be:

SELECT `Status`, COUNT(*) FROM `Records` GROUP BY `Status`

How do we do this with Core Data?

start with a fetch request:

NSFetchRequest* fetch = [NSFetchRequest fetchRequestWithEntityName:@"Record"];

set up an attribute description and expression description for the two values we want in the results:

NSEntityDescription* entity = [NSEntityDescription entityForName:@"Record"
                                          inManagedObjectContext:myManagedObjectContext];
NSAttributeDescription* statusDesc = [entity.attributesByName objectForKey:@"status"];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"url"]; // Does not really matter
NSExpression *countExpression = [NSExpression expressionForFunction: @"count:"
                                                          arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: @"count"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];

tell the fetch request to only fetch these, and group by the status attribute description

[fetch setPropertiesToFetch:[NSArray arrayWithObjects:statusDesc, expressionDescription, nil]];
[fetch setPropertiesToGroupBy:[NSArray arrayWithObject:statusDesc]];
[fetch setResultType:NSDictionaryResultType];
NSError* error = nil;
NSArray *results = [myManagedObjectContext executeFetchRequest:fetch
                                                         error:&error];

And voila. The result is an array of dictionaries, each with “status” and “count” keys and corresponding values. Even though the group by guarantees the grouped values be unique, there could be more than one grouped column, so it does make sense.
If you think that’s a lot of code, it is. Let’s compare that with the Rails solution:

Record.count(:group => :status)

And that returns a dictionary (Hash), keyed by status, with the counts in the values

Advertisements

Xcode testing AFNetwork Operation callback blocks

Just recently, I was writing some tests in Xcode for some HTTP requests using the AFNetwork library. Previously I’ve used the ASIHTTPRequest library, but in this particular project, I’ve chosen to use AFNetworking for its JSON support.

Since the requests run asynchronously we need a way to wait for the operation to complete. This is easy:

- (void)testRequest
{
    MyHTTPClient* api = [MyHTTPClient sharedInstance]; // subclass of AFHTTPClient
    NSDictionary* parameters = [NSDictionary dictionary]; // add query parameters to this dict.
    __block int status = 0;
    AFJSONRequestOperation* request = [api getPath:@"path/to/test"
                                        parameters:parameters
                                           success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                               // success code
                                               status = 1;
                                               NSLog(@"succeeded");
                                           } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                               // failure
                                               status = 2;
                                               NSLog(@"failed");
                                           }];
    [api enqueueHTTPRequestOperation:request];
    [api.operationQueue waitUntilAllOperationsAreFinished];

    STAssertTrue([request isFinished], @"request finished");
    STAssertEquals(request.response.statusCode, 200, @"request returned 200 OK");
    STAssertEquals(status, 1, @"success block was executed");
}

This is great for testing that the request completes, and verifying its status. But if we need to test anything in the success or failure callbacks, the last test will fail with `status == 0`.

This is because AFNetwork processes its response in a background thread, and the final success or failure block callback is dispatched asynchronously from there to a specific queue, which unless provided is the main queue. This means that the block won’t get called until *AFTER* the test code has completed.

Putting in some kind of a lock causes a deadlock, since the test is running on the main thread, and the block callback never gets an opportunity to run. The solution is to manually run the main threads runloop until the callbacks have been processed.

Here’s my solution:

- (void)testRequest
{
    MyHTTPClient* api = [MyHTTPClient sharedInstance]; // subclass of AFHTTPClient
    NSDictionary* parameters = [NSDictionary dictionary]; // add query parameters to this dict.
    __block int status = 0;
    AFJSONRequestOperation* request = [api getPath:@"path/to/test"
                                        parameters:parameters
                                           success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                               // success code
                                               status = 1;
                                               NSLog(@"succeeded");
                                           } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                               // failure
                                               status = 2;
                                               NSLog(@"failed");
                                           }];
    [api enqueueHTTPRequestOperation:request];
    [api.operationQueue waitUntilAllOperationsAreFinished];

    while (status == 0)
    {
        // run runloop so that async dispatch can be handled on main thread AFTER the operation has 
        // been marked as finished (even though the call backs haven't finished yet).
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate date]];
    }

    STAssertTrue([request isFinished], @"request finished");
    STAssertEquals(request.response.statusCode, 200, @"request returned 200 OK");
    STAssertEquals(status, 1, @"success block was executed");
}

This addition will continually pump that run loop which allows AFNetwork’s async dispatch of the block to the main queue to execute, and hey presto! We now have a test that can also verify code in the success (or failure) completion blocks of an AFNetwork request operation.