Blog

Back to Blog

Extending ShinobiGrids with Sorting

Posted on 23 May 2012 Written by Ryan Grey

Updated 17 Oct 2012

Overview

We’ve created ShinobiGrids with some pretty great features, but what if the API doesn’t directly support the functionality that you want to include? 

An example of something that is not (yet) included in the ShinobiGrid API – but you might want your grid to be able to handle – is sorting. What we’ll specifically look at is letting the users of your app tap a particular column header and have the grid sort its rows according to that headers category. It turns out that achieving this is pretty simple given ShinobiGrids use of delegate callback methods and the Cocoa frameworks inbuilt sorting capabilities for collections!

To keep this as short and sweet as possible we will adapt one of the sample apps included with the ShinobiGrids trial or premium downloads – that way we can focus on the code that really counts. SimpleGrid is the project we’ll be modifying – this app shows a two column grid; one displaying country names and the other displaying the populations of those countries. Our aim is to let the user of the app tap the “Country” or “Population” column headers and have the grid lay itself out in an alphabetical or numeric order respectively. Something like this…

Sorted Grid 

Preparing the way

When the user taps a column header we want to indicate to them the direction in which the column will be sorted. Arrows tend to be quite good at pointing out directions so we’ll use one of those! In order to do that we’ll have to add a new class to the project – so open up the SimpleGrid project and create an objective-c class called Arrow, or something with an equally imaginative name. The Arrow.h file will look like this:

@class SGridCell;

@interface Arrow : UIImageView {
}

@property (nonatomic, assign) UIImageOrientation orientation;

- (UIImageOrientation) switchOrientation;
- (void) fadeIn;
- (void) fadeOut;

@end

The three methods correspond to the three actions we’d like the arrow to be able to perform. The arrow should be able to point up and down (switchOrientation), fade in (fadeIn) and fade out (fadeOut). Heads up! – here comes the implementation file:

#import <UIKit/UIKit.h>

#import "ShinobiGrids/SGridCell.h"
#import "Arrow.h"

@implementation Arrow
@synthesize orientation;

+ (UIImage *) arrowImage {
    return [UIImage imageNamed:@"arrow.png"];
}

- (id) init {
    
    UIImage *rotated = [[UIImage alloc] initWithCGImage:[[Arrow arrowImage] CGImage]
                                                  scale:1.0
                                            orientation:UIImageOrientationUp];
    
    self = [super initWithImage:rotated];
    [rotated release];
    if(self){
        self.alpha = 0;
    }
    return self;
}

- (void) fadeTo:(float)alpha {
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.2f];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    self.alpha = alpha;
    [UIView commitAnimations];
}

- (UIImageOrientation) switchOrientation {
    if (orientation == UIImageOrientationUp)
        orientation = UIImageOrientationDown;
    else
        orientation = UIImageOrientationUp;
    
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.2f];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    self.transform = CGAffineTransformRotate(self.transform, M_PI);
    [UIView commitAnimations];
    
    return orientation;
}

- (void) fadeIn {
    [self fadeTo:1];
}

- (void) fadeOut {
    [self fadeTo:0];
}

@end

You’ll notice that the static arrowImage method creates a UIImage from a file called “arrow.png”. Any image can be used here – just drag and drop the png you wish to use into your project tree (ensuring that you copy the file over if you are prompted). Notice that in the init method we set the alpha to 0 – this is because the data is initially unsorted and we only want to show an arrow once the user taps. The other methods do exactly what they say on the tin. We won’t go into anymore detail on the above implementation as the Arrow class is used to tart up the aesthetics of the app, whereas the focus of this article is on the sorting of the grid.

Arrow

Next we’ll get the really boring bit out of the way – we need to alter the existing datasource ever so slightly for the function we’ll be writing later. Change the DataSource.h file to look like the following:

@interface DataSource : NSObject <SGridDataSource> {
    
}

@property (nonatomic, retain) NSArray *countries;
@property (nonatomic, retain) Arrow *countryArrow;
@property (nonatomic, retain) Arrow *popArrow;

- (void) sortForCountries:(BOOL)countrySort;

@end

Now change the init method of the DataSource.m file to the following and ensure that you synthesize the countries, countryArrow and popArrow properties at the top of the file:

-(id)init
{
    self = [super init];
    if(self) {
        struct
        {
            NSString *name;
            NSString *population;
        } countryData[] = {
            { @"Bangladesh"                       , @"142,319,000"   },
            { @"Mexico"                           , @"112,336,538"   },
            { @"China"                            , @"1,339,724,852" },
            { @"Vietnam"                          , @"87,840,000"    },
            { @"India"                            , @"1,210,193,422" },
            { @"United States"                    , @"312,972,000"   },
            { @"Russia"                           , @"143,030,106"   },
            { @"Indonesia"                        , @"237,641,326"   },
            { @"Brazil"                           , @"192,376,496"   },
            { @"Pakistan"                         , @"178,627,000"   },
            { @"Nigeria"                          , @"162,471,000"   },
            { @"Japan"                            , @"127,730,000"   },
            { @"Philippines"                      , @"94,013,200"    },
            { nil                                 , nil              },
        };
        
        NSMutableArray *newCountries = [[NSMutableArray alloc] init];
        
        for(int i = 0; countryData[i].name; i++){
            Country *newCountry = [[Country alloc]
                                   initWithName:countryData[i].name
                                   andPopulation:countryData[i].population];
            
            [newCountries addObject:[newCountry autorelease]];
        }
        self.countries = newCountries;
        [newCountries release];
    }
    
    self.countryArrow = [[[Arrow alloc]init]autorelease];
    self.popArrow = [[[Arrow alloc]init] autorelease];
    
    return self;
}

All we have done here is change the type of array and the way in which we assign  countries. We’ve also stripped out some unnecessary data that won’t be visible in the grid and shuffled the remaining data so that it starts unsorted. In addition to this we have instantiated our two arrow objects which we still need to add as subviews to the appropriate cells. In order to do this find the line “//HEADER CELLS” and replace the if/else block with this snippet of code:

        //HEADER CELLS
        if (gridCoord.column == 0) {
            headerLabel.text = @"Country";
            
            self.countryArrow.frame = [self arrowFrame];
            [headerLabel addSubview:countryArrow];
        } else {
            headerLabel.text = @"Population";
            
            self.popArrow.frame = [self arrowFrame];
            [headerLabel addSubview:popArrow];
        }

The arrowFrame method is a convenience method that sizes and positions the arrows for us and can be placed anywhere in the DataSource.m file:

- (CGRect) arrowFrame {
    CGRect arrowFrame = CGRectZero;
    arrowFrame.origin.y = 5.f;
    arrowFrame.size.height = 40.f - 10.f;
    arrowFrame.origin.x = (40.f - 10.f)/2.f;
    arrowFrame.size.width = 15.f;
    
    return arrowFrame;
}

The more eagle-eyed of you will have a fair idea of what’s coming next, having noticed the method prototype we added to the header file. 

Getting sorted!

That’s right! We’re going to write a sorting method – if we’re going to sort our grid we’ll need one of those right? Add the following method to the DataSource.m file. Don’t worry we’ll explore what the method does below.

- (void) sortForCountries:(BOOL)countrySort {
    
    if (countrySort) {
        [self.countryArrow fadeIn];
        [self.countryArrow switchOrientation];
        [self.popArrow fadeOut];
    } else {
        [self.popArrow fadeIn];
        [self.popArrow switchOrientation];
        [self.countryArrow fadeOut];
    }
    
    self.countries = [countries sortedArrayUsingComparator:^NSComparisonResult(id firstObject, id secondObject) {
        if (countrySort) {
            
            Country *firstCountry = (Country*)firstObject;
            Country *secondCountry = (Country*)secondObject;
            
            NSString *firstCountryName = firstCountry.name;
            NSString *secondCountryName = secondCountry.name;
                        
            if (countryArrow.orientation == UIImageOrientationDown) {
                return [secondCountryName localizedCaseInsensitiveCompare:firstCountryName];
            } else {
                return [firstCountryName localizedCaseInsensitiveCompare:secondCountryName];
            }
        } else {
            Country *firstCountry = (Country*)firstObject;
            Country *secondCountry = (Country*)secondObject;
            
            NSString *firstString = [firstCountry.population stringByReplacingOccurrencesOfString:@"," withString:@""];
            NSString *secondString = [secondCountry.population stringByReplacingOccurrencesOfString:@"," withString:@""];
            
            int firstCountryPop = firstString.intValue;
            int secondCountryPop = secondString.intValue;
            
            if (popArrow.orientation == UIImageOrientationDown) {
                if (firstCountryPop == secondCountryPop) {
                    return NSOrderedSame;
                } else if(firstCountryPop < secondCountryPop) {
                    return NSOrderedDescending;
                } else {
                    return NSOrderedAscending;
                }
            } else {
                if (firstCountryPop == secondCountryPop) {
                    return NSOrderedSame;
                } else if(firstCountryPop < secondCountryPop) {
                    return NSOrderedAscending;
                } else {
                    return NSOrderedDescending;
                }
            }
        }
    }];
}

As mentioned earlier, we are using the inbuilt sorting functionality of the Cocoa framework collections, specifically the  sortedArrayUsingComparator:  method of NSArray, to achieve the desired behaviour. In a nutshell, the above method takes two objects from the array we wish to sort (countries) and compares them – we let the sorting function know if the second object from the array is less than, greater than or equal to the first object by returning the appropriate NSComparisonResult. You will notice that the sort order is based on the orientation of the respective arrow – the switchOrientation function of Arrow rotates the image 180° and changes the orientation property. This means that if the user taps on a specific header twice they will get ascending and then descending order for that column. The use of the fadeIn and fadeOut methods allow us to show the arrow for the actively sorting column and hide the arrow for the irrelevant column with a fancy animation.

Simple Grid Sort

Our method uses the countrySort boolean parameter to define whether we are sorting alphabetically (by country name) or numerically (by population size). In the case of the alphabetic sort there is a handy NSString function (localizedCaseInsensitiveCompare: that already returns an NSComparisonResult - hey presto! No further work for sorting country names! But for the numeric population data sort we have a little bit more work to do. First we need to strip the commas from the population by using the stringByReplacingOccurrencesOfString: method; then we manually compare the population integer in the if/else if/else block. Now all we need to do is ensure that we set countrySort  appropriately, depending on which column header the user taps.

Connecting it up

Now that we have our sorting method, we can put it to use in the ViewController class. In order to do this we can implement the grid delegate method  shinobiGrid:didSelectCellAtCoord: which means we’ll be notified whenever a cell is tapped in our grid. It’s important that we don’t commit the heinous crime of using the willSelect variant here – if we were to do this, cell selection would continue after the grid reload which would cause all kinds of unwanted trouble. In our implementation of shinobiGrid:didSelectCellAtCoord: we’ll change the action for our header cells:

if ([gridCoord hasRow:SGridRowZero]) {
        DataSource *ds = (DataSource*)grid.dataSource;
        [ds sortForCountries:!gridCoord.column];
        [grid reload];
}

By passing !gridCoord.column  as the boolean parameter to our method we will actually pass YES if the user taps the first (index 0) column and NO otherwise (we only have two columns so that’s fine for our needs).  Now, whenever the user taps a column in the top row (one of the header cells) the grid will be sorted and then reloaded. The reload call is pretty important, as it tells the grid to have another peek at the datasource and then lay itself out again based on what it’s seen.

Summary

The app should be good to go now. Try firing it up and give it a try (full code here if you got lost!). 

Rows now have a notion of order, which allows them to be sorted. We could use a similar approach to sort cell’s that contain no text (perhaps image cells etc) where we would define the greater than, less than  and equal to concepts ourselves. There are many other ways in which we could have applied sorting to our grid – you can pick the method that is most efficient for your application’s needs!


 


Back to Blog