Blog

Back to Blog

A Financial DataGrid

Posted on 28 Aug 2013 Written by Jan Akerman

The 2.0 release of ShinobiGrids brought a whole host of new features to the table that once upon-a-time had to be implemented by you! This blog post aims to demonstrate how to apply some basic styling to a grid, and give updating cells a highlighted effect, using the SDataGridDataSourceHelper to make our life easier!

By the end of the blog post we will have a financial data-grid that displays a number of different financial instruments.These instruments will update dynamically to show (mock) changes in instrument prices. The grid will also let you select instruments to be traded, and allow you to sort them by either name or amount of items owned.

To best understand this blog it would be best to clone or browse the full project on GitHub, or download the project and follow along, as I will not be posting all of the code here. Below you can see what we will achieve by the end of this post. 

Blog Preview

 

 

The ShinobiGrid and ShinobiDataGrid allow you to render any data model structure you like; this makes it both powerful and flexible. However, this comes at the price of more work from you – it requires you to tell the grid how to render these structures.

This is where the data-source helper comes in. The data-source helper provides an easy mechanism for rendering objects stored in an NSArray. It allows you to completely avoid writing your own data-source!

The data-source helper can automatically help with the populating of un-customised Shinobi cells, sorting, grouping, model object selection and row reordering. This all sounds pretty great but remember the data-source helper adds one restriction – your data must be stored in an NSArray. I will explain how to use the data-source helper later in the blog but for now, lets set up our data layer & grid.

 

The data layer

We will be using the data-source helper so we need a model object with some properties to populate the grid. Let’s create our Instrument object – this will be the foundation of the data we want to be displayed on the grid. See the interface below:

@interface Instrument : NSObject

@property InstrumentType type;
@property NSString *name;
@property NSNumber *amount;
@property NSNumber *bidSell;
@property NSNumber *bidSellDelta;
@property NSNumber *askBuy;
@property NSNumber *askBuyDelta;
@property NSNumber *spread;
@property NSNumber *high;
@property NSNumber *low;
@property NSNumber *netChange;
@property NSNumber *percentChange;
@property NSDate *lastUpdated;

@end

 

We have a number of properties on the instrument, most of which we will be displaying as columns in our grid.

For simplicity we will be making our own mock data to populate the grid. I made the class MyInstrumentList, which holds an NSArray of instruments which it populates in its init method:

@implementation MyInstrumentList

-(id)init {
    if (self = [super init]) {
        [self setupMockList];
    }
    return self;
}

…

@end

 

Pretty simple so far!

 

Setting up the mock updates!

We want to simulate a financial grid, so we need some mechanism of updating our stocks. Since this is not a real financial grid implementation we don’t want to waste our time getting real financial data from the web, so lets update our stocks with some random data!

To handle this updating I created a class called StockManager. This class has a reference to our instrument list, and our grid. This allows it to update the objects in my instrument list and tell the grid to update its row for the newly updated instrument! 

In the startManager method of the StockManager I schedule a timer to fire every 2 seconds which calls its own updateInstruments method. This updateInstruments method randomly chooses an instrument to update and changes its bid and ask price randomly by up to ± 20%. The stockManager adds the instrument to its array of recently updated instruments. It then tells the grid to reload. Once the grid has reloaded it removes the instrument from its array of recently updated instruments.

Again, this isn’t really an interesting part of the post so download the project to see the StockManager code.

Setting up our custom cells

Next, we are going to make our custom cells, as we will need these when setting up the grid. In this blog we will be making two types of custom cells. I won’t go into too much detail on how to make them, as that will be covered by another blog post.

In the final financial grid we want highlighting rows to show our users recently updated instruments. When an instrument updates we want the row to turn yellow and the bid/ask cells to highlight red or green depending on whether they rise or fall.

To do this I made two cell subclasses, one with the functionality to highlight yellow, and the other to highlight red or green depending on whether the cells new value was positive or negative change. To see the code for the custom cells feel free to download the entire project from the top of this blog post.

Setting up the data-grid

We are going to create a setupGrid and styleGrid method, which we will be calling from the viewDidLoad method of our main view controller. I added the grid to my project using Interface Builder by simply dragging in a UIView and changing its class to ShinobiDataGrid. In the setupGrid method we set some interaction properties on the grid and add some columns to the grid.

- (void)setupGrid {

    // Prohibit selection and editing.
    _financialGrid.singleTapEventMask = SDataGridEventSelect;
    _financialGrid.doubleTapEventMask = SDataGridEventNone;
    
    // Allow selection of multiple grid rows.
    _financialGrid.selectionMode = SDataGridSelectionModeRowMulti;
    
    SDataGridColumn *nameColumn = [[SDataGridColumn alloc] initWithTitle:@"Instrument" forProperty:@"name" cellType:[UpdateInstrumentNameCell class] headerCellType:nil];
    nameColumn.width = @150;
    // Set the name column to enable sorting in two states only.
    nameColumn.sortMode = SDataGridColumnSortModeBiState;
    [_financialGrid addColumn:nameColumn];

…

    // Create our instrument list - this is mock data.
    MyInstrumentList *instrumentsList = [[MyInstrumentList alloc] init];
    
    // Set up the data-source helper and give it our list of instruments as its data array.
    _dataSourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:_financialGrid];
    _dataSourceHelper.delegate = self;
    _dataSourceHelper.data = instrumentsList.instruments;
}

 

As can be seen from above, we enable selection, set the grid to allow multiple selections, and add our columns to the grid. On each column we set the title, the property (on our model object this column represents), and the cell classes for our custom cells (we don’t have a custom header so we leave that nil).

For each column we assign a property on our Instrument model object. This property should correspond to the property on the model object we defined in our data-layer. For example, when we setup the name column we give it the title “Instrument”, this is what will display in our column header. We then tell it to populate itself using the ‘name’ property on our Instrument object. Then, we set the width of the column, and a sort mode (if we want that column to sort) and add it to the grid. This process is repeated for every column in the grid.

Next we create our data-source helper and pass it our grid so it knows which ShinobiGrid it is looking after. Next, we set its delegate to our view controller so we can implement its delegate methods to populate our custom cells and then we set its data to our NSArray of Instrument objects.

With our grids structure set up, we then want to style our grid. Styling isn’t the main focus of this blog post so I will skip over this part. If you want to see the code for the styling then you can download the entire project as mentioned above. 

So, below you can see what we have so far.

No Custom Cells

 

There are a few things to note about what we currently have:

  • Some cells contain too many decimal points.
  • Our updated column isn’t sowing us the time, it is showing us the date!
  • Our custom cells have not rendered.

The first two points are simply because our data-source helper is pulling in the data from our array and displaying it in its default format. That is, the decimal points are not limited and the full date is being printed instead of the time. We need to intercept the cell population and format those strings! The data-source helper exposes a delegate method that is designed to let you do exactly that. Then we will worry about our third issue!

 

Configuring our data-source helper

First things first, lets set up our decimal number formatter and our date formatter as ivars of our view controller as below:

    // Create a number formatter that will output number with decimal places.
    _decimalFormatter = [[NSNumberFormatter alloc] init];
    _decimalFormatter.usesGroupingSeparator = YES;
    _decimalFormatter.groupingSize = 3;
    _decimalFormatter.groupingSeparator = @",";
    _decimalFormatter.maximumFractionDigits = 5;
    _decimalFormatter.minimumFractionDigits = 2;
    _decimalFormatter.minimumIntegerDigits = 1;
    
    // Create a date formatter that will output a time only.
    _dateFormatter = [[NSDateFormatter alloc] init];
    _dateFormatter.dateFormat = @"HH:mm:ss";

 

Then, the method we need to implement is dataGridDataSourceHelper: displayValueForProperty: withSourceObject:. Every time a cell is rendered this delegate method is called giving us a chance to alter what is put in the cell. Whatever we return from this method is what the data-source helper will attempt to populate the cell with.

What we need to do here is check which column the cell we are about to render belongs to, if the column has a property key that corresponds to a decimal number (spread, high, low, netChange, or percentChange) then we want to use our decimal formatter to return a formatted string. If the column has a property key that corresponds to a date (the lastUpdated property key) then we need to format our date to a string that shows the time only, and return that. This is exactly what the code below does:

// This method gives us an opportunity to format the properties being rendered to te grid from our model object.
-(id)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper displayValueForProperty:(NSString *)propertyKey withSourceObject:(id)object {
    
    Instrument *instrument = (Instrument *)object;
    
    // Format these columns with the decimal formatter.
     if ([propertyKey isEqualToString:@"spread"] || [propertyKey isEqualToString:@"high"] || [propertyKey isEqualToString:@"low"] || [propertyKey isEqualToString:@"netChange"] || [propertyKey isEqualToString:@"percentChange"]) {
        
        return [_decimalFormatter stringFromNumber:[instrument valueForKey:propertyKey]];
        
    // Format the last updated column with the date formatter.
    } else if ([propertyKey isEqualToString:@"lastUpdated"]) {
        
        return [_dateFormatter stringFromDate: [instrument valueForKey:propertyKey]];
    }
    
    // If we return nil instead of a formatted value the data-source helper will use the properties default format.
    return nil;
}

 

This is all still much easier than writing a data-source yourself!

Running the project now, we can see that all our decimal places and our date are all formatter how we want them to be. The next thing we need to tackle is our lack of custom cells. Our custom cells are not rendering because our data-source helper doesn’t know how to render them (as we made them!). Fortunately, the data-source helper exposes a delegate method that allows us to intercept its cell rendering, giving us a chance to manually populate our custom cells.

The data-source helper delegate method we need to implement in our view controller is the method dataGridDataSourceHelper:populateCell:withValue:forProperty:sourceObject:, quite a long method signature!

We only want to manually populate the columns that are using custom cells. The columns that are using custom cells in this project are the name column, the bid column and the ask column. This means we need to check for the property keys ‘name’, ‘bidSell’, and ‘askBuy’.

For the custom cells in the name column, we simply set its text label to the name of the instrument being rendered, and then if the instrument is contained in our StockManager’s recentlyUpdated array, we highlight the cell.

For the custom cells in the bidSell and askBuy column, we have to set the text label of the cell as above, then if the instrument was recently updated we also call its showHighlightForDelta: method with the appropriate delta property on the instrument we are rendering. This is a method I implemented on my subclass of SDataGrid cell that holds the positive/negative highlighting logic. This method will cause the cell to highlight green or red, depending on the change, and will display an up or down arrow too.

Now you might be wondering why we have to use this recentlyUpdated list on our StockManager to decide whether to highlight the cell. Surely it would be simpler to just highlight it every time the cell renders, as it only renders when we reload it? Not true, as our grid re-uses cells it means that rows that have gone out of view will be re-rendered when they are scrolled back in view. If we didn’t check our StockManagers recentlyUpdated array, every row that scrolled back into view would be highlighted!

So, I have described what our implementation of the populateCell delegate method will do, so here is the code:

// This method gives us an opportunity to manually render and custom cells in the grid. We need to do this because the grid has no idea how to render our custom cells.
-(BOOL)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper populateCell:(SDataGridCell *)cell withValue:(id)value forProperty:(NSString *)propertyKey sourceObject:(id)object {
    Instrument *instrument = (Instrument *)object;
    
    // The name cell is using a custom cell so we must populate it manually.
    if ([propertyKey isEqualToString:@"name"]) {
        // Cast the cell given to us to an UpdateInstrumentNameCell as we specified that the name column will be using this custom cell when we set it up.
        UpdateInstrumentNameCell *nameCell = (UpdateInstrumentNameCell *)cell;
        
        // Set the name cell's text label to the name of the instrument.
        nameCell.label.text = instrument.name;
        
        if ([_stockManager.recentlyUpdatedInstruments containsObject:instrument]) {
            [nameCell showHighlight];
        }
        
        // We return YES so that the data-source helper knows we have manually populated our custom cell, so that it knows it doesn't have to attempt to populate it.
        return YES;
    // We are using the same custom cell for the bidSell and the askBuy column. We must populate cells in these columns manually.
    } else if ([propertyKey isEqualToString:@"bidSell"] || [propertyKey isEqualToString:@"askBuy"]) {
        // Cast the cell given to us to an UpdateHighlightCell as we specified that these columns will be using this custom cell when we set up the columns.
        UpdateHighlightCell *highlightCell = (UpdateHighlightCell *)cell;
        
        // Set the cells label to a formatted version of the property we are populating.
        highlightCell.label.text = [_decimalFormatter stringFromNumber:[instrument valueForKey:propertyKey]];
        
        // Get the change in value for the property we are populating and tell the cell to show the correct arrow.
        NSNumber *delta = [instrument valueForKey:[propertyKey stringByAppendingString:@"Delta"]];
        [highlightCell showArrowForDelta:delta];
        
        // If the instrument we are currently rendering is in the recently updated list then trigger the correct highlight for the change in value.
        if ([_stockManager.recentlyUpdatedInstruments containsObject:instrument]) {
            [highlightCell showHighlightForDelta:delta];
        }
        
        // We return YES so that the data-source helper knows we have manually populated our custom cell, so that it knows it doesn't have to attempt to populate it.
        return YES;
    }
    
    // We return NO for all other cells so that the data-source helper knows it needs to populate them for us.
    return NO;
}

 

So now we should have something that looks like below:

Nearly THere

 

Selection

As I said at the very beginning of this blog post, the data-source helper is a middleman between the grid and us. The data-source helper delegate protocol actually confirms to the SDataGridDelegate protocol, meaning all delegate events from the grid get forwarded to the data-source helper’s delegate (our view controller). This allows us to use the selection delegate methods from the grid.

The data-grid makes setting and getting the selected rows really easy. The new data-source helper makes getting the actual model object behind the row really easy! In our case, the selectedItems property on our data-source helper holds all the instrument objects that are associated with the currently selected rows. What makes this property even more useful is that it isn’t read-only. This means you can actually set which rows are selected on your grid by setting this property to the array of instruments you want selected.

To show you this sorting we are going to implement some (very fake) stock trading. First, we create a button with the title ‘Trade Stocks’ and set it to be hidden, I did this in Interface Builder. Then we implement the following data-grid delegate methods:

// When a row is selected show the trade button.
-(void)shinobiDataGrid:(ShinobiDataGrid *)grid didSelectRow:(SDataGridRow *)row {
    _tradeSelectedButton.hidden = NO;
}

// When a row is deselected, hide the trade button if there are no other rows selected.
-(void)shinobiDataGrid:(ShinobiDataGrid *)grid didDeselectRow:(SDataGridRow *)row {
    if (_financialGrid.selectedRows.count == 0) {
        _tradeSelectedButton.hidden = YES;
    }
}

 

The above code simply shows the button we just created when we have selected rows, and hides the button when we have no rows selected. Now, we just need to implement our very fake stock trading.

First at the end of our viewDidLoad method, add a target to our button control to call a method, in my project this method is called tradeSelectedButtonTapped. Inside this method we get the array of selected instrument objects. Easy.

NSArray *selectedInstruments = _dataSourceHelper.selectedItems;

 

Next we simply loop through the array of instrument objects and create a string out of their names, which is then displayed in a UIAlertView. I won’t show the code for that bit, as it isn’t really relevant to the grid. 

 

In Summary

When following anything like this it is good practice to summarise what you have just read, so here it is:

  • We setup our data layer & mock objects
  • Created our custom cells
  • Set up and styled the data-grid
  • Configured our data-source helper, specifically;
    • Intercepted the default cell rendering to format our values
    • Populated our custom cells manually
    • Implemented row selection

 

Hopefully after reading this and having a good look at the project (find the full project on GitHub, or download the project as a zip) you will be able to use the data-source helper in your own projects. I cannot stress how much simpler it makes most data-grid use-cases!

Thanks for reading.

Back to Blog