thumbnail2

Back to Blog

Using Core Data in ShinobiGrids

Posted on 17 Apr 2014 Written by Alison Clarke

If you’re using Core Data in your app, you may have wondered if you can display that data using our iOS Grids. The answer is, “Of course you can!” This tutorial will show you how.

We’re going to build a simple app which uses Core Data to store information about a collection of books, then use a ShinobiDataGrid to allow the user to view and edit the data.

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

Setting up the model

To get started, create a new Empty Application in Xcode, checking Use Core Data (we called our project “Bibliotheca”, since it represents a collection of books). If you’re new to Core Data you might want to read the Ray Wenderlich tutorial Core Data Tutorial for iOS: Getting Started before reading on, to find out how to set up a Core Data model. If you get stuck creating the model, you can download the starter project and then follow the tutorial from the Adding Model Helpers section.

Our model is going to store information about books, so we’ll create entities representing books, authors and publishers. A book can have a single author and a single publisher (to keep things simple) but an author can be related to multiple books, as can a publisher.

Create the following 3 entities in your new model:

  • Author:
    • Attributes:
      • forenames (String)
      • surname (String)
    • Relationships:
      • books (Type: To Many)
  • Publisher:
    • Attributes:
      • name (String)
    • Relationships:
      • books (Type: To Many)
  • Book:
    • Attributes:
      • title (String)
      • year (Integer 16)
    • Relationships:
      • author (Destination: Author, Inverse: books)
      • publisher (Destination: Publisher, Inverse: books)

So the model should end up looking like this:

Model

Next, open up Editor > Create NSManagedObject Subclass and select all three entities from your model, to create classes Book, Author and Publisher.

Adding model helpers

To help us to do useful things like sorting the data, we’re going to define a protocol, BiobliothecaManagedObject, then create categories on each of these entity classes which conform to the protocol. This will allow us to make subsequent changes to the model and regenerate the NSManagedObject for each entity, without losing our custom code.

The protocol is very simple:

@protocol BiobliothecaManagedObject <NSObject>
+ (NSString *) sortField;
@end

Once you’ve got your protocol, you can add categories on each of the Book, Author and Publisher classes to define the sortField property, as well as providing a description implementation to enable us to get a useful description of each entity which we can display in our grid. The implementations will look like this:

Book+BibliothecaManagedObject.m:

#import "Book+BiobliothecaManagedObject.h"

@implementation Book (BibliothecaManagedObject)

- (NSString *)description {
    return self.title;
}

+ (NSString*) sortField {
    return @"title";
}

@end

Author+BibliothecaManagedObject.m:

#import "Author+BiobliothecaManagedObject.h"

@implementation Author (BiobliothecaManagedObject)

- (NSString *)description {
    return [NSString stringWithFormat:@"%@ %@", self.forenames, self.surname];
}

+ (NSString *)sortField {
    return @"surname";
}

@end

Publisher+BibliothecaManagedObject.m:

#import "Publisher+BibliothecaManagedObject.h"

@implementation Publisher (BibliothecaManagedObject)

- (NSString *)description {
    return self.name;
}

+ (NSString *) sortField {
    return @"name";
}

@end

Next, we’ll create a helper class, BibliothecaDataHelper, to help us manipulate the data. The interface looks like this:

@interface BibliothecaDataHelper : NSObject

-(id)initWithContext:(NSManagedObjectContext*)context;
-(NSArray*)fetchAllEntitiesOfType:(Class <BibliothecaManagedObject>)type;
-(void)saveChanges;

@end

The implementation is all fairly standard core data code, so I won’t go into the details here. Instead, just grab a copy of BibliothecaDataHelper.m from GitHub. Here’s a summary of what it does:

  • The initializer creates some dummy data if there aren’t yet any books in the database. (If you want to trash the data and start from scratch, edit this method to call setupData:true.)
  • fetchAllEntitiesOfType: simply pulls out all the entities of the given type, sorting them by the sortField defined on the type.
  • saveChanges saves the current changes back to the Core Data store.

So, we now have our model and some helpers – it’s time to build something we can see!

Adding the data to a grid

Our demo app will be a single view application, but because Xcode doesn’t let you create one of those with Core Data, we created an empty application. So we’ll have to create a ViewController and get it up and running ourselves.

First, create a new Objective-C class called ViewController, subclass of UIViewController (don’t create a xib). Next, add a ViewController property in AppDelegate.h:

@property (nonatomic, strong) ViewController *viewController;

Now, edit application:didFinishLaunchingWithOptions: to initialize viewController and set it to be the root view controller for the window. The method will end up looking like this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];

    self.viewController = [[ViewController alloc] init];
    self.window.rootViewController = self.viewController;

    [self.window makeKeyAndVisible];
    return YES;
}

We’re now ready to add a ShinobiDataGrid to our project. Add ShinobiGrids.framework to your project (either find it under “Developer Frameworks” if you’ve used the installer, or drag the framework folder into the project from wherever you’ve saved it).

Import <ShinobiGrids/ShinobiDataGrid.h> into ViewController.h and add a new ShinobiDataGrid property:

#import <UIKit/UIKit.h>
#import <ShinobiGrids/ShinobiDataGrid.h>

@interface ViewController : UIViewController<SDataGridDataSourceHelperDelegate>

@property (strong, nonatomic) ShinobiDataGrid *shinobiDataGrid;

@end

Now open up ViewController.m and add the following instance variables:

@implementation ViewController {
    SDataGridDataSourceHelper* _datasourceHelper;
    BibliothecaDataHelper *_bibliothecaDataHelper;
    NSArray *_data;
    SDataGridCellStyle *_cellStyle;
}

Next, add the following code to viewDidLoad (we’ll look at it in detail shortly):

- (void)viewDidLoad
{
    [super viewDidLoad];

    _shinobiDataGrid = [[ShinobiDataGrid alloc] 
                        initWithFrame:CGRectInset(self.view.frame, 30, 30)];
    [self.view addSubview:_shinobiDataGrid];

    // Get the managed object context from the app delegate, and create a BibliothecaDataHelper
    AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    _bibliothecaDataHelper = [[BibliothecaDataHelper alloc] 
                              initWithContext:[appDelegate managedObjectContext]];

    // Fetch all the books and keep hold of them in our _data array
    _data = [_bibliothecaDataHelper fetchAllEntitiesOfType:[Book class]];

    // Create a datasource helper and hand it the books
    _datasourceHelper = [[SDataGridDataSourceHelper alloc] initWithDataGrid:_shinobiDataGrid];
    _datasourceHelper.data = _data;

    // Create a cell style with the font size we want
    _cellStyle = [[SDataGridCellStyle alloc] init];
    _cellStyle.font = [UIFont systemFontOfSize:13];

    // Add columns
    [self addColumnWithTitle:@"Title" 
                 forProperty:@"Title" 
                    cellType:[SDataGridMultiLineTextCell class] 
                       width:@222];
    [self addColumnWithTitle:@"Author" 
                 forProperty:@"Author" 
                    cellType:[SDataGridMultiLineTextCell class] 
                       width:@160];
    [self addColumnWithTitle:@"Publisher" 
                 forProperty:@"Publisher" 
                    cellType:[SDataGridMultiLineTextCell class] 
                       width:@220];
    [self addColumnWithTitle:@"Year" 
                 forProperty:@"Year" 
                    cellType:[SDataGridMultiLineTextCell class] 
                       width:@105];

    // Reload the data in the grid
    [_shinobiDataGrid reload];
}

The first thing the method does is to create a data grid and add it to the view.

Next, it gets the NSManagedContext from the AppDelegate and uses it to create a BibliothecaDataHelper instance. The helper is then used to get hold of all the books we’ve got stored.

The next lines create a SDataGridDataSourceHelper, which we can use to display the data in the grid. We pass it the data we’ve pulled out of the Core Data store.

Next we create a SDataGridCellStyle and set its font size, because we want to use a smaller font than the default.

We then add columns to the grid, one for each of the book’s attributes/relationships, using a helper method which you’ll need to add to the view controller:

// Helper method to set up columns
- (void)addColumnWithTitle:(NSString *)title forProperty:(NSString *)property
                  cellType:(Class)class width:(NSNumber *)width
{
    SDataGridColumn *column = [[SDataGridColumn alloc] initWithTitle:title
                                                         forProperty:property
                                                            cellType:class
                                                      headerCellType:[SDataGridHeaderMultiLineCell class]];
    column.width = width;
    column.cellStyle = _cellStyle;
    [_shinobiDataGrid addColumn:column];
}

This method creates a column, sets its width and the style for its cells, and adds it to the grid. The forProperty: parameter defines which of the Book’s properties will be displayed in the column. And because we’ve added description methods to our entities, the Publisher and Author will be shown just as we want them.

If you run the app now, here’s what it should look like:

Screenshot1

And that’s all there is to getting your data from CoreData into a ShinobiDataGrid! But it’s not quite all we can do with it…

Editing the attributes

We’ve published a previous tutorial on creating a custom editable cell which goes into the details of making editable cells, so we’ll cover it fairly briefly here – take a look at that post if you want more info. We’re going to use the PickerCell created in that tutorial again here, as it allows the user to use a UIPicker to select an item from a list of predefined values – just what we want to allow the user to change the author or publisher of a book. But we’ll start with the straightforward part: editing a book’s title and year attributes.

First, add in the following line to addColumnWithTitle:forProperty:cellType:width:, just after the column is created, to set the columns to be editable:

column.editable = YES;

Next, we’ll need an SDataGridDataSourceHelperDelegate to respond to the edit events: we’ll use our ViewController as the delegate, so add it to the interface declaration in ViewController.h:

@interface ViewController : UIViewController<SDataGridDataSourceHelperDelegate>

and set the view controller as the grid’s delegate in viewDidLoad, just after creating the grid:

_datasourceHelper.delegate = self;

We’ll now implement the delegate method shinobiDataGrid:didFinishEditingCellAtCoordinate: to receive edits from the title and year columns, and save the changes in our model:

-(void)shinobiDataGrid:(ShinobiDataGrid *)grid 
    didFinishEditingCellAtCoordinate:(SDataGridCoord *)coordinate
{
    // Locate the model object for this row
    Book* book = _data[coordinate.row.rowIndex];

    // Get the cell that was edited
    SDataGridCell* cell = [_shinobiDataGrid visibleCellAtCoordinate:coordinate];

    // Determine which column this cell belongs to, and handle the edit appropriately
    NSString *cellTitle = cell.coordinate.column.title;

    if ([cellTitle isEqualToString:@"Title"] || [cellTitle isEqualToString:@"Year"]) {
        // Find the cell that was edited
        SDataGridMultiLineTextCell* textCell = (SDataGridMultiLineTextCell*) cell;

        // Find the text entered by the user
        NSString* updatedText = textCell.text;

        if ([cellTitle isEqualToString:@"Title"]) {
            // Just update the text of the book's title
            book.title = updatedText;

        } else if ([cellTitle isEqualToString:@"Year"]) {
            // Parse the input text to make sure it's numeric
            NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
            [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
            NSNumber* newYear = [formatter numberFromString:updatedText];

            if (newYear) {
                // If the input was valid, update the model
                book.year = newYear;
            } else {
                // Input was invalid so alert the user and reset to the old value
                UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Invalid year"
                                                                message:@"That's not a valid year. Please try again."
                                                               delegate:self
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
                textCell.text = book.year.stringValue;
            }
        }
    }

    // Save everything
    [_bibliothecaDataHelper saveChanges];
}

Looking at this in more detail: the first thing we do is to get hold of the Book object relating to the row that was just edited. Next, we get hold of the cell that was just edited, and check its column’s title. For a title or year column, we can cast the cell to an SDataGridMultiLineTextCell, which allows us to grab the updated text.

If the change was made to the title, we can simply update the book’s title property. If the change was made to the year, we need to parse it and check that it’s a valid number: if so, we update the book’s year property; if not, we just display an alert and change the cell’s value back to what it was before.

Finally, we save the changes made to the model.

If you run the project now, you’ll be able to edit the title and year columns – and when you stop and restart the app, your changes to the model will still be there.

Editing text

Currently you can also click to edit the author and publisher columns, but those changes don’t persist across restarts, because we’re not saving them in shinobiDataGrid:didFinishEditingCellAtCoordinate:. We don’t really want to edit them as text anyway: we want to be able to choose from the authors and publishers in the data store. So let’s use the PickerCell to do that.

Editing the relationships

We’re going to reuse the PickerCell created in a previous tutorial. So just grab the following files from GitHub and add them to your project:

Now, back in viewDidLoad in ViewController.m, edit the lines which create the author and publisher columns so that they create cells of type PickerCell:

    [self addColumnWithTitle:@"Author"
                 forProperty:@"Author"
                    cellType:[PickerCell class]
                       width:@160];
    [self addColumnWithTitle:@"Publisher"
                 forProperty:@"Publisher"
                    cellType:[PickerCell class]
                       width:@220];

We now need to implement the SDataGridDataSourceHelperDelegate method dataGridDataSourceHelper:populateCell:withValue:forProperty:sourceObject: to tell our SDataGridDataSourceHelper how to populate a picker cell:

// Called when the data source helper is populating a cell
- (BOOL)dataGridDataSourceHelper:(SDataGridDataSourceHelper *)helper
                    populateCell:(SDataGridCell *)cell
                       withValue:(id)value
                     forProperty:(NSString *)propertyKey
                    sourceObject:(id)object {
    if ([cell isKindOfClass:[PickerCell class]]) {
        // Create a picker cell to display the property
        PickerCell* pickerCell = (PickerCell*)cell;
        pickerCell.dataGrid = self.shinobiDataGrid;
        pickerCell.values = [_bibliothecaDataHelper
                             fetchAllEntitiesOfType:NSClassFromString(propertyKey)];
        for (int i=0; i<pickerCell.values.count; i++) {
            if ([[pickerCell.values[i] objectID] isEqual:[value objectID]]) {
                pickerCell.selectedIndex = i;
                break;
            }
        }
        
        return YES;
    }
    
    // Return 'NO' so that the datasource helper populates all the other cells in the grid.
    return NO;
}

So if the cell is a PickerCell instance, we first set it up to use our data grid. Next, we grab a list of all the entities of the type given by propertyKey (which in our case will either be “Author” or “Publisher”), and set those as the picker cell’s list of values. Finally, we iterate through the values to see which one matches the cell’s current value, and set the picker cell’s selected index when we find a match.

The final piece of the jigsaw is to change our shinobiDataGrid:didFinishEditingCellAtCoordinate: implementation to save the changes from the picker cell. Add in an else block as follows:

if ([cellTitle isEqualToString:@"Title"] || [cellTitle isEqualToString:@"Year"]) {
    …
} else {
    PickerCell* pickerCell = (PickerCell*) cell;
    NSManagedObject* obj = pickerCell.values[pickerCell.selectedIndex];

    if ([cellTitle isEqualToString:@"Publisher"]) {
        // Update the book's Publisher object
        [book.publisher removeBooksObject:book];
        Publisher *publisher = (Publisher*) obj;
        book.publisher = publisher;
        [publisher addBooksObject:book];
    } else if ([cellTitle isEqualToString:@"Author"]) {
        // Update the book's Author object
        [book.author removeBooksObject:book];
        Author *author = (Author*) obj;
        book.author = author;
        [author addBooksObject:book];
    }
}

So if we’ve just edited a picker cell, we grab its currently selected value, then update the book’s publisher or author to match the new selection, ensuring we make the changes to the old and new author/publisher as well as to the book itself.

If you run the app again now, when you double-click on an author or publisher, it will bring up a picker view to allow you to select the item you want:

Editing publisher

Again, you can stop and restart the app after making changes, and see that your changes have been persisted to the Core Data store.

Taking it further

This tutorial has hopefully shown how easy it is to use ShinobiGrids to display whatever you’ve got stored in Core Data. You could expand the project into more comprehensive Core Data editor by adding in functionality to add or delete rows, and by creating a view for each entity (maybe using the tabbed view from ShinobiEssentials to switch between them). So feel free to fork the GitHub project and make it do what you will!

Back to Blog