Back to Blog

Building your own editable data grid cells

Posted on 28 Jan 2014 Written by Alison Clarke

ShinobiGrids comes with some really useful cell types out of the box, allowing you to easily create editable data grids. But have you ever wanted to give your users something other than a text field to play with when they edit the contents of a cell? This tutorial shows you how to create your own editable cell that displays a UIPickerView in edit mode, a bit like the one in the ToDo list in ShinobiPlay.

Shinobiplaypicker

This tutorial will take you through everything step-by-step, but if you get stuck, you can browse the project on GitHub or download it as a zip. You’ll need to a copy of ShinobiGrids (v2.5.3 or greater) to get the project working – if you don’t have one, you can download a free trial version.

Getting started

Everything ready? Let’s get started. First, create a new project in Xcode. We’ll use a Single View Application and set it up for iPad. Then in your new project, go to the Build Phases tab, open the “Link Binary With Libraries” section and add ShinobiGrids.framework. If you’ve installed ShinobiGrids using the installer, the framework should be available to choose under “Developer Frameworks”; if you haven’t used the installer then click “Add Other…” and select “ShinobiGrids.framework” from wherever you saved ShinobiGrids.

The first thing we’ll do is to create a ShinobiDataGrid in our view. Open up Main.storyboard, go to the File Inspector, and under “Interface Builder Document”, uncheck the “Use Autolayout” box. Add a new View to the storyboard, then open the Identity Inspector and edit its Custom Class to be ShinobiDataGrid. It should then look something like this:

Interfacebuilder

Now open up the Assistant Editor, right-click on the new view and drag it over to ViewController.h, to create a new Outlet called shinobiDataGrid. Add the following import to ViewController.h to fix the build errors:

#import <ShinobiGrids/ShinobiDataGrid.h>

Setting up the data

We’ve now got an empty data grid, so what are we going to put in it? Let’s borrow and modify the PersonDataObject that is used by a few of the ShinobiGrids sample projects. I’m not going to go over this code line by line as it’s not particularly interesting, so just take a copy of the following files from GitHub:

To summarize the classes:

  • A PersonDataObject has a title (of type PersonTitle), a surname (string) and a forename (string). It also defines an enum called PersonTitle with a list of possible titles, and provides a class method to get a list of all the display names for PersonTitles, and an instance method which returns the display name for the instance’s title property.
  • The PersonDataSource class just provides a method to generate a list of person objects, using random combinations of title, forename and surname. (You might realize if you look at the code that as we haven’t specified which forenames and titles are male and female, we’ll probably end up with some slightly odd combinations of title and forename, but hey, it’s only sample data.)

Setting up the grid

Now we’ve got some data to use, let’s set up a grid. Open up ViewController.m. If you’re using the trial version, then add the following line to viewDidLoad, filling in your license key:

// Set up the license key if you're using the trial version
self.shinobiDataGrid.licenseKey = @""; // TODO: add your trial license key here!

We need to add some columns to our grid. We’re going to use the SGridDataSourceHelper to create a grid data source from an array of PersonDataObjects, so we’ll use the initWithTitle:forProperty: method to create a column for each of the objects’ properties. Add the following to viewDidLoad:

// Add a title column, with the custom PickerCell type
SDataGridColumn* titleColumn = [[SDataGridColumn alloc] initWithTitle:@"Title" forProperty:@"titleDisplayName"];
titleColumn.editable = YES;
[self.shinobiDataGrid addColumn:titleColumn];
    
// Add a forename column
SDataGridColumn* forenameColumn = [[SDataGridColumn alloc] initWithTitle:@"Forename" forProperty:@"forename"];
forenameColumn.editable = YES;
[self.shinobiDataGrid addColumn:forenameColumn];
    
// Add a surname column
SDataGridColumn* surnameColumn = [[SDataGridColumn alloc] initWithTitle:@"Surname" forProperty:@"surname"];
surnameColumn.editable = YES;
[self.shinobiDataGrid addColumn:surnameColumn];

Here we’re just creating three columns, and making them editable. Note that for the title column we use the titleDisplayName property (for now) to display a nice title rather than just the enum value.

Next we need to generate some data, and create a datasource helper. We’ll need to import PersonDataSource.h and PersonDataObject.h, and store the data and the datasource helper as instance variables:

#import "ViewController.h"
#import "PersonDataSource.h"
#import "PersonDataObject.h"

@interface ViewController ()
{
    NSArray* _data;
    SDataGridDataSourceHelper* _datasourceHelper;
}
@end

Then at the bottom of viewDidLoad we create the data and the datasource helper, and pass the data to the helper:

// Create some data to populate the grid
_data = [PersonDataSource generatePeople:20];
    
// Create the data source helper and set its delegate and data 
_datasourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:self.shinobiDataGrid];
_datasourceHelper.data = _data;

You can now run the app and you’ll get a grid displaying 20 random people:

 Screenshot01

We made the cells editable, so when you double-click on them a keyboard appears and you can edit the text:

 Screenshot02

Hooking up the edit events

Currently when you edit a cell’s text, all that happens is the value that’s displayed changes. What we’d like to do is to get hold of the new value and update our “model”. In this case, that just means updating the values in our array of PersonDataObjects.

Handily, ShinobiGrids makes it easy to hook into edit events, using the SDataGridDelegate protocol. We’ll make ViewController conform to the protocol, then use the view controller as our grid’s delegate. First, we need to declare the protocol, in ViewController.h:

@interface ViewController : UIViewController<SDataGridDelegate>

Next, we set the grid’s delegate in viewDidLoad in ViewController.m:

// Set the grid's delegate
self.shinobiDataGrid.delegate = self;

Finally, we implement the delegate method shinobiDataGrid:didFinishEditingCellAtCoordinate:, which is called after an edit has been made:

#pragma mark - SDataGridDelegate methods

// Called when a cell within the ShinobiDataGrid object has been edited.
- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didFinishEditingCellAtCoordinate:(SDataGridCoord *)coordinate
{
    // Find the cell that was edited (all our cells are SDataGridTextCells)
    SDataGridTextCell* cell = (SDataGridTextCell*)[self.shinobiDataGrid visibleCellAtCoordinate:coordinate];
    
    // Locate the 'model' object for this row
    PersonDataObject* person = _data[coordinate.row.rowIndex];
    
    // Determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"Forename"])
    {
        person.forename = cell.textField.text;
    }
    if ([cell.coordinate.column.title isEqualToString:@"Surname"])
    {
        person.surname = cell.textField.text;
    }
    if ([cell.coordinate.column.title isEqualToString:@"Title"])
    {
        // What should we do here?
    }
}

This method first grabs the cell which has just been edited, casting it to an SDataGridTextCell (which is the default cell type, so all our cells will be of that type). Next, it grabs the relevant PersonDataObject from our array of data. It then checks for the cell’s column title, and sets the forename or surname if the cell is in one of those columns.

But what if the cell is in the “Title” column? We could probably check whether the value typed in can be parsed to a valid PersonTitle, and tell the user if the value isn’t valid. But that’s not really a great experience for the user. What we’d really like is for the user to be able to choose from a list of valid titles, using a UIPickerView. So let’s create a custom cell type to do just that.

Creating a custom cell

Before we get stuck into creating the picker cell, let’s think about exactly what we want. Adding a UIPickerView directly into the grid wouldn’t work too well because picker views are pretty tall. Instead, we’ll display a popover when the cell is in edit mode, and put the picker view inside it, so it will look something like this:

Screenshot03

We’ll need to do that in two steps:

  1. Create a UIViewController subclass which displays a UIPickerView (and conforms to the UIPickerDataSource and UIPickerDelegate protocols). We need this because a popover requires a view controller to manage its content.
  2. Create a custom picker cell class which subclasses SDataGridCell, and owns a UIPopoverController containing an instance of our new view controller, which it will display when the cell is in edit mode.

The picker view controller

First, we’ll create our new view controller to manage the contents of the popover (which will simply be a UIPickerView). Add a new Objective-C class called PickerViewController, subclass of UIViewController. Edit PickerViewController.h as follows:

@interface PickerViewController : UIViewController<UIPickerViewDataSource,UIPickerViewDelegate>

@property (nonatomic, assign) int selectedIndex;
@property (nonatomic, strong) NSArray* values;

@end

The view controller holds an array of valid values to display in its picker view, and an integer indicating the index of the currently selected value. We’re also declaring that PickerViewController will conform to the UIPickerViewDataSource and UIPickerViewDelegate protocols. These are needed to display the data in a UIPickerView, and to respond to an item being selected in the picker.

Now open up PickerViewController.m. We’re going to be creating the view controller programmatically rather than using nibs, so delete the methods that are auto-generated, and instead add the following:

@implementation PickerViewController
{
    UIPickerView *_pickerView;
}

- (void)loadView
{
    // Create and set up a UIPickerView if it hasn't already been created
    if (_pickerView == NULL)
    {
        _pickerView = [[UIPickerView alloc] init];
        _pickerView.delegate = self;
        _pickerView.dataSource = self;
        _pickerView.frame = CGRectMake(0, 0, 150, _pickerView.frame.size.height);
        
        // Select the row matching _selectedIndex
        [_pickerView selectRow:_selectedIndex inComponent:0 animated:NO];
    }
    
    // Set our view to be _pickerView
    self.view = _pickerView;
    // Set our preferred size to that of our picker view
    self.preferredContentSize = _pickerView.frame.size;
}

loadView is called when the view controller requests a view, and the view is nil. So we’ll create a new UIPickerView (if that hasn’t already been done), set its delegate and data source to be ourselves, and update its frame to make sure it’s not too wide. (You could probably make this code more generic by calculating the width of each item being displayed, but we’ll keep it simple for now.) Next, we select the row in the picker corresponding to the currently selected index. Finally, we set our picker view as the view controller’s view, and set its preferred content size.

You’ll probably have a couple of warnings at this point, because we haven’t yet implemented the required methods of UIPickerViewDataSource. Let’s add them in:

#pragma mark UIPickerViewDataSource methods

// Returns the number of 'columns' to display.
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
    return 1;
}

// Returns the # of rows in each component..
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent: (NSInteger)component
{
    return [_values count];
}

These methods simply tell the UIPickerView how many columns and rows to display.

To get the actual values into the picker, we implement the UIPickerViewDelegate‘s pickerView:titleForRow:forComponent: method:

#pragma mark UIPickerViewDelegate methods

// Returns the display value for the relevant row
-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    return [_values[row] description];
}

This method simply gets the value for the relevant row from the _values array, and returns its description (in case the value isn’t a string).

Our PickerViewController will now display a picker view containing the values passed to it, but we need to be able to respond to the user selecting a row. UIPickerViewDelegate provides us with a method for that, but we’ll need to pass the new value back to the cell that created us. We’ll do that using a new delegate.

We define the delegate protocol at the top of PickerViewController.h, and add a delegate property to our view controller:

@protocol PickerDelegate <NSObject>
@required
-(void)didSelectRowAtIndex:(int)newIndex;
@end

@interface PickerViewController : UIViewController<UIPickerViewDataSource,UIPickerViewDelegate>

@property (nonatomic, assign) int selectedIndex;
@property (nonatomic, strong) NSArray* values;
@property (nonatomic, weak) id<PickerDelegate> delegate;

@end

The new PickerDelegate protocol requires a single method, didSelectRowAtIndex:, which we can use to pass the new selection back to the custom cell.

Now in PickerViewController.m, we can implement the UIPickerViewDelegate‘s pickerView:didSelectRow:inComponent: method, to call our own delegate method:

// Called when the user selects a row
-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    // Call our PickerDelegate's didSelectRowAtIndex method
    if (_delegate != nil) {
        [_delegate didSelectRowAtIndex:row];
    }
}

All this does is call the delegate method and pass it the new row index.

Our PickerViewController is now ready to go: it can accept a list of value and a currently selected index, display a picker view, and respond to a selection by calling a delegate method.

(If you like, you could make the view inside here a bit more complex, to add a close button like the one in ShinobiPlay, but I’ll leave that as an exercise to the reader).

The picker cell

Next we’ll create the custom picker cell class. Add a new Objective-C class to the project, called PickerCell, a subclass of SDataGridTextCell. We’ll want to conform to the PickerDelegate protocol to receive updates from the picker. We also want to be able to pass our cell an array of values to choose from, and an integer indicating the index within the array of currently selected value. We’ll also pass it the current ShinobiDataGrid for use later on.

So edit PickerCell.h as follows:

#import <ShinobiGrids/ShinobiDataGrid.h>
#import "PickerViewController.h"

/**
 PickerCell subclasses SDataGridCell to display a UIPicker inside a popover when in edit mode
 */
@interface PickerCell : SDataGridCell<PickerDelegate>

@property (nonatomic, assign) int selectedIndex;
@property (nonatomic, strong) NSArray* values;
@property (nonatomic, strong) ShinobiDataGrid* dataGrid;

@end

Now open up PickerCell.m, and delete the auto-generated methods. Then add the following private variables:

@implementation PickerCell
{
    UILabel* _label;
    PickerViewController* _pickerViewController;
    UIPopoverController* _popover;
}

_label will display the current value inside the cell; _pickerViewController and _popover will be used to display the picker view when the cell is in edit mode.

Next, we need to override the initWithReuseIdentifier: constructor from SDataGridCell

- (id)initWithReuseIdentifier:(NSString *)identifier
{
    if (self = [super initWithReuseIdentifier:identifier]) {
        // Add a label
        _label = [[UILabel alloc] init];
        _label.font = [UIFont systemFontOfSize:15];
        [self addSubview:_label];
        
        // Create a PickerViewController ready to display when in edit mode
        _pickerViewController = [[PickerViewController alloc] init];
        _pickerViewController.delegate = self;
    }
    return self;
}

Here we create a UILabel to display the selected value, changing its font to match the other cells in our grid, and add it to the current view. We also create a PickerViewController instance, and set ourself as its delegate.

Next, we need to override the setters for the values and selectedIndex properties, so we can pass the new values to the picker view controller:

- (void) setValues:(NSArray*)values
{
    // Sets the list of possible values for the cell
    _values = values;
    // Pass the list to the PickerViewController
    [_pickerViewController setValues:values];
}

- (void) setSelectedIndex:(int)selectedIndex
{
    // Check the index is in the bounds of our values array
    if ([_values count] > selectedIndex)
    {
        // Set the selected index, and pass it to the PickerViewController
        _selectedIndex = selectedIndex;
        _pickerViewController.selectedIndex = selectedIndex;
    
        // Update the displayed text with the new value
        _label.text = [_values[selectedIndex] description];
    }
}

When updating the selected index, we also update the label, to get the relevant display value from our list of values.

We also need to override setFrame, so that when the grid sets the cell’s frame, the label is displayed correctly inside it.

- (void) setFrame:(CGRect)frame
{
    [super setFrame:frame];
    // Set up the label's frame so it's inset by 20px from left/right and 10px from top/bottom
    _label.frame = CGRectMake(20, 10, self.bounds.size.width-40, self.bounds.size.height-20);
}

Responding to edit requests

Now, we need to work out how and when to put the cell in edit mode. We could respond to a double tap event on the cell, but that wouldn’t cater for the cases when the grid has been set up to edit on a single tap. What we really want to do is to hook into the grid’s edit event, so that our cell will behave in the right way no matter how the grid’s events have been set up.

Luckily there’s a protocol for this, which SDataGridCell already conforms to: the SGridEventResponder protocol. It has a method respondToEditEvent which is called  whenever the grid’s current edit gesture is made on a cell. So if we override respondToEditEvent method to open up our popover and display the picker view, it will automatically be displayed at the right time.

Here’s what the overridden method looks like:

// Called when the grid's edit event is triggered on this cell
- (void) respondToEditEvent {
    // We need to call the grid's delegate methods for editing cells before doing any more
    
    // Call the shouldBeginEditingCellAtCoordinate method on the grid's delegate (if the method exists)
    if ([self.dataGrid.delegate respondsToSelector:@selector(shinobiDataGrid:shouldBeginEditingCellAtCoordinate:)]) {
        if([self.dataGrid.delegate shinobiDataGrid:self.dataGrid shouldBeginEditingCellAtCoordinate:self.coordinate] == NO) {
            return;
        }
    }
    
    // Call the willBeginEditingCellAtCoordinate method on the grid's delegate (if the method exists)
    if ([self.dataGrid.delegate respondsToSelector:@selector(shinobiDataGrid:willBeginEditingCellAtCoordinate:)]) {
        [self.dataGrid.delegate shinobiDataGrid:self.dataGrid willBeginEditingCellAtCoordinate:self.coordinate];
    }
    
    // Finally create and display the popover
    _popover = [[UIPopoverController alloc] initWithContentViewController:_pickerViewController];
    [_popover presentPopoverFromRect:self.bounds inView:self permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

The first couple of sections deal with the data grid’s delegate methods, first checking whether the cell should be edited, and then calling the pre-edit delegate method willBeginEditingCellAtCoordinate. (We need to do this groundwork here because SDataGridCell which we’re subclassing isn’t editable by default.)

The final section actually displays our popover and its picker view. We just create a new popover controller, passing it our custom view controller, and tell it to present itself on the edge of the cell.

Responding to a new selection

The final step is to implement the PickerDelegate method didSelectRowAtIndex, so that when a user selects a new item in the picker view, we can update the cell’s contents, make the popover disappear, and call the relevant method on the data grid’s delegate. Here’s what the method looks like:

#pragma mark PickerDelegate methods

// Called when the a new value has been selected in the UIPickerView
-(void)didSelectRowAtIndex:(int)newIndex {
    // Set the new index value
    [self setSelectedIndex:newIndex];
    
    // Dismiss the popover
    [_popover dismissPopoverAnimated:YES];
    _popover = nil;
    
    // Call the didFinishEditingCellAtCoordinate method on the grid's delegate (if the method exists)
    if ([self.dataGrid.delegate respondsToSelector:@selector(shinobiDataGrid:didFinishEditingCellAtCoordinate:)]) {
        [self.dataGrid.delegate shinobiDataGrid:self.dataGrid didFinishEditingCellAtCoordinate:self.coordinate];
    }
}

The first line here just updates our cell’s selected index to the new value, which will then update the label to match. Next, we dismiss the popover. Finally, because we’ve finished editing, we call the shinobiDataGrid:didFinishEditingCellAtCoordinate: method on the grid’s delegate.

Putting it all together

The final stage in this process is to start using our custom cell. First, when creating the title column in viewDidLoad in ViewController.m, we need set the cell type. (Don’t forget to import its headers too!)

...
// Add a title column, with the custom PickerCell type
SDataGridColumn* titleColumn = [[SDataGridColumn alloc] initWithTitle:@"Title" forProperty:@"title"];
titleColumn.editable = YES;
titleColumn.cellType = [PickerCell class];
[self.shinobiDataGrid addColumn:titleColumn];
...

Note that we’ve also reverted to using the title property rather than displayTitle.

Next, because we need to provide the picker cell with a list of valid titles when it’s created, we need to make use of another protocol, SDataGridDataSourceHelperDelegate. Add the protocol in ViewController.h:

@interface ViewController : UIViewController<SDataGridDelegate, SDataGridDataSourceHelperDelegate>

And set ourselves as the data source helper’s delegate inside viewDidLoad in ViewController.m:

...
// Create the data source helper and set its delegate and data
_datasourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:self.shinobiDataGrid];
_datasourceHelper.delegate = self;
_datasourceHelper.data = _data;
...

Then, add the following method:

#pragma mark - SDataGridDataSourceHelperDelegate methods

// Called when the data source helper is populating a cell
- (BOOL)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper populateCell:(SDataGridCell *)cell withValue:(id)value forProperty:(NSString *)propertyKey sourceObject:(id)object
{
    if ([propertyKey isEqualToString:@"title"])
    {
        // Create a picker cell to display the title property
        PickerCell* pickerCell = (PickerCell*)cell;
        pickerCell.dataGrid = self.shinobiDataGrid;
        pickerCell.values = [PersonDataObject titleDisplayNames];
        pickerCell.selectedIndex = [value integerValue];
        
        return YES;
    }
    
    // Return 'NO' so that the datasource helper populates all the other cells in the grid.
    return NO;
}

This method is called whenever the data source helper populates a cell. If it’s populating a title column, we cast the cell provided to a PickerCell (the cell will have been created as a PickerCell because we set the cell type on the column), set its dataGrid, values, and selectedIndex, and return YES (to say we’ve populated the cell). For other columns, we return NO so that the datasource helper will populate the column in the usual way.

The final final step (I promise!) is to modify shinobiDataGrid:didFinishEditingCellAtCoordinate: to handle edits from our PickerCell. We need to refactor our previous version a bit to cope with the different cell types, so modify the method to this:

// Called when a cell within the ShinobiDataGrid object has been edited.
- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didFinishEditingCellAtCoordinate:(SDataGridCoord *)coordinate
{
    // Find the cell that was edited (all our cells are SDataGridTextCells)
    SDataGridCell* cell = (SDataGridCell*)[self.shinobiDataGrid visibleCellAtCoordinate:coordinate];
    
    // Locate the 'model' object for this row
    PersonDataObject* person = _data[coordinate.row.rowIndex];
    
    // Determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"Title"])
    {
        // The title column uses our custom picker cell
        PickerCell* pickerCell = (PickerCell*) cell;
        
        // Retrieve the selected index from the picker cell, which will map directly to a PersonTitle
        int title = pickerCell.selectedIndex;
        
        if (PersonDataObjectIsValidTitle(title)) {
            person.title = pickerCell.selectedIndex;
        } else {
            pickerCell.selectedIndex = person.title;
        }
    } else {
        SDataGridTextCell *textCell = (SDataGridTextCell*) cell;
        
        if ([cell.coordinate.column.title isEqualToString:@"Forename"])
        {
            person.forename = textCell.textField.text;
        }
        if ([cell.coordinate.column.title isEqualToString:@"Surname"])
        {
            person.surname = textCell.textField.text;
        }
    }
    
}

So now, in the case of a title cell, we cast the cell to a PickerCell so we can get its selected index, then set that title on our person object if it’s valid. If it’s not valid (which shouldn’t happen, but better to be safe than sorry), we revert the cell’s value to the previous title.

Summing it up

If you run the app now, you can double-tap on a cell in the “Title” column, and a picker will appear for you to select the person’s new title:

 Screenshot03.png

You can check that we’ve hooked this up to the grid’s edit events properly by changing the grid’s singleTapEventMask in viewDidLoad:

self.shinobiDataGrid.singleTapEventMask = SDataGridEventEdit;

Re-running the app now, you can single tap on a title cell to display the popover.

So, now you’ve seen how to creating a custom editable cell in ShinobiGrids. The finished project is in GitHub – please feel free to fork it and create your own custom editable cell types!

Back to Blog