Blog

Back to Blog

Creating a Custom Check Box Cell

Posted on 21 May 2013 Written by Jan Akerman

As a ShinobiGrids developer, often you will need a way to let your users select rows in your grid. ShinobiGrids provides row selection out-of-the-box but sometimes you just want to go back to the old school and there ain’t nothing as old school as a check-box.

By the end of this blog post we will have a custom check-box cell that notifies our view controller of any changes, updates our data model and our grid’s styling. Our final product will be a list of students, their grades and a check box deciding whether they can or can’t graduate Shinobi school. The picture below shows a grid I made earlier, and hopefully what your grid will look like after following this post!

 

final product

 

The code for the completed project is available as a zip file download here. If you do not already own a copy of ShinobiControls you’ll have to get yourself a 30 day free trial – available on the website.

The data layer

Each row on our grid will logically correspond to a student so it makes sense to have an object representing a student. In this example we only care about three things, their name, their credits and whether we will let them graduate or not.  So, here is our Student class’s interface: 

@interface Student : NSObject

@property NSString *name;
@property NSNumber *credits;
@property BOOL canGraduate;

- (id)initWithName:(NSString *)name andCredits:(NSNumber *)credits canGraduate:(BOOL)canGraduate;

@end

 

For this blog we are going to need some mock students to populate our grid. In my view controller I added the following method to provide me with a list of mock students.

- (NSArray *)createMockStudentArray {
    return @[[[Student alloc] initWithName:@"Bill"      andCredits:@40  canGraduate:NO],
               [[Student alloc] initWithName:@"Rob"     andCredits:@80  canGraduate:YES],
               [[Student alloc] initWithName:@"James"   andCredits:@80  canGraduate:YES]
…
}

Setting up the grid

I have decided to use the newly released ShinobiDataGrid combined with the ShinobiDataGridDataSourceHelper in this blog post, as it really simplifies the code we will need to write, whilst still allowing us to configure our grid exactly how we need it. So let’s go ahead and create our grid in our view controller’s viewDidLoad method: 

_grid = [[ShinobiDataGrid alloc] initWithFrame:CGRectMake(0, 80, self.view.frame.size.width, self.view.frame.size.height - 80)];
[self.view addSubview:_grid];

_grid.licenseKey = @""; // TODO: add your trial licence key here! 

 

If you are using a trial version of ShinobiGrids then now is the time to set your trial key as the grids license key. If you are using a full version of ShinobiGrids feel free to remove that line of code.

We now need to setup our grids columns and style our grid, the following code can go just after where you init your ShinobiDataGrid. First, we set the default cell style for our header row using a cell style object. I chose to make my grid’s header dark grey with white text, you’ll also notice that I set the text to be both vertically and horizontally aligned:

// Style grid.
_grid.backgroundColor = [UIColor whiteColor];
UIColor *backgroundColor = [UIColor darkGrayColor];
UIColor *textColor = [UIColor whiteColor];
_grid.defaultCellStyleForHeaderRow = [[SDataGridCellStyle alloc] initWithBackgroundColor:backgroundColor withTextColor:textColor withTextAlignment:NSTextAlignmentCenter withVerticalTextAlignment:UIControlContentVerticalAlignmentCenter withFont:nil];

 

Next I disable single tap and double tap events on the grid as we don’t need the ShinobiDataGrid’s selection or edit events in this example:

// Disable grid events (select & edit).
_grid.singleTapEventMask = SDataGridEventNone;
_grid.doubleTapEventMask = SDataGridEventNone;

 

For simplicity we are also going to disable row dragging like so:

// Disable row dragging.
_grid.canReorderRows = NO;

 

 In this example, we want a column for each property on our model object. We are going to tie each column in our grid to a single property on our Student object and then add the column to our grid like so:

SDataGridColumn *nameColumn = [[SDataGridColumn alloc] initWithTitle:@"Name" forProperty:@"name"];
nameColumn.width = @255;
[_grid addColumn:nameColumn];

SDataGridColumn *creditColumn = [[SDataGridColumn alloc] initWithTitle:@"Credits" forProperty:@"credits"];
creditColumn.width = @255;
[_grid addColumn:creditColumn];

SDataGridColumn *tickColumn = [[SDataGridColumn alloc] initWithTitle:@"Can Graduate" forProperty:@"canGraduate" cellType:[MyCheckBoxCell class] headerCellType:nil];
tickColumn.width = @255;
[_grid addColumn:tickColumn];

 

The only thing missing now is the data-source helper; so let’s create it:

// Create our data-source helper and give it some mock students.
_dataSourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:_grid];
_dataSourceHelper.delegate = self;
_dataSourceHelper.data = [self createMockStudentArray];

 

The data-source helper sets itself as the grid’s delegate and acts as a middleman between the grid and the grid’s usual delegate (our view controller). The data-source helper forwards on all delegate methods from the data-grid so it gives us our standard cell population with no work from us, whilst still giving us full control of the data-grid! So as you can see above we set our view controller to be the delegate of the data-source helper and then we populate our data-source helper with our array of mock Student objects.

If you run your project now you should be able to see your grid, populated with students and their marks, but the last column will be empty (see the image below). This column is empty because the data-source helper doesn’t know how to interpret a BOOL value. Not to worry though, we will sort that out after we have made our check box cell. 

 

Without -checkbox -grid 

Creating your check box cell

To make our check box we are going to create a custom cell by sub-classing SDataGridCell. Below is the header file of our to-be check box cell class MyCheckBoxCell:

@class MyCheckBoxCell;

@protocol MyCheckBoxDelegate <NSObject>

-(void)myCheckBoxCellDidChange:(MyCheckBoxCell *)checkBox;

@end

@interface MyCheckBoxCell : SDataGridCell

@property id<MyCheckBoxDelegate> myCheckCellDelegate;
@property BOOL checked;

@end

 

You’ll notice that we have defined a protocol as well as a class interface. Our check box needs to notify something when its value is changed, so we will define our very own delegate protocol for our check box.

When the data-grid initialises our custom cell it will be calling the initWithReuseIdentifier: method so we need to implement that:

-(id)initWithReuseIdentifier:(NSString *)identifier {
    if (self = [super initWithReuseIdentifier:identifier]) {
        
        // Load in tick images.
        _onImage = [UIImage imageNamed:@"tick_on.png"];
        _offImage = [UIImage imageNamed:@"tick_off.png"];
        
        // Initialise the checkbox with the off image and the correct size.
        _checkBox = [[UIImageView alloc] initWithImage:_offImage];
        _checkBox.frame = CGRectMake(0, 0, CHECKBOX_IMAGE_SIZE.width, CHECKBOX_IMAGE_SIZE.height);
        _checkBox.userInteractionEnabled = YES;
        [self addSubview:_checkBox];

 

The above method loads in our on/off images and creates our check box image view (with an initial state of unchecked). We next need to add a tap gesture recognizer to our check box image view so we can toggle its state when our users interact with it:

     // Add a tap gesture recognizer that will toggle our check box on and off.
        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggle)];
        [_checkBox addGestureRecognizer:tapRecognizer];
        
    }
    return self;
}

// This is the method that will be called by our tap gesture recognizer on our check box.
- (void)toggle {
    
    // Toggle the check box's state.
    [self setChecked:!_checked];

    // If our custom cell has a delegate then we need to notify it that the cell has changed.
    if ([self.myCheckCellDelegate respondsToSelector:@selector(myCheckBoxCellDidChange:)]) [self.myCheckCellDelegate myCheckBoxCellDidChange:self];
}

 

The tap gesture recogniser will call our toggle method, which negates the checked property of our custom cell, changing the cells state. The toggle method also sends a message to our cell’s delegate letting it know that the cell’s checked value has changed.

We need to change the image view’s image every time our custom cell’s checked property is changed. To do this we are going to override the setter of the checked property like so:

// Set the check box's state according to the given parameter.
-(void)setChecked:(BOOL)checked {
    _checked = checked;
    
    if (_checked) {
        _checkBox.image = _onImage;
    } else {
        _checkBox.image = _offImage;
    }
}

Adding our custom cell to the grid

As we want to add our check box cell into third column of the grid we need to change the code for the tick column to be the following:

SDataGridColumn *tickColumn = [[SDataGridColumn alloc] initWithTitle:@"Can Graduate" forProperty:@"canGraduate" cellType:[MyCheckBoxCell class] headerCellType:nil];

 

The above code tells the grid that it is to use our custom cell class for all cells in this column and since we don’t have a custom header cell we can just leave that as nil.

So the above code has told your grid to use your custom cell in the third column but if you run your app now nothing will have changed. This is because the grid doesn’t know how to populate our custom cell, so we need to populate the cell ourselves. The data-source helper’s delegate provides the method:

dataGridDataSourceHelper:populateCell:withValue:forProperty:sourceObject:

 

This delegate method gives us an opportunity to populate our custom cell manually. We do a simple check to ensure we only manually populate our custom check box cells by checking the columns property key. We are then going to set the check box depending on whether the student can or can’t graduate, returning YES to let the data-source helper know we manually populated our cell. 

-(BOOL)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper populateCell:(SDataGridCell *)cell withValue:(id)value forProperty:(NSString *)propertyKey sourceObject:(id)object {
    
    Student *student = (Student *)object;
    
    // I only want to populate the third column (our checkbox column) manually.
    if ([propertyKey isEqualToString:@"canGraduate"]) {
        
        MyCheckBoxCell *checkCell = (MyCheckBoxCell *)cell;
        
        // As the datasource-helper handles the cell reuse we need to set the check box's delegate here.
        checkCell.myCheckCellDelegate = self;
        
        // Make the checkbox show whether the student can graduate.
        checkCell.checked = student.canGraduate;
        
        // Return YES to tell the data-source helper that we have manually populated this cell.
        return YES;
    }
    
    // For all other cell types (other than our custom checkbox) we want to return NO to let the data-source helper know it needs to handle the cell population.
    return NO;
}

 

So if you run the project now, your grid should be showing your check boxes in the third column! You can click your text boxes but nothing will happen. This is because we haven’t implemented the methods defined by the delegate protocol we created for our check box class!

When our check box is clicked we want to update the Student object’s canGraduate property and reload the grid. To do this we will implement the check box delegate method in our view controller like so: 

// This is the delegate we created for our checkbox, letting us know when its state has changed.

-(void)myCheckBoxCellDidChange:(MyCheckBoxCell *)checkBox {
    
    // Get the student object that corresponds to the checkbox's row and update whether he can graduate or not.
    Student *student = _dataSourceHelper.data[checkBox.coordinate.row.rowIndex];
    student.canGraduate = [checkBox checked];
    
    // Reload the row so that the change in row style can take place.
    [_grid reloadRows:@[checkBox.coordinate.row]];
}

Perfect, now clicking the tick box actually changes the model object that the check box is representing. There is just one missing link now – we need to show this change on our grid some how, otherwise this is a very boring blog post! 

To show this change we are going to style the row of each student green or red depending on whether they can or can’t graduate. We are going to implement the data-grid delegate method:

shinobiDataGrid:alterStyle:beforeAddingToCellAtCoordinate:

This gives us a chance to look at the data object behind the row and make some decisions about the row’s styling. Inside this delegate method we get the Student object for the row that the cell belongs to, and apply the relevant background colour like so:

-(void)shinobiDataGrid:(ShinobiDataGrid *)grid alterStyle:(SDataGridCellStyle *)styleToApply beforeApplyingToCellAtCoordinate:(SDataGridCoord *)coordinate {
    Student *student = _dataSourceHelper.data[coordinate.row.rowIndex];
    
    if (student.canGraduate) {
        styleToApply.backgroundColor = [UIColor colorWithRed:225./255. green:252./255. blue:225./255. alpha:1];
    } else {
        styleToApply.backgroundColor = [UIColor colorWithRed:252./255. green:225./255. blue:225./255. alpha:1];
    }
}

 

And there you have it – pass or fail anyone you like! Below is a picture of our finished work.

Follow -along -finished

Summing it all up

When trying to learn new things it is always important to go back and look at the steps you have taken, so here they are:

  • We created our data layer and mock data.
  • Set up our grid’s styling:
    • Set up its styling.
    • Linked each of its columns to a model object property.
  •  Created our check box SDataGridCell subclass.
  • Told our grid where and how to populate our custom subclass.
  • Implemented our check box delegate so we can do something when the box is clicked.

Now you should have a good idea on how to create a flexible custom check box cell that you can reuse throughout all your future projects!

Back to Blog