Overview

This document provides an overview of the ShinobiDataGrid control. It describes the features of the control and its associated concepts, together with some step-by-step guides for achieving common tasks.

The ShinobiDataGrid provides a powerful and flexible way to display tabular data within your application. The data-grid is composed of rows which run horizontally and columns which run vertically. In a typical usage each row will represent an object or data-item within your application while each column will represent some property of these objects. For example, you might use a data-grid to render a collection of Person objects, where the columns of the data-grid represent the properties of these objects, such as age and name.

The data-grid makes it easy to perform a number of actions on the items rendered within the grid, such as grouping into sections, dragging rows to re-order, selection and sorting. These topics are covered in detail in the various step-by-step guides listed below.

If you simply want to get up and running, follow the Quick Start Guide - Objective-C, or the Quick Start Guide - Swift. Alternatively, for a more detailed description of how the grid works and the features it presents, head over to the ShinobiDataGrid Control Overview. Finally, for guides that tackle specific usage scenarios, head on over to the ShinobiDataGrid How-to Guides.

The data-grid has a complete set of Xamarin.iOS bindings, allowing you to make use of all of its features from within applications written in C#. In order to get up and running, follow the Quick Start Guide for Xamarin iOS.

Quick Start Guide

Introduction

The following guide will get you up-and-running with the ShinobiDataGrid as quickly as possible. In this guide we’ll cover initial project set-up, how to specify a datasource and some simple styling. The end result will be a grid that displays a list of people as shown below. You can follow along with the related code sample: GettingStarted.xcodeproj.

Installation

ShinobiGrids now ships with an installer, to make it easier to get started. To find the installer open the ‘ShinobiGrids.dmg’ file which you downloaded from ShinobiControls and look for ‘install.pkg’.

To install the ShinobiGrids framework and documentation run the ‘install.pkg’ file. This will install the framework and documentation into Xcode for you. This means you can add the framework to your project in the same way as you would any of the frameworks which are automatically shipped with Xcode.

If you don’t want to run the installer, the framework is also contained within the ‘ShinobiGrids’ folder in the disk image. Regardless of whether you ran the installer, you should copy this folder onto your machine. Drag the ‘ShinobiGrids’ folder onto the Desktop icon in the disk image. This will copy the folder onto your desktop.

The ‘ShinobiGrids’ folder contains:

  • A copy of the framework.
  • A copy of the documentation for the framework.
  • A set of samples to demonstrate getting started with ShinobiGrids.
  • An uninstall script for uninstalling the ShinobiGrids framework and documentation from Xcode.
  • The Xamarin.ios version of the framework.
  • A README file with setup steps.
  • A change log stating the changes made in each release.
  • A copy of the ShinobiGrids Licence.
  • A text file containing the version number of the framework.

Creating the basic grid

Start-up Xcode and create a new project via File / New / Single View Application.

Within your newly created project add a reference to the ShinobiGrids framework. The easiest way to do this is to locate the folder where you installed the framework, locate the ShinobiGrid.framework and drag it directly into your project.

ShinobiGrids makes use of a few other frameworks, so add the following as well: - QuartzCore framework - libc++.tbd (Trial version only)

Open up ViewController.h and import the ShinobiGrids.h file:

#import <ShinobiGrids/ShinobiGrids.h>

@interface ViewController : UIViewController 

@end

Next open up ViewController.m and add the following code:

@implementation ViewController
{
    ShinobiDataGrid* _shinobiDataGrid;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    ShinobiGrids.trialKey = @"your trial key"; // TODO: add your trial key here!

    // create a grid - with a 40 point padding
    _shinobiDataGrid = [[ShinobiDataGrid alloc] initWithFrame:CGRectInset(self.view.bounds, 40,40)];

    // add to the view
    [self.view addSubview:_shinobiDataGrid];
}

In the above code, within the viewDidLoad method, an instance of a ShinobiDataGrid is created and assigned to the _shinobiDataGrid ivar (instance variable). The frame of the data-grid is inset a few points within the view bounds, which means it will occupy the entire view, with a small margin. Finally, the data-grid is added as a sub-view of the view controller.

If you have downloaded a trial version of the ShinobiGrids you will have been issued with a trial key. Add the key that you were supplied with at the location indicated above.

Creating some data

The next step is to create some data for the grid to render. Typically this data will be a collection of ‘data objects’ that are used within your application, this could be a collection of emails, contacts, flight details … all sorts of things!

For the purposes of this quick-start guide we’ll create some dummy data.

Control-click the project and select the New File … option, selecting the iOS\Cocoa Touch\Objective-C class template. Name the class PersonDataObject and make it a subclass of NSObject.

Open up the newly created PersonDataObject.h file and add the following:

@interface PersonDataObject : NSObject

@property (retain, nonatomic) NSString* name;
@property (retain, nonatomic) NSNumber* age;

+ (PersonDataObject*) personWithName:(NSString*)name age:(NSNumber*)age;

@end

This defines name and age properties together with a ‘factory’ method that makes it easier to create instances of this class. Open up PersonDataObject.m and add the following:

@implementation PersonDataObject

+ (PersonDataObject*) personWithName:(NSString *)name age:(NSNumber *)age
{
    PersonDataObject* obj = [[PersonDataObject alloc] init];
    obj.name = name;
    obj.age = age;
    return obj;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"%@ - %@", _name, _age];
}

@end

We are letting Xcode synthesize the two properties automatically. The implementation of the description method is just for convenience, it outputs the person’s name and age when an object is logged or printed in the output window.

Now that we have a suitable object, open up ViewController.m and import the header:

#import "PersonDataObject.h"

Then add an NSArray instance variable and a utility method to create the test data:

@implementation ViewController
{
   ShinobiDataGrid* _shinobiDataGrid;
    NSArray* _data;
}

- (void)createSomeTestData
{
    // create some data to populate the grid
    _data = @[[PersonDataObject personWithName:@"Leonardo" age:@45],
    [PersonDataObject personWithName:@"Michelangelo" age:@42],
    [PersonDataObject personWithName:@"Donatello" age:@47],
    [PersonDataObject personWithName:@"Raphael" age:@36]];
}

For the sake of brevity, the imaginatively named createSomeTestData method creates just 4 people. Within a real application you will probably have tens, hundreds or even thousands of items rendered within your grid.

Now that we have some data, it’s time to prepare the data-grid to display it …

Creating some columns

Before informing the data-grid of the data objects that are being rendering, you need to tell the grid how to render this data. The data-grid is composed of rows which run vertically and columns which run horizontally. Typically each column will represent a property of the data object being rendered within the grid.

The PersonDataObject has two properties, age and name, so we’ll go ahead and add a column for each property.

Open up ViewController.m and at the end of the viewDidLoad method add a couple of columns:

// add a name column
SDataGridColumn* nameColumn = [[SDataGridColumn alloc] initWithTitle:@"Name"];
nameColumn.width = @484;
[_shinobiDataGrid addColumn:nameColumn];

// add an age column
SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"Age"];
ageColumn.width = @200;
[_shinobiDataGrid addColumn:ageColumn];

The above code adds two columns to the grid, one for rendering the person’s age and the other for their name.

The data-grid uses a concept called virtualization for rendering the data objects. In order to minimize memory usage, the grid only creates cells for the rows which are currently visible. As the user scrolls the grid, the cells which move out of view are re-cycled, i.e. they are re-used for the rows that have now become visible.

This re-cycling of cells all happens automatically. All you have to do is specify the ‘type’ of cell that you would like the data-grid to create for cells in each column.

Within this example the person’s name and age are going to be rendered as ‘text’ , which is why an SDataGridTextCell is used.

The columns are a very important component of the data-grid, as well as being used to specify cell types, you can also specify the sort behaviour, styles, width and all kinds of other properties.

Now that the data-grid is fully configured it is time to add the data …

Adding a Datasource

In order to render your data within the data-grid you need to supply a ‘datasource’, this is a class that adopts the SDataGridDataSource protocol methods.

Open up ViewController.h and adopt the SDataGridDataSource protocol:

@interface ViewController : UIViewController SDataGridDataSource

You can use any class you like as the datasource, however, here we are just using the view controller for simplicity.

The SDataGridDataSource protocol defines two mandatory methods that must be implemented. We’ll go ahead and add them now. Open up ViewController.m and add the following:

-(NSUInteger)shinobiDataGrid:(ShinobiDataGrid *)grid numberOfRowsInSection:(NSInteger) sectionIndex
{
    return _data.count;
}

This method informs the grid of the number of rows in each section. The data-grid allows you to group items within sections, for example, you might want to group a large list of people objects by the first letter of their name. In this example, we have a ‘flat’ list without sections, so the above code simply returns the number of items in the array. Next add the following:

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid prepareCellForDisplay:(SDataGridCell *)cell
{
    // both columns use a SDataGridTextCell, so we are safe to perform this cast
    SDataGridTextCell* textCell = (SDataGridTextCell*)cell;

    // locate the person that is rendered on this row
    PersonDataObject* person = _data[cell.coordinate.row.rowIndex];

    // determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"Name"])
    {
        // render the name in the 'name' column
        textCell.textField.text = person.name;
    }
    if ([cell.coordinate.column.title isEqualToString:@"Age"])
    {
        // render the age in the 'age' column
        textCell.textField.text = [person.age stringValue];
    }
}

As mentioned in the previous steps, each grid declares the type of cell that the grid should create for the cells within the column. The data-grid creates the required cells then invokes the above method on the datasource so that it can prepare the cell for display.

The cell has a coordinate property which indicates its location within the grid. The above code uses the coordinate.row.rowIndex to locate the data object that this row represents. Following that, the coordinate.column.title is used to identify the column for this cell, and the name or age property value used accordingly.

With these two methods in place, the final step is to set the data-grid datasource. At the end of viewDidLoad add the following:

_shinobiDataGrid.dataSource = self;

If you build and run, you now have a fully functioning data-grid:

If you got stuck at any point, take a look at our related code sample: GettingStarted.xcodeproj

Now that you have created a basic data-grid sample, why not read about selection or sorting? There is a whole lot more you can do with your data now that it is in a data-grid!

Quick Start Guide - Swift

Introduction

The following guide will get you up-and-running with the ShinobiDataGrid as quickly as possible using Swift. In this guide we’ll cover initial project set-up, how to specify a datasource and some simple styling. The end result will be a grid that displays a list of people as shown below. You can follow along with the related code sample: GettingStartedSwift.xcodeproj.

Installation

ShinobiGrids now ships with an installer, to make it easier to get started. To find the installer open the ‘ShinobiGrids.dmg’ file which you downloaded from ShinobiControls and look for ‘install.pkg’.

To install the ShinobiGrids framework and documentation run the ‘install.pkg’ file. This will install the framework and documentation into Xcode for you. This means you can add the framework to your project in the same way as you would any of the frameworks which are automatically shipped with Xcode.

If you don’t want to run the installer, the framework is also contained within the ‘ShinobiGrids’ folder in the disk image. Regardless of whether you ran the installer, you should copy this folder onto your machine. Drag the ‘ShinobiGrids’ folder onto the Desktop icon in the disk image. This will copy the folder onto your desktop.

The ‘ShinobiGrids’ folder contains:

  • A copy of the framework.
  • A copy of the documentation for the framework.
  • A set of samples to demonstrate getting started with ShinobiGrids.
  • An uninstall script for uninstalling the ShinobiGrids framework and documentation from Xcode.
  • The Xamarin.ios version of the framework.
  • A README file with setup steps.
  • A change log stating the changes made in each release.
  • A copy of the ShinobiGrids Licence.
  • A text file containing the version number of the framework.

Setting up the project

Start-up Xcode and create a new project via File / New / Single View Application. Be sure to select “Swift” as the language for your new project.

Within your newly created project add a reference to the ShinobiGrids framework. The easiest way to do this is to locate the folder where you installed the framework, locate the ShinobiGrid.framework and drag it directly into your project.

ShinobiGrids makes use of a few other frameworks, so add the following as well: - QuartzCore framework - libc++.tbd (Trial version only)

Linking to the bridging header file

In order for Xcode to use an Objective-C based framework in Swift, it needs to have a bridging header file. We have included a bridging header file in our framework. To link to it, you must open the build settings for the your new target, and search for the Objective-C Bridging Header setting. You must then provide the path to the ShinobiDataGrid-Bridging-Header.h file, which is inside the Headers directory of the ShinobiGrids.framework.

Setting the Swift project's bridging header file

In the screenshot above, the framework is three directories above the root of the project, hence it is set to

$(SRCROOT)/../../../ShinobiGrids.framework/Headers/ShinobiDataGrid-Bridging-Header.h

This path will vary based on the location of the framework on your file system.

Adding a Grid

Open up ViewController.swift and add the following code:

override func viewDidLoad() {
    super.viewDidLoad()

    ShinobiDataGrids.trialKey = "" // TODO: add your trial key here!
    let grid = ShinobiDataGrid(frame: self.view.bounds)
    self.view.addSubview(grid);
}

In the above code, within the viewDidLoad method, an instance of a ShinobiDataGrid is created and is added as a sub-view of the view controller.

If you have downloaded a trial version of the ShinobiGrids you will have been issued with a trial key. Add the key that you were supplied with at the location indicated above.

Creating some data

The next step is to create some data for the grid to render. Typically this data will be a collection of ‘data objects’ that are used within your application, this could be a collection of emails, contacts, flight details … all sorts of things!

For the purposes of this quick-start guide we’ll create some dummy data. Add a new Swift file named Person to the project.

Open up the newly created Person.swift file and add the following to define a Person class and some name properties.

class Person : NSObject {
    var firstName: String = ""
    var lastName: String = ""
}

Then add a property to the ViewController class:

var data : Array<Person> = PersonDataGenerator.generatePeople(50)

PersonDataGenerator is another Swift class that has a class method generatePeople, that takes an Int representing the number of people that it should return. For more information on this class, take a look at the GettingStartedSwift project in the samples.

Now that we have some data, it’s time to prepare the data-grid to display it …

Creating some columns

Before informing the data-grid of the data objects that are being rendering, you need to tell the grid how to render this data. The data-grid is composed of rows which run vertically and columns which run horizontally. Typically each column will represent a property of the data object being rendered within the grid.

The Person class has two properties, first and last and name, so we’ll go ahead and add a column for each property.

Open up ViewController.swift and at the end of the viewDidLoad method, add a couple of columns:

let firstNameColumn = SDataGridColumn(title: "First Name")
grid.addColumn(firstNameColumn)

let lastNameColumn = SDataGridColumn(title: "Last Name")
grid.addColumn(lastNameColumn)

The above code adds two columns to the grid, one for rendering the person’s first name, and one for rendering their last name.

The data-grid uses a concept called virtualization for rendering the data objects. In order to minimize memory usage, the grid only creates cells for the rows which are currently visible. As the user scrolls the grid, the cells which move out of view are re-cycled, i.e. they are re-used for the rows that have now become visible.

This re-cycling of cells all happens automatically. All you have to do is specify the ‘type’ of cell that you would like the data-grid to create for cells in each column.

Within this example the person’s name and age are going to be rendered as ‘text’ , which is why an SDataGridTextCell is used.

The columns are a very important component of the data-grid, as well as being used to specify cell types, you can also specify the sort behaviour, styles, width and all kinds of other properties.

Now that the data-grid is fully configured it is time to add the data …

Adding a Datasource

In order to render your data within the data-grid you need to supply a ‘datasource’, this is a class that adopts the SDataGridDataSource protocol methods.

Open up ViewController.swift and adopt the SDataGridDataSource protocol:

class ViewController: UIViewController, SDataGridDataSource {

You can use any class you like as the datasource, however, here we are just using the view controller for simplicity.

The SDataGridDataSource protocol defines two mandatory methods that must be implemented. We’ll go ahead and add them now.

Add the following to ViewController.m:

func shinobiDataGrid(grid: ShinobiDataGrid!, numberOfRowsInSection sectionIndex: Int) -> Int  {
    return data.count;
}

This method informs the grid of the number of rows in each section. The data-grid allows you to group items within sections, for example, you might want to group a large list of people objects by the first letter of their name. In this example, we have a ‘flat’ list without sections, so the above code simply returns the number of items in the array.

Next, add the following:

func shinobiDataGrid(grid: ShinobiDataGrid!, prepareCellForDisplay cell: SDataGridCell!) {
    let person = data[cell.coordinate.row.rowIndex]

    let textCell = cell as SDataGridTextCell

    if textCell.coordinate.column.title == "First Name" {
        textCell.textField.text = person.firstName
    }
    else {
        textCell.textField.text = person.lastName
    }
}

As mentioned in the previous steps, each grid declares the type of cell that the grid should create for the cells within the column. The data-grid creates the required cells then invokes the above method on the datasource so that it can prepare the cell for display.

The cell has a coordinate property which indicates its location within the grid. The above code uses the coordinate.row.rowIndex to locate the data object that this row represents. Following that, the coordinate.column.title is used to identify the column for this cell, and the first or last name property value is used accordingly.

With these two methods in place, the final step is to set the data-grid datasource. Add the following just after you create your grid:

 grid.dataSource = self;

If you build and run, you now have a fully functioning data-grid:

If you got stuck at any point, take a look at our related code sample: GettingStartedSwift.xcodeproj

Now that you have created a basic data-grid sample, why not read about selection or sorting? There is a whole lot more you can do with your data now that it is in a data-grid!

Quick Start Guide for Xamarin.iOS

Introduction

This Quick Start Guide is intended for users of Xamarin.iOS and leads you through exactly the same steps as the Objective-C Quick Start Guide. If you are an Objective-C / Xcode developer you can safely ignore this section!

The following guide will get you up-and-running with the ShinobiDataGrid as quickly as possible. In this guide we’ll cover initial project set-up, how to specify a datasource and some simple styling. The end result will be a grid that displays a list of people as shown below:

Creating the basic grid

Start-up Xamarin Studio and create a new solution via File / New / Soloution… – selecting the C# / iOS / iPad / Single View Application template.

Within your newly created project add a reference to the ShinobiGrids framework. The easiest way to do this is to right-click the References folder in your project, selecting Edit References …. The data-grid is distributed as an assembly, so select the .NET Assembly tab and navigate to the grids folder and add a reference to ShinobiGrids.dll.

Open up GettingStartedViewController.cs and add a using statement for the ShinobiGrid namespace:

using ShinobiGrids;

Add a the following member variable to the view controller:

private ShinobiDataGrid _shinobiDataGrid;

Next navigate to the ViewDidLoad method and replace with the following:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    ShinobiDataGrids.trialKey = ""; //TODO: add your trial key here!

    // Perform any additional setup after loading the view, typically from a nib.

    RectangleF frame = new RectangleF(40, 40, this.View.Bounds.Width - 80, this.View.Bounds.Height - 90);
    _shinobiDataGrid = new ShinobiDataGrid(frame);

    // add to the view
    View.AddSubview(_shinobiDataGrid);
}

In the above code, within the ViewDidLoad method, an instance of the ShinobiDataGrid is created and assigned to the _shinobiDataGrid member variable. The frame of the data-grid is inset a few points within the view bounds, which means it will occupy the entire view, with a small margin. Finally, the data-grid is added as a sub-view of the view controller.

If you have downloaded a trial version of the ShinobiGrids you will have been issued with a trial key. Add the key that you were supplied with at the location indicated above.

Creating some data

The next step is to create some data for the grid to render. Typically this data will be a collection of ‘data objects’ that are used within your application, this could be a collection of emails, contacts, flight details … all sorts of things!

For the purposes of this quick-start guide we’ll create some dummy data.

Right-click the project and select the Add / New File … option, selecting the Empty Class option and naming the class PersonDataObject.

Add a couple of properties and a constructor to this class:

public class PersonDataObject
{
    public PersonDataObject (NSInteger age, string name)
    {
        Age = age;
        Name = name;
    }

    public NSInteger Age { get; private set;}

    public string Name { get; private set;}

}

Now that we have a suitable object, open up GettingStartedViewController and import the generic collections namespace:

using System.Collections.Generic;

Then add an array member variable and a utility method to create the test data:

private List<PersonDataObject> _data;

private void CreateSomeTestData()
{
    _data = new List<PersonDataObject>() {
        new PersonDataObject(45, "Leonardo"),
        new PersonDataObject(82, "Michelangelo"),
        new PersonDataObject(66, "Donatello"),
        new PersonDataObject(33, "Raphael")
    };
}

For the sake of brevity, the imaginatively named CreateSomeTestData method creates just 4 people. Within a real application you will probably have tens, hundreds or even thousands of items rendered within your grid.

Now that we have some data, it’s time to prepare the data-grid to display it …

Creating some columns

Before informing the data-grid of the data objects that are being rendering, you need to tell the grid how to render this data. The data-grid is composed of rows which run vertically and columns which run horizontally. Typically each column will represent a property of the data object being rendered within the grid.

The PersonDataObject has two properties, Age and Name, so we’ll go ahead and add a column for each property.

Open up GettingStartedViewController and at the end of the ViewDidLoad method add a couple of columns:

// add a name column
SDataGridColumn nameColumn = new SDataGridColumn("Name");
nameColumn.Width = 484;
_shinobiDataGrid.AddColumn(nameColumn);

// add an age column
SDataGridColumn ageColumn = new SDataGridColumn("Age");
ageColumn.Width = 200;
_shinobiDataGrid.AddColumn(ageColumn);

The above code adds two columns to the grid, one for rendering the person’s age and the other for their name.

The data-grid uses a concept called virtualization for rendering the data objects. In order to minimize memory usage, the grid only creates cells for the rows which are currently visible. As the user scrolls the grid, the cells which move out of view are re-cycled, i.e. they are re-used for the rows that have now become visible.

This re-cycling of cells all happens automatically. All you have to do is specify the ‘type’ of cell that you would like the data-grid to create for cells in each column.

Within this example the person’s name and age are going to be rendered as ‘text’ , which is why an SDataGridTextCell is used.

The columns are a very important component of the data-grid, as well as being used to specify cell types, you can also specify the sort behaviour, styles, width and all kinds of other properties.

Now that the data-grid is fully configured it is time to add the data …

Adding a Datasource

In order to render your data within the data-grid you need to supply a ‘datasource’, this is a class that extends the SDataGridDataSource abstract base class.

Right-click the project and select the Add / New File … option, selecting the Empty Class option, naming the class PersonDataSource. This class will be responsible for responding to the data-grid’s requests for data.

Change the superclass to be SDataGridDataSource and add a constructor which is used to provide your data to the datasource:

using ShinobiGrids;

...

public class PersonDataSource : SDataGridDataSource
{
    private List<PersonDataObject> _data;

    public PersonDataSource(List<PersonDataObject> data)
    {
        _data = data;
    }

}

The SDataGridDataSource protocol defines two abstract methods that must be implemented. We’ll go ahead and add them now.

Add an implementation for GetNumberOfRowsInSection:

protected override uint GetNumberOfRowsInSection (ShinobiDataGrid grid, NSInteger sectionIndex)
{
    return (uint)_data.Count;
}

This method informs the grid of the number of rows in each section. The data-grid allows you to group items within sections, for example, you might want to group a large list of people objects by the first letter of their name. In this example, we have a ‘flat’ list without sections, so the above code simply returns the number of items in the array.

Next add the following:

protected override void PrepareCellForDisplay (ShinobiDataGrid grid, SDataGridCell cell)
{
    SDataGridTextCell textCell = (SDataGridTextCell)cell;

    // locate the person that is rendered on this row
    PersonDataObject person = _data[cell.Coordinate.Row.Index];

    // determine which column this cell belongs to
    if (cell.Coordinate.Column.Title == "Name")
    {
        // render the name in the 'name' column
        textCell.TextField.Text = person.Name;
    }
    if (cell.Coordinate.Column.Title == "Age")
    {
        // render the age in the 'age' column
        textCell.TextField.Text = person.Age.ToString();
    }
}

As mentioned in the previous steps, each grid declares the type of cell that the grid should create for the cells within the column. The data-grid creates the required cells then invokes the above method on the datasource so that it can prepare the cell for display.

The cell has a coordinate property which indicates its location within the grid. The above code uses the cell.Coordinate.Row.Index to locate the data object that this row represents. Following that, the cell.Coordinate.Column.Title is used to identify the column for this cell, and the name or age property value used accordingly.

With these two methods in place, the final step is to set the data-grid datasource. At the end of viewDidLoad add the following:

CreateSomeTestData();
_shinobiDataGrid.DataSource = new PersonDataSource(_data);

If you build and run, you now have a fully functioning data-grid:

Now that you have created a basic data-grid sample, why not read about selection or sorting? There is a whole lot more you can do with your data now that it is in a data-grid!

Shinobi DataGrid Control Overview

The ShinobiDataGrid provides a powerful and flexible way to display tabular data within your application. The data-grid makes it easy to perform a number of actions on the items rendered within the grid, such as grouping into sections, dragging rows to re-order, selection and sorting.

This section describes the features and concepts behind the data-grid.

The anatomy of the data-grid

An annotated data-grid is shown below, with the key UI features highlighted:

The data-grid is composed of rows which run horizontally and columns which run vertically.

Each column has a header, which typically indicates the nature of the data displayed within that column. The column header not only indicates the type of data that it displays, it also allow the user to interact with the column, allowing them to sort the data that it contains, drag to re-order or pinch to resize.

The data-grid can optionally display grouped data within a number of sections. Each section has a header, which spans the entire width of the data-grid, and a number of rows. This can be used, for example, to group emails by date, or perhaps contacts by the first letter of their name.

In order to use the data-grid you will typically perform the following tasks:

  • Add some columns to the data-grid – these define the columns that are rendered by the data-grid at runtime.
  • Set the data-grid dataSource property – this is used to provide data to the grid.
  • (Optionally) Set the data-grid delegate property – this is used to respond to a user’s interactions with the data-grid.

Cell Creation and Recycling

The data-grid is capable of rendering grids that are composed of thousands of rows. One of the key concepts behind the data-grid that supports this is that way that it dynamically creates the various elements of the UI, pooling and recycling these elements as required.

The data-grid only creates the elements that it requires to render the data that is currently visible on the device’s screen. As an example, when the user scrolls the grid vertically, the cells that scroll beyond the upper bounds of the screen are not destroyed, instead these cells are re-positioned at the bottom of the scrolling container so that the come into view once more, as illustrated below:

The same principle applies to vertical or diagonal scrolling.

The data-grid handles the process of polling and recycling cells, and with typical data-grid usages you don’t need to be concerned with the way that cells are recycled. However, you should keep this process in mind – if for example, you make changes to the state of a visible cell; you cannot expect the state to be the same if the cell is scrolled off-screen then back on again. Indeed, you cannot expect it to be the same cell!

Column Configuration

In order to render your data within the data-grid you need to add one or more columns, which are defined by the SDataGridColumn class. One of the most important functions of the columns is to specify the type of cells which are used. A cell must be a subclass of SDataGridCell, which provides the basic functionality required by the data-grid for pooling, and other ‘core’ features such as selection.

Each column also specifies the header text which is displayed at the top of each column. You can also specify the type of cell used for the header by setting the headerCellType property.

A user can sort the rows of the data-grid by tapping on the column headers. The sort behaviour is specified on a per-column basis by setting the sortMode property. This can have the following values:

  • SDataGridSortModeNone – This indicates that the data-grid cannot be sorted by the data within this column.
  • SDataGridSortModeBiState – This indicates that the data-grid can be sorted by the data within this column. When a column is in bi-state sort mode it toggles between an ascending and descending sort with each tap.
  • SDataGridSortModeTriState – This indicates that the data-grid can be sorted by the data within this column. When a column is in tri-state sort mode it iterates through the following sort orders {ascending, descending, none}. A tri-state sort mode can be used for data that has a ‘natural’ sort order, the user to sort the data-grid ascending or descending, then return it to its original sort order.

The current sort order for a column can be retrieved from its sortOrder property. This can also be used to programmatically set the sort state of the data-grid.

Column sorting is mutually exclusive, in other words, when the user taps to sort by a column, and other columns that are currently sorted have their sortOrder set to none.

Changes in sort order can be detected by providing a delegate to the data-grid, as described in detail later.

The datasource

In order to render data within the data-grid you need to supply a ‘source’ of data. When the data-grid is used to render a large number of rows, it only needs to be informed of the contents of the visible cell. For this reason the datasource protocol requests data on a cell-by-cell basis as the data is required.

You supply the object that acts as the datasource to the data-grid by setting the dataSource property. This object must adopt the SDataGridDataSource protocol.

The SDataGridDataSource protocol has two methods which must be implemented, these are shinobiDataGrid:numberOfRowsInSection: which informs the grid of how many rows there are in each section. If the data-grid is rendering data that is not grouped, then the implementation of this method simply returns the number of rows to be rendered in the data-grid.

The second mandatory method is shinobiDataGrid:prepareCellForDisplay:, this method is used by the datasource to ‘prepare’ the given cell for displaying within the grid. This involves setting the UI state of the cell to reflect that data that is being rendered in the given row and column.

Each cell has a coordinate property which indicates the column and row to which it belongs.

Each column defines the type of cell that it should create, therefore within your implementation of shinobiDataGrid:prepareCellForDisplay: you can safely cast the supplied cell based on the column that it belongs to. Following this you can set its state, for example, you might set the textField property of an SDataGridTextCell within this method.

The datasource has optional methods which allow you to render data grouped within sections. In order to render grouped data, you must implement the numberOfSectionsInShinobiDataGrid method, returning the number of sections that the grid should render. The shinobiDataGrid:numberOfRowsInSection: can then be used to inform the grid of the number of rows within each section.

Refreshing the data displayed in the grid

If the data which you are rendering changes, or you add / remove columns, you must inform the data-grid so that it can re-render itself. When you inform the data-grid it will re-query your data via the dataSource in order to pick up these changes.

The data-grid offers a number of different methods which allow you to specify the ‘granularity’ of an update. The simplest technique is to invoke reload on the data-grid which will cause it to throw away all of the cells that it is currently displaying and rebuild the grid entirely. Depending on the size of your grid, this can be a time consuming operation.

If only a number of rows or columns have changed, you can use the reloadRows or reloadColumns methods. This will ensure that only the cells for given rows / columns are re-rendered.

Finally, if you simply want to change the way in which a cell is rendered, but not the contents - i.e. if you want to change the style for a cell, you can use the refreshRows or refreshColumns methods. These will not invoke the dataSource methods but will invoke the delegate styling methods for the given cells.

Styling the data-grid

The data-grid has a powerful and flexible mechanism for customising its visual appearance. The style for each cell within the data-grid is specified via an SDataGridCellStyle object, which describes the various colours, font and text alignment for the cell.

The data-grid offers a number of ways to specify the style for cells, allowing you to, for example easily create a grid with alternating background colors for each row, or a grid where the values in a single column are highlighted in bold.

The style for each cell is determined by a process of style inheritance as illustrated in the diagram below:

The data-grid defaultCellStyleForRows applies to all of the cells in the grid (other than header cells and section headings). This is then combined with any style specified for individual columns, following this it is combined with the defaultCellStyleForAlternateRows, for rows with an odd row index. Finally, if a row is selected, the style is combined with the defaultCellStyleForSelectedRows.

When two styles are combined, the properties of each style are combined, with the property values of the style with a higher precedence taking priority. If the style property has a nil value, it is ignored.

As an example, if you want to create a grid where alternating rows have a light gray background color, and one column where all the cells are rendered in bold, this can be achieved as follows:

// alternating rows have a gray background
_shinobiDataGrid.defaultAlternateRowStyle.backgroundColor = [UIColor lightGrayColor];

// add an age column
SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"age"];

// render this column with a bold font
SDataGridCellStyle* boldStyle = [[SDataGridCellStyle alloc] init];
boldStyle.font = [UIFont boldSystemFontOfSize:_shinobiDataGrid.defaultCellStyleForRows.font.pointSize];
ageColumn.cellStyle = boldStyle;

[_shinobiDataGrid addColumn:ageColumn];

Notice that in the above code that the font from the style associated with the column is applied, even though the alternating row style has a higher priority, due to the font property of the alternating row style being nil.

When color style properties are combined, their alpha value is considered. Any color with an alpha of 1.0 will override the lower precedence color, however, an alpha of < 1.0 will result in a color blend. This can be used to create subtle effects where, for example, row and column styles are combined.

Themes

When a data-grid first loads, it will use a theme object to define its appearance. The theme sets the default style properties of each style property element to predefined values. There are four themes:

  • SDataGridLightTheme is brighter colors based on a white background
  • SDataGridDarkTheme is based on softer colors on a black background
  • SDataGridiOSTheme is based on the gradients and colors used in iOS 6
  • SDataGridiOS7Theme is based on the sharp and brightly coloured style used in iOS 7

By default, with no theme explicitly set, the data-grid will take its theme from the global ShinobiDataGrids object. This is determined by the iOS version of the device; with iOS6 (and lower) devices using the SDataGridiOSTheme and iOS7+ devices using SDataGridiOS7Theme. To set a theme that all of your data-grids universally adopt, you should set the theme on the ShinobiDataGrids object:

[ShinobiDataGrids setTheme:[SDataGridDarkTheme new]];

You should do this before any data-grids are created, as the data-grid will query this object when it is first initialized - the app delegate is a good location.

If you’d like to use a different theme to style each data-grid, you should use the applyTheme method on each instance:

[myDataGrid applyTheme:[SDataGridDarkTheme new]];

The above code will set all of the style object properties to match those specified by the darker theme. This will override any previously set values for the style objects! Be sure to make any direct customizations of the style objects after you call applyTheme.

The delegate

The shinobi data-grid has a delegate property which follows the standard UIKit pattern for notifying your application when the user interacts with the grid. In order to react to user interaction such as row drag or section collapse, you need to supply the data-grid with a class that adopts the SDataGridDataSource protocol.

For most user interactions there are a number of corresponding methods on the delegate protocol. These methods follow the convention:

  • should - delegate methods prefixed with ‘should’ are invoked before a user interaction is processed. The ‘should’ methods give you the opportunity to cancel an interaction. For example, you can se a ‘should’ method to cancel the selection of certain rows on your data-grid.
  • will - delegate methods prefixed with ‘will’ are invoked just before the interactions is processed. These are not cancellable. You typically implement a ‘will’ method if you need to capture the state of the data-grid just before a change occurs.
  • did - delegate methods prefixed with ‘did’ are invoked after the interaction has been processed. When the ‘did’ method is invoked the data-grid state will have changed to reflect the change due to the user interaction.

The datasource helper

The datasource helper provides an easy mechanism for rendering arrays of objects with the data-grid.

The shinobi data-grid does not place any restrictions on the format of your underlying data. You can render data that is stored in arrays of objects, dictionaries, JSON, XML, core-data - or any other format. To render your data you simply provide a datasource that populates cells on request.

This approach provides great flexibility, however, because the data-grid makes no assumptions about the ‘shape’ of your underlying data, it is not able to automatically sort or group your data. As a result, in order to provide sorting to your end-users, you must handle the sort delegate methods, sort your data, then inform the data-grid that the underlying data has changed. This is described in detail in the guide - How to: Sort the Shinobi DataGrid.

The datasource helper provides a simpler alternative to manually writing your own datasource and will automatically handle populating the grid, sorting, grouping, selection and row-reordering. In order to use the datasource helper you data must be held within an NSArray.

In order to use the datasource helper, you need to set the propertyKey for each column within the grid. This is typically done at the point of column construction:

// add a name column
SDataGridColumn* surnameColumn = [[SDataGridColumn alloc] initWithTitle:@"Surname" forProperty:@"surname"];
surnameColumn.sortMode = SDataGridColumnSortModeTriState;
[_shinobiDataGrid addColumn:surnameColumn];

// add a name column
SDataGridColumn* forenameColumn = [[SDataGridColumn alloc] initWithTitle:@"Forename" forProperty:@"forename"];
forenameColumn.sortMode = SDataGridColumnSortModeTriState;
[_shinobiDataGrid addColumn:forenameColumn];

The propertyKey is used by the datasource helper to determine which object property to render within a given column. The datasource helper uses NSObject:valueForKey, with the supplied propertyKey.

To use the datasource helper, initialize it by passing the data-grid that it will be associated with:

_datasourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:_shinobiDataGrid];

On initialisation the datasource helper will assign itself as the dataSource for the given data-grid.

Finally, provide some data to the datasource helper:

NSArray* myData = // create some data here!
_datasourceHelper.data = myData;

The datasource helper is able to automatically handle sorting, and row re-ordering. In order to group the data into sections, you must provide a groupedPropertyKey as shown below:

_datasourceHelper.groupedPropertyKey = @"surname";

The datasource helper also supports the concept of ‘virtual’ properties. You can provide a propertyKey, for a column or group, that does not exist on the objects within the supplied array. The values for this ‘virtual’ property are supplied via the SDataGridDataSourceHelperDelegate. For example, to group by the first letter of a surname property, you can do the following:

_datasourceHelper.groupedPropertyKey = @"surname-first-letter";

...

- (id)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper
         groupValueForProperty:(NSString *)propertyKey
              withSourceObject:(id)object
{
    // for the 'surname-first-letter' property, extract the first letter of the surname
    if ([propertyKey isEqualToString:@"surname-first-letter"])
    {
        PersonDataObject* person = (PersonDataObject*)object;
        return [person.surname substringToIndex:1];
    }

    // for any other property, return nil, which will result in the default behaviour being applied.
    return nil;
}

The delegate has two other methods similar to the one show above, one which allows you to customize how a property is rendered, allowing you to provide custom formatting, and the other which allows you to specify the value used to sort a property.

Pull to Action

To make refreshing your data-grid easier, we’ve included a Pull to Action control out of the box. All you need to do is turn it on:

_shinobiDataGrid.showPullToAction = YES;

By default the data-grid is the Pull to Action’s delegate and triggering the action will call reload on your data-grid. You can access the Pull to Action component via the pullToAction property on your data-grid, and modify it.

Shinobi DataGrid How-to Guides

This section provides a number of guides which provide detailed instructions, including code examples, for how to use the core features of the data-grid.

How to: Provide data to the Shinobi DataGrid

This how-to guide will show you the steps required to render your data within a Shinobi data-grid control.

In order to render data within the grid you need to do the following:

  1. Add a number of columns to the data-grid. Each column has a header at the top, under a number of cells arranged vertically beneath it.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.

The ShinobiDataGrid has a number of methods that allow you to add / remove columns. In the example below, a couple of columns are added to the data-grid via the addColumn method:

// add a name column
SDataGridColumn* nameColumn = [[SDataGridColumn alloc] initWithTitle:@"name"];
[_shinobiDataGrid addColumn:nameColumn];

// add an age column
SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"age"];
[_shinobiDataGrid addColumn:ageColumn];

The datasource is any class that implements the SDataGridDataSource protocol. For simple applications this is often the view controller that contains the data-grid, however you can use any class you like.

If you are rendering a grid without grouping, your datasource must implement the two mandatory SDataGridDataSource methods.

The first method informs the data-grid about how many rows to render. Because this grid is not rendering data which is grouped into sections, the sectionIndex can be ignored. A typical implementation where an NSArray of objects is being rendered would simply return the count.

-(NSUInteger)shinobiDataGrid:(ShinobiDataGrid *)grid numberOfRowsInSection: (NSInteger) sectionIndex {
    return _data.count;
}

The next method is invoked by the data-grid when it has created (or recycled) a cell for rendering at a specific location (row / column) within the grid. Again, a typical implementation would use the rowIndex of the supplied cell to determine the data object being rendered. The cell contents then depends on the column that it belongs to.

Once the column has been determined, the cell can be cast to the expected type (as defined by the columns cellType property), and its visual state set:

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid prepareCellForDisplay:(SDataGridCell *)cell
{
    // both columns use a SDataGridTextCell, so we are safe to perform this cast
    SDataGridTextCell* textCell = (SDataGridTextCell*)cell;

    // locate the person that is rendered on this row
    PersonDataObject* person = _data[cell.coordinate.row.rowIndex];

    // determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"Name"])
    {
        // render the name in the 'name' column
        textCell.textField.text = person.forename;
    }
    if ([cell.coordinate.column.title isEqualToString:@"Age"])
    {
        // render the age in the 'age' column
        textCell.textField.text = [person.age stringValue];
    }
}

See related code sample: GettingStarted.xcodeproject

How to: Create a Shinobi DataGrid with Multiple Sections

This how-to guide will lead you through the steps required to render data that is grouped into sections with the Shinobi data-grid.

In order render grouped data within the data-grid you need to do the following:

  1. Add a number of columns to the data-grid.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. Implement both the required datasource methods, together with the optional numberOfSectionsInShinobiDataGrid: method.

Rendering grouped data with the ShinobiDataGrid simply involves implementing one of the optional SDataGridDataSource methods. For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

In simple terms, all you have to do in order to render grouped data is implement the numberOfSectionsInShinobiDataGrid: method. This will inform the data-grid how many sections it will render. When populating cells via the shinobiDataGrid:prepareCellForDisplay: method, you must them consider the sectionIndex of the cell as well as the rowIndex and column. We’ll look at how you might handle this in more detail.

In order to render a data-grid of items that are grouped, it is probably easiest to structure your underlying data so that it is already grouped. As an example, if you want to group ‘Person’ data objects via the first letter of their name, you could create a PersonGroup object:

// a data object that represents a group of people
@interface PersonGroup : NSObject

@property NSString* letter;

@property NSArray*  items;

@end

Where each PersonGroup contains person instances that belong to the same group (i.e. they might all have names that start with the letter ‘Y’). We’ll leave out the details of actually assembling your data into groups, although if you are struggling with this, see the DataGridWithSections.xcodeproject code sample.

Assuming an instance variable _data which is an array of PersonGroup instances, the optional datasource method is implemented as follows:

- (NSUInteger)numberOfSectionsInShinobiDataGrid:(ShinobiDataGrid *) grid
{
    return _data.count;
}

You can also inform the grid of the title it should render for each section by implementing the following datasource method:

- (NSString *)shinobiDataGrid:(ShinobiDataGrid *)grid titleForHeaderInSection:(NSInteger) section
{
    PersonGroup* group = (PersonGroup*)_data[section];
    return group.letter;
}

The implementation of shinobiDataGrid:numberOfRowsInSection: method needs to use the supplied sectionIndex to inform the grid of the number of rows (in a non-grouped grid you can safely ignore this argument):

-(NSUInteger)shinobiDataGrid:(ShinobiDataGrid *)grid numberOfRowsInSection:(NSInteger) sectionIndex
{
    PersonGroup* group = (PersonGroup*)_data[sectionIndex];
    return group.items.count;
}

Finally, you need to consider the section and row indices when supplying the grid with data:

-(void)shinobiDataGrid:(ShinobiDataGrid *)grid prepareCellForDisplay:(SDataGridCell *)cell
{
    // both columns use a SDataGridTextCell, so we are safe to perform this cast
    SDataGridTextCell* textCell = (SDataGridTextCell*)cell;

    // locate the group that this row belongs to
    PersonGroup* group = _data[cell.coordinate.row.sectionIndex];
    PersonDataObject* person = group.items[cell.coordinate.row.rowIndex];

    // determine which column this cell belongs to
    if ([cell.coordinate.column.title isEqualToString:@"surname"])
    {
        textCell.textField.text = person.surname;
    }
}

When you render grouped data, the user can tap on the sections headers to expand / collapse individual sections. You can received notifications when this occurs by supplying a delegate to the data-grid, and also customise this behaviour by determining whether a user is allowed to collapse specific sections.

See related code sample: DataGridWithSections.xcodeproject

How to: Style the Shinobi DataGrid

The data-grid has a powerful styling concept which allows you to set the styles for alternating rows, selected rows, columns and header cells.

Styles are defined by the SDataGridCellStyle object which allows you to set the background color, font, text color and even gradients for cells.

In order to create a grid with an alternating gray background, create a SDataGridCellStyle with a backgroundColor of the required color. Then set the data-grid defaultCellStyleForAlternateRows property as follows:

// create an alternating row style that just sets the background color
SDataGridCellStyle* alternatingStyle = [[SDataGridCellStyle alloc] init];
alternatingStyle.backgroundColor = [UIColor colorWithWhite:0.9f alpha:1.0f];
_shinobiDataGrid.defaultCellStyleForAlternateRows = alternatingStyle;

If for example you wish to make one of the columns render using bold text, you can create a style and apply it as follows:

// make the symbol column bold
SDataGridCellStyle* boldStyle = [[SDataGridCellStyle alloc] init];
boldStyle.font = [UIFont boldSystemFontOfSize:_shinobiDataGrid.defaultCellStyleForRows.font.pointSize];
symbolColumn.cellStyle = boldStyle;

Notice that the bold font is applied to both regular and alternate rows. This is because the font property of the style applied to alternating rows is nil, which means that this value falls back to the value given by the column style.

The SDataGridCellStyle also allows you to specify gradients. In the example below a gradient is applied to the selected rows:

// add a gradient to the selected row
NSArray* colors = @[[UIColor colorWithWhite:1.0 alpha:0.5],[UIColor colorWithWhite:1.0 alpha:0.1]];
NSArray* stops = @[@0.0, @1.0];
_shinobiDataGrid.defaultCellStyleForSelectedRows.gradient =
    [SDataGridGradient gradientWithColors:colors locations:stops];

See related code sample: StylingTheGrid.xcodeproject

How to: Sort the Shinobi DataGrid

You can specify the sort behaviour of the data-grid on a per-column basis. This how-to guide shows the steps required to enable sorting for the columns of a data-grid and how you should handle the delegate methods that are invoked when sorting occurs.

In order to create a grid which supports sorting you need to perform the following steps:

  1. Add a number of columns to the data-grid.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. For each column, enable sorting via the sortMode property.
  4. Provide a delegate that is informed of changes in column sorting.
  5. When the delegate method is invoked, sort your data accordingly.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

The sort behaviour of the data-grid is specified on a per-column basis. If sorting is enabled for a column, the sort state changes as when the user taps on the column header. You can enable sorting on a column by setting its sortMode property.

SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"age"];
ageColumn.sortMode = SDataGridColumnSortModeTriState;
[_shinobiDataGrid addColumn:ageColumn];

The sort mode can be one of the following:

  • SDataGridColumnSortModeNone - this indicates that the column cannot be sorted
  • SDataGridColumnSortModeBiState - when the user taps this column it will alternate between ascending and descending sort order.
  • SDataGridColumnSortModeTriState - when the user taps this column it will alternate between ascending, descending and non-sorted order.

It is recommended that you use the tri-state mode if your data has a ‘natural’ sort order that you would like to use for the third sort state.

The data-grid only requests the data for the cells that are currently visible via the datasource. Because the data-grid is not aware of all of the data that you might be supplying it with, it is unable to sort your data for you. In order to handling sorting you have to supply a delegate to the grid and handle the various sort methods.

In a typical implementation, that supports tri-state sorting, you will probably want to store two arrays of data, one that is the original state and one that is the current sorted state:

// create some data to populate the grid
_data = [PersonDataSource generatePeople:50];

// we have a second array that stores the sorted data
_sortedData = [NSArray arrayWithArray:_data];

The data-grid datasource methods would use the _sortedData instance variable above in order to retrieve the data for each cell.

The delegate has an optional method shinobiDataGrid:didChangeSortOrderForColumn:, which is invoked each time the sort state for a column changes. A typical implementation of this method would check to see which column has been sorted, then sort the data accordingly. See the example below that uses the NSArray method sortedArrayUsingComparator to sort the data:

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didChangeSortOrderForColumn:(SDataGridColumn *)column
                   from:(SDataGridColumnSortOrder)oldSortOrder
{
    if (column.sortOrder == SDataGridColumnSortOrderNone)
    {
        // if there is no sorting, use the 'natural' order of the data
         _sortedData = [NSArray arrayWithArray:_data];
    }
    else
    {
        if ([column.title isEqualToString:@"age"])
        {
            // sort by the age property
            _sortedData = [_data sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
                id valueOne = [obj1 age];
                id valueTwo = [obj2 age];
                NSComparisonResult result = [valueOne compare:valueTwo];
                return column.sortOrder == SDataGridColumnSortOrderAscending ? result : -result;
            }];
        }
        else if ([column.title isEqualToString:@"name"])
        {
            // sort by the forename property
            _sortedData = [_data sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
                id valueOne = [obj1 forename];
                id valueTwo = [obj2 forename];
                NSComparisonResult result = [valueOne compare:valueTwo];
                return column.sortOrder == SDataGridColumnSortOrderAscending ? result : -result;
            }];
        }
    }

    // inform the grid that it should re-load the data
    [_shinobiDataGrid reload];
}

Note, that once the data has been sorted you must inform the grid so that it can be re-drawn. This is achieved via the reload method.

See related code sample: SortingTheGrid.xcodeproject

How to: Handling Editing with the Shinobi DataGrid

The data-grid can be a useful UI component for data-entry applications. The delegate provides notification of edit events allowing you to update your underlying ‘model’. This how-to guide shows you the steps in solved in creating a simple editable grid.

In order to create a grid that supports editing you need to do the following:

  1. Add a number of columns to the data-grid, and make one or more of them editable, via the editable property.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. Provide a delegate that is informed when the user edits a cell.
  4. When a cell is edited, update the underlying model.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

By default, all cells are read-only. Editing can be enabled on a column-by-column basis by setting the editable property:

// add an editable age column
SDataGridColumn* ageColumn = [[SDataGridColumn alloc] initWithTitle:@"Age"];
ageColumn.editable = YES;
[_shinobiDataGrid addColumn:ageColumn];

The data-grid can be configured to edit or select cells using single or double taps. This is achieved by setting the event masks, for example, you can configure the cell to edit on double-tap as follows:

// enable editing on double-tap
_shinobiDataGrid.singleTapEventMask = SDataGridEventNone;
_shinobiDataGrid.doubleTapEventMask = SDataGridEventEdit;

The SDataGridDelegate has various optional methods that are invoked when cells are edited. When a cell is edited the data-grid does not update the underlying ‘model’, i.e. your internal representation of the data rendered in the grid. In order to support editing you need to implement shinobiDataGrid:didFinishEditingCellAtCoordinate: which is invoked when an edit is ‘committed’ for a cell.

The code snippet below shows an example implementation of shinobiDataGrid:didFinishEditingCellAtCoordinate: - firstly the cell that has been edited is located and the updated text is extracted from the textField. Following this the respective model object property is updated.

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didFinishEditingCellAtCoordinate:(SDataGridCoord *)coordinate
{
    // find the cell that was edited
    SDataGridTextCell* cell = (SDataGridTextCell*)[_shinobiDataGrid visibleCellAtCoordinate:coordinate];

    // find the text entered by the user
    NSString* updatedText = cell.textField.text;

    // 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 = updatedText;
    }
    if ([cell.coordinate.column.title isEqualToString:@"Age"])
    {
        // for numeric values you need to parse the input text
        NSNumberFormatter* f = [[NSNumberFormatter alloc] init];
        [f setNumberStyle:NSNumberFormatterDecimalStyle];
        NSNumber* newAge = [f numberFromString:updatedText];

        if (newAge != nil)
        {
            // if the input was valid - update the model
            person.age = newAge;
        }
        else
        {
            // if input was invalid, update the cell state
            cell.textField.text = [person.age stringValue];
        }
    }
}

Note that for the numeric field the value entered by the user is parsed in order to ensure it is a valid numeric value.

See related code sample: HandlingEditing.xcodeproject

How to: Row and Column Re-ordering with the Shinobi DataGrid

The data-grid allows the user to re-order both rows and columns via a long-press gesture. This how-to guide shows the steps required to enable row and column drag, and shows you how to update your data accordingly.

In order to create a grid where rows and columns can be dragged you need to do the following:

  1. Add a number of columns to the data-grid.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. Provide a delegate that is informed when the user drags a row or column.
  4. Enable row re-ordering, or re-ordering of specific columns.
  5. In the case of row dragging, update your data accordingly.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

Row re-ordering can be enabled by setting the canReorderRows property as follows:

// enable row re-ordering
_shinobiDataGrid.canReorderRows = YES;

And column re-ordering can be enabled on a per-column basis as follows:

// enable re-ordering on some specific columns
symbolColumn.canReorderViaLongPress = YES;
nameColumn.canReorderViaLongPress = YES;

The data-grid only requests the data for the cells that are currently visible via the datasource. Because the data-grid is not aware of all of the data that you might be supplying it with, it is unable to handle row drag operations for you. In order to handle row-drag you have to supply a delegate to the grid and implement the row drag methods.

When the user is dragging a row the shinobiDataGrid:row:hasBeenSwitchedWithRow: delegate method is invoked each time the row passes another one. With the indices passed to this method you must switch the rows in your underlying data representation.

As an example, if you have two arrays, one which holds the data in its original order, and the other which is used to store the current ‘sort’ state:

// create some data to populate the grid
_data = [StockPriceDataSource getStockPrices];
_sortedData = [NSArray arrayWithArray:_data];

Then as the user drags a row, you simply switch the elements in the _sortedData array:

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid
                    row:(SDataGridRow *)rowSwitched
 hasBeenSwitchedWithRow:(SDataGridRow *)rowSwitchedWith
{
    // clone the sorted data into a mutable array
    NSMutableArray* sortedData = [[NSMutableArray alloc] initWithArray:_sortedData];

    // perform the switch
    [sortedData exchangeObjectAtIndex:rowSwitched.rowIndex withObjectAtIndex:rowSwitchedWith.rowIndex];

    // copy back to the sorted data property
    _sortedData = [[NSArray alloc] initWithArray:sortedData];
}

Note, that the datasource methods should use the _sortedData array to supply data to the grid.

See related code sample: RowAndColumnReOrdering.xcodeproject

How to: Handle selection with the Shinobi DataGrid

The data-grid has a number of selection modes which you can use to create applications with interactive data-grids. This how-to guide shows the steps required to enable selection, handle changes in selection as the user taps on the grid, and update selection state programmatically.

In order to create a grid where rows can be selected you need to do the following:

  1. Add a number of columns to the data-grid.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. Specify the data-grid selection mode.
  4. Provide a delegate that is informed of selection changes.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

The data-grid has a selectionMode property that is used to specify the selection behaviour of the grid. The grid supports a combination of either single or multi selection with either rows or cells.

_shinobiDataGrid.selectionMode = SDataGridSelectionModeRowSingle;

In multi-select mode multiple rows or cells can be selected - tapping a row / cell that is not selected adds it to the selection, whereas tapping a row / cell that is currently selected removes it from the selection.

In single-select mode tapping a row / cell that is not selected will result in only the tapped row / cell being selected. Previous selections are removed.

The current selection state of the data-grid is always available via the selectedRows property (if using a row-selection mode), or selectedCells property (if using a cell-selection mode).

In order to detect changes in selection state you need to provide the data-grid with a delegate, which can be any class that adopts the SDataGridDelegate protocol. The delegate has a number optional methods which are called at various stages of the selection state change. In the case of row selection, the order in which these delegate methods are invoked and the changes in state are as follows:

  1. shinobiDataGrid:shouldDeselectRow - this is invoked before a row is de-selected. Returning NO will cancel the de-selection.
  2. shinobiDataGrid:willDeselectRow - this is invoked before a row is de-selected.
  3. selectedRowsproperty changes - at this point the row being de-selected will no longer be present in the selectedRows array.
  4. shinobiDataGrid:didDeselectRow - this is invoked after a row is de-selected.
  5. shinobiDataGrid:shouldSelectRow - this is invoked before a row is selected. Returning NO will cancel the de-selection.
  6. shinobiDataGrid:willSelectRow - this is invoked before a row is selected.
  7. selectedRowsproperty changes - at this point the row being selected will have been added to the selectedRows array
  8. shinobiDataGrid:didSelectRow - this is invoked after a row is selected.

There is an identical set of delegate methods for cell selection, which are called in the same order as above.

It is unlikely that you application will need to provide an implementation for all of these delegate methods - simple pick the ones that indicate the state you need to detect for your needs.

The various selection delegate methods pass the row / cell that is currently being (de)selected. If, for example, your grid is being used to render an array of email messages, you can locate the email that was just tapped by the user by inspecting the rowIndex property of the row.

- (void)shinobiDataGrid:(ShinobiDataGrid *)grid didSelectRow:(SDataGridRow *)row
{
    // locate the email that this relates to
    Email* email = _emails[row.rowIndex];
    self.currentEmail = email;
}

You can programmatically modify the selection state of the grid via the setSelectedRows:animated: method. This takes an array of SDataGridRow instances that indicated the required selection and gives the option to animate the selection change.

For example, this code would select the 5'th row:

[_shinobiDataGrid setSelectedRows:@[[SDataGridRow rowWithRowIndex:5 sectionIndex:0]]];

Note that when the row selection state is changed programmatically the same delegate methods are invoked and data-grid state changes occur as described, however the ‘should’ methods are not invoked. These are primarily intended for cancelling user interactions.

See related code sample: HandlingRowSelection.xcodeproject

How to: Simplify your code using the datasource helper

The data-grid does not impose any restrictions on the format of your underlying data, which means that it is highly flexible. However, this does mean that in order to implement sorting, row re-ordering and grouping, you have to do a lot of the work yourself.

The datasource helper provides a simpler option for rendering an array of objects within the data-grid.

In order to use the datasource helper you need to do the following:

  1. Add a number of columns to the data-grid, setting the propertyKey for each column.
  2. Create an NSArray of data, where the objects have properties which are referred to by the propertyKey of each column.
  3. Construct a datasource helper.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

In order to use the datasource helper you need to set the propertyKey of each column:

// add a name column
SDataGridColumn* surnameColumn = [[SDataGridColumn alloc] initWithTitle:@"Surname" forProperty:@"surname"];
surnameColumn.sortMode = SDataGridColumnSortModeTriState;
[_shinobiDataGrid addColumn:surnameColumn];

// add a name column
SDataGridColumn* forenameColumn = [[SDataGridColumn alloc] initWithTitle:@"Forename" forProperty:@"forename"];
forenameColumn.sortMode = SDataGridColumnSortModeTriState;
[_shinobiDataGrid addColumn:forenameColumn];

You can then create a datasource helper, associated with the grid, then supply your data:

// create the helper
_datasourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:_shinobiDataGrid];

_data = @[ /* create your array of objects here */];

// supply the data to the helper
_datasourceHelper.data = _data;

And that’s it, your data should now be rendered, with support for sorting!

In order to group your data simply set the groupedPropertyKey on the datasource helper to indicate the property of your objects which you wish to group by.

If you have custom formatting requirements you can implement the displayValueForProperty method provided by the SDataGridDataSourceHelper delegate. The example below appends the text ‘yrs’ to the end of an age column:

- (id)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper
       displayValueForProperty:(NSString *)propertyKey
              withSourceObject:(id)object
{
    if ([propertyKey isEqualToString:@"age"])
    {
        PersonDataObject* person = (PersonDataObject*)object;
        return [NSString stringWithFormat:@"%@ yrs", person.age];
    }

    // for any other property, return nil, which will result in the default behaviour being applied.
    return nil;
}

See related code sample: DataGridDataSourceHelper.xcodeproject

How to: Creating custom cells

The data-grid ships with a few different cell types that can be used to render text. If you want to render more complex content, you need to create your own custom cells to support this. Fortunately this is quite an easy process, as detailed in this how-to guide!

In order to render a grid with custom cells you need to:

  1. Add a number of columns to the data-grid.
  2. Provide a datasource. This can be any class that implements the SDataGridDataSource protocol.
  3. Create a custom cell that subclasses SDataGridCell.
  4. Set the cellType of one, or more, columns to your custom cell.

For the purposes of this how-to guide it is assumed that you already know how to render basic tabular data within the data-grid, if not, it is suggested that you follow the How to: Provide data to the ShinobiDataGrid guide.

Custom cells must subclass SDataGridCell, typically you would add one or more properties to the cell that are used to define its visual appearance. For example, a cell that renders a price with an up / down change arrow could have the following interface:

typedef enum {
    PriceMovementNone = 0,
    PriceMovementUp,
    PriceMovementDown
} PriceMovement;

@interface PriceCell : SDataGridCell

@property (nonatomic) CGFloat price;

@property (nonatomic) PriceMovement priceMovement;

@end

The implementation of your cell should add the required UI elements to the view within the initWithReuseIdentifier method:

@implementation PriceCell
{
    UILabel* _label;
    UILabel* _priceMovementLabel;
}

...

- (id)initWithReuseIdentifier:(NSString *)identifier
{
    if (self = [super initWithReuseIdentifier:identifier]) {


        // create the two labels that are used to render the cell contents
        // NOTE: the background for each label is clear so that the border and background color of the cell
        // still shows through.
        _label = [[UILabel alloc] init];
        _label.backgroundColor = [UIColor clearColor];
        _label.textAlignment = NSTextAlignmentRight;
        [self addSubview:_label];

        _priceMovementLabel = [[UILabel alloc] init];
        _priceMovementLabel.textColor = [UIColor redColor];
        _priceMovementLabel.backgroundColor = [UIColor clearColor];
        [self addSubview:_priceMovementLabel];
    }
    return self;
}

...

@end

In the above example a couple of labels are added to the UI. Note that their background color is set to clear in both cases in order that the background rendered by the SDataGridCell superclass is visible behind the text.

The cell frame is computed and set by the data-grid. If you simply want your UI elements to fill the entire cell frame set the fitSubviewsToView property to true. Otherwise, you need to ensure that the UI elements of your custom cell are positioned correctly when the frame is set:

- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];

    // when the cell frame is set, update the frames of the two labels it contains
    _priceMovementLabel.frame = CGRectMake(20, 0, 50, self.bounds.size.height);
    _label.frame = CGRectMake(60, 0, self.bounds.size.width - 80, self.bounds.size.height);

}

Finally, you need to ensure that when the public properties of the cell are set, the UI elements are updated accordingly, for example:

- (void)setPrice:(CGFloat)price
{
    _price = price;

    // when the price property is set, update the label that renders this value
    _label.text = [NSString stringWithFormat:@"%.2f", price];
}

To use this cell for a column, set the cellType:

bidColumn.cellType = [PriceCell class];

This ensures that the column creates instances of your custom cell type. You can then populate this cell just as you would any other within your implementation of the shinobiDataGrid:prepareCellForDisplay: datasource method. If you are using the datasource helper, you need to handle the following delegate method in order to populate the custom cell:

- (BOOL)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper populateCell:(SDataGridCell *)cell withValue:(id)value forProperty:(NSString *)propertyKey sourceObject:(id)object
{
    if ([propertyKey isEqualToString:@"bidPrice"])
    {
        StockPrice* stockPrice = (StockPrice*)object;
        PriceCell* priceCell = (PriceCell*)cell;

        priceCell.price = stockPrice.bidPrice;
        priceCell.priceMovement = stockPrice.bidPriceMovement;

        return YES;
    }

    // return 'NO' so that the datasource helper populates all the other cells in the grid.
    return NO;
}

See related code sample: CreatingCustomCells.xcodeproject