Tutorial: Storyboard app with Core Data

It was my original intention to make a universal application for iPhone and iPad but due to time constraints I’m going to throw out this tutorial on using Core Data instead. With a bit of luck it’ll help someone out! A quick note before you start, so you’re not confused later – one of the screenshots later is missing an add button. Don’t worry if yours has one – I moved a chunk of the tutorial to make it easier to follow.

This tutorial will probably take about half an hour if you’re following it carefully. Good luck!
 
 
 
 
 
 
 
 
 

What will the app do?

The application we will be making will be fairly simple. We will start with an empty project and design our core data layout (including some default data) which will be used in the main application and a login page. On successful login we will then display a table with a bunch of entries and associated thumbnails. We will include the ability to add and delete entries, and to use the camera to take the photo for each entry (or choose from an album). Here’s a shot of the screen flow for the application when finished….

It’s worth noting that this tutorial makes use of the Camera on iPhone. If you don’t have a camera, don’t worry – it’ll still work, but you’ll need to select images from an album. Also note that NO checking has been done to see if the camera is available, so if you select the option and you don’t have a camera – it WILL crash! You have been warned!

 

Okay, first fire up Xcode and create a new project. Choose the Empty Application template. For the product name, enter PictureList. Make sure the device family is set to iPhone and that Core Data and Use Automatic Reference Counting are both checked. At this stage, notice that using storyboards isn’t a tickable option. Don’t worry about it for now – we’ll add it manually a bit later on.

Once you’ve created your project, lets do a little housekeeping. First of all, I don’t like the way the Supported Files group is in with the App Delegate files, so lets drag that group out of there and place it just above Frameworks. Next, create a new group (right-click on the PictureList project icon at the top of the list and choose New Group. Click to select the new group and again to edit the title. Change it to Core Data. Now drag the .xcdatamodel file into your new group and drag the whole group further down the list (above Supporting Files for instance). Your initial layout should be similar to the inset image.

 

Designing our tables

We’re now going to set up our core data layout. Select PictureList.xcdatamodeld from inside your Core data group. This will open up the data model and it should be completely empty. We are going to add two entities here. One will store our list of authorised users and passwords for login to the app and the other will store our picture list.

Click the Add Entity icon at the bottom and call the entity Users. With this selected, click the + button at the bottom of the Attributes table and call the new attribute username (lowercase). Click the Type column and select String. Again, click the + button to create another attribute and call this password (again lower case). Change it to type String too. That’s all we need.

Now lets create the Pictures entity. Click Add Entity at the bottom of the screen and call the new entity Pictures. With it selected (you may need to select away and back again before it shows the attributes properly!) add a new attribute called title and make it of type String. Now add another entity called desc and make it of type String. Add a third attribute and call it smallPicture (note the case) and make it of type Binary Data. That’s all.

Your final output should be like this (in the order it’s displayed on screen)

Picture (entity)

  • desc (String)
  • smallPicture (Binary Data)
  • title (String)

Users (entity)

  • username (String)
  • password (String)
Good. Our Core Data is now set up and ready to roll. Before we leave the data model, we’re going to generate some accessors for our core data entities. These allow us access to our attributes in code. Select both of the entities (Pictures and Users) by holding down Shift and clicking each until highlighted. Then right-click the Core Data group (yes, the one where your file list is) and choose New File. On the left hand side choose Core Data and on the right choose NSManagedObject subclass. After you click Create, Xcode creates a class for each of the entities we selected. Handy! The project files should now look like it does below…

 

 

The Core Data Helper

If you’re a regular visitor to this site, you may have read about the Core Data Helper class which I sometimes use. It was originally written by Bjorn Sallarp and I’ve modified it over time to add some bits and pieces. We will be using the helper to access our data.

Right-click on the Core data group and choose New File. On the left hand side, choose Cocoa Touch and on the right, choose Objective-C class. Lets call this class CoreDataHelper and make it a subclass of NSObject.

Now, replace the code in the two newly created files with this…

CoreDataHelper.h

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
 
@interface CoreDataHelper : NSObject
 
// For retrieval of objects
+(NSMutableArray *)getObjectsForEntity:(NSString*)entityName withSortKey:(NSString*)sortKey andSortAscending:(BOOL)sortAscending andContext:(NSManagedObjectContext *)managedObjectContext;
+(NSMutableArray *)searchObjectsForEntity:(NSString*)entityName withPredicate:(NSPredicate *)predicate andSortKey:(NSString*)sortKey andSortAscending:(BOOL)sortAscending andContext:(NSManagedObjectContext *)managedObjectContext;
 
// For deletion of objects
+(BOOL)deleteAllObjectsForEntity:(NSString*)entityName withPredicate:(NSPredicate*)predicate andContext:(NSManagedObjectContext *)managedObjectContext;
+(BOOL)deleteAllObjectsForEntity:(NSString*)entityName andContext:(NSManagedObjectContext *)managedObjectContext;
 
// For counting objects
+(NSUInteger)countForEntity:(NSString *)entityName andContext:(NSManagedObjectContext *)managedObjectContext;
+(NSUInteger)countForEntity:(NSString *)entityName withPredicate:(NSPredicate *)predicate andContext:(NSManagedObjectContext *)managedObjectContext;
 
@end

CoreDataHelper.h

#import "CoreDataHelper.h"
 
@implementation CoreDataHelper
 
#pragma mark - Retrieve objects
 
// Fetch objects with a predicate
+(NSMutableArray *)searchObjectsForEntity:(NSString*)entityName withPredicate:(NSPredicate *)predicate andSortKey:(NSString*)sortKey andSortAscending:(BOOL)sortAscending andContext:(NSManagedObjectContext *)managedObjectContext
{
	// Create fetch request
	NSFetchRequest *request = [[NSFetchRequest alloc] init];
	NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:managedObjectContext];
	[request setEntity:entity];	
 
	// If a predicate was specified then use it in the request
	if (predicate != nil)
		[request setPredicate:predicate];
 
	// If a sort key was passed then use it in the request
	if (sortKey != nil) {
		NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortKey ascending:sortAscending];
		NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
		[request setSortDescriptors:sortDescriptors];
	}
 
	// Execute the fetch request
	NSError *error = nil;
	NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
 
	// If the returned array was nil then there was an error
	if (mutableFetchResults == nil)
		NSLog(@"Couldn't get objects for entity %@", entityName);
 
	// Return the results
	return mutableFetchResults;
}
 
// Fetch objects without a predicate
+(NSMutableArray *)getObjectsForEntity:(NSString*)entityName withSortKey:(NSString*)sortKey andSortAscending:(BOOL)sortAscending andContext:(NSManagedObjectContext *)managedObjectContext
{
	return [self searchObjectsForEntity:entityName withPredicate:nil andSortKey:sortKey andSortAscending:sortAscending andContext:managedObjectContext];
}
 
#pragma mark - Count objects
 
// Get a count for an entity with a predicate
+(NSUInteger)countForEntity:(NSString *)entityName withPredicate:(NSPredicate *)predicate andContext:(NSManagedObjectContext *)managedObjectContext
{
	// Create fetch request
	NSFetchRequest *request = [[NSFetchRequest alloc] init];
	NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:managedObjectContext];
	[request setEntity:entity];
	[request setIncludesPropertyValues:NO];
 
	// If a predicate was specified then use it in the request
	if (predicate != nil)
		[request setPredicate:predicate];
 
	// Execute the count request
	NSError *error = nil;
	NSUInteger count = [managedObjectContext countForFetchRequest:request error:&error];
 
	// If the count returned NSNotFound there was an error
	if (count == NSNotFound)
		NSLog(@"Couldn't get count for entity %@", entityName);
 
	// Return the results
	return count;
}
 
// Get a count for an entity without a predicate
+(NSUInteger)countForEntity:(NSString *)entityName andContext:(NSManagedObjectContext *)managedObjectContext
{
	return [self countForEntity:entityName withPredicate:nil andContext:managedObjectContext];
}
 
#pragma mark - Delete Objects
 
// Delete all objects for a given entity
+(BOOL)deleteAllObjectsForEntity:(NSString*)entityName withPredicate:(NSPredicate*)predicate andContext:(NSManagedObjectContext *)managedObjectContext
{
	// Create fetch request
	NSFetchRequest *request = [[NSFetchRequest alloc] init];
	NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:managedObjectContext];
	[request setEntity:entity];	
 
	// Ignore property values for maximum performance
	[request setIncludesPropertyValues:NO];
 
	// If a predicate was specified then use it in the request
	if (predicate != nil)
		[request setPredicate:predicate];
 
	// Execute the count request
	NSError *error = nil;
	NSArray *fetchResults = [managedObjectContext executeFetchRequest:request error:&error];
 
	// Delete the objects returned if the results weren't nil
	if (fetchResults != nil) {
		for (NSManagedObject *manObj in fetchResults) {
			[managedObjectContext deleteObject:manObj];
		}
	} else {
		NSLog(@"Couldn't delete objects for entity %@", entityName);
		return NO;
	}
 
	return YES;	
}
 
+(BOOL)deleteAllObjectsForEntity:(NSString*)entityName andContext:(NSManagedObjectContext *)managedObjectContext
{
	return [self deleteAllObjectsForEntity:entityName withPredicate:nil andContext:managedObjectContext];
}
 
@end

The contents of this class really aren’t that important. Basically it’s going to provide us with a shorter way to grab our objects from Core Data without having to write lots of code each time. If you really want to learn about what’s going on, I’ve fully commented it to help you understand – though it’s essentially just a convenience class.

 

Creating a storyboard

Because we created an empty project, using storyboarding wasn’t a selectable option for us at the time. Likewise if we had created a single view application, then we could have selected Storyboard but then Core Data wouldn’t have been an option (something I really don’t understand – more consistency required Apple!). Because we don’t have a storyboard yet, we need to create one and tell our application to use it.

Right-click on your Supporting Files group and choose New File. From the left hand side, choose User Interface and from the right, choose Storyboard. Make sure the device family is set to iPhone. Let’s call it PictureListStoryboard.

Great, we’ve created the storyboard. Now lets tell the app that we want to use this storyboard when the app launches. Click on the PictureList project icon at the top of your files list. On the right hand side, from the Main Storyboard drop down box choose PictureListStoryboard.

Finally, open up AppDelegate.m and within the didFinishLaunchingWithOptions: method, remove everything except ‘return YES;’. We no longer need this code because our storyboard will be taking over.

 

Inserting a default user into Core Data

When our app runs for the first time, Core Data won’t have any objects. So in order to use our login page we need to add at least one username and password which we can use. Most of the time when I’m adding default data I’d use XML which I would parse and add into Core Data the first time the app runs. But because we only need one user, I’m going to simply add an object into Core Data during the application launch. Clearly this wouldn’t usually be a good idea especially for a username and password!

Open up AppDelegate.m and add an #import for Users.h. In the didFinishLaunchingWithOptions: method, paste the following code at the start…

// Get a reference to the stardard user defaults
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
 
// Check if the app has run before by checking a key in user defaults
if ([prefs boolForKey:@"hasRunBefore"] != YES)
{
    // Set flag so we know not to run this next time
    [prefs setBool:YES forKey:@"hasRunBefore"];
    [prefs synchronize];
 
    // Add our default user object in Core Data
    Users *user = (Users *)[NSEntityDescription insertNewObjectForEntityForName:@"Users" inManagedObjectContext:self.managedObjectContext];
    [user setUsername:@"admin"];
    [user setPassword:@"password"];
 
    // Commit to core data
    NSError *error;
    if (![self.managedObjectContext save:&error])
        NSLog(@"Failed to add default user with error: %@", [error domain]);
}

So what’s happening here? Well, first of all we are importing the class for the Core Data ‘Users’ entity so we can access it’s attributes. Then in the didFinishLaunchingWithOptions: method, we first get a reference to the application user defaults (an easy place to store settings for your app). We then check to see if it has an entry called ‘hasRunBefore’. On first run, this won’t exist so we then set the key to prevent it running twice and recreating the default data. Finally we create a Users object and set the username and password attributes, before forcing Core Data to store our new object.

 

A quick recap!

Okay, so far we’ve created our empty project and tidied stuff up a bit. We then designed our core data model and manually added a storyboard (and pointed the project to it at launch). Next, we added a new convenience class to grab objects from Core Data. Finally we created a default user, to be inserted into Core Data on first run of the app.

If you were to run the project at this stage (which you’re welcome to do) it would yield a really unsatisfying black screen and an error message about a default view controller. This is expected because at the moment our storyboard doesn’t have anything in it. That’s going to change very soon!

 

The Login View Controller

Let’s jump in and do a bit of coding before we design our storyboard. We need to create a view controller for our login screen so that we can check login details before going into the main application.

Right-click the PictureList group and choose New file. Choose Cocoa Touch on the left and UIViewController Subclass on the right. We will call our new file LoginViewController and make it a subclass of UIViewController. We do NOT need a XIB file because we will be doing this in the storyboard. Now change the content of the .H and .M files to the following…

LoginViewController.h

#import <UIKit/UIKit.h>
 
@interface LoginViewController : UIViewController
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) IBOutlet UITextField *usernameField;
@property (strong, nonatomic) IBOutlet UITextField *passwordField;
 
@end

LoginViewController.h

#import "LoginViewController.h"
#import "CoreDataHelper.h"
 
@implementation LoginViewController
 
//  Synthesize accessors
@synthesize managedObjectContext, usernameField, passwordField;
 
//  When we are done editing on the keyboard
- (IBAction)resignAndLogin:(id)sender
{
    //  Get a reference to the text field on which the done button was pressed
    UITextField *tf = (UITextField *)sender;
 
    //  Check the tag. If this is the username field, then jump to the password field automatically
    if (tf.tag == 1) {
 
        [passwordField becomeFirstResponder];
 
    //  Otherwise we pressed done on the password field, and want to attempt login
    } else {
 
        //  First put away the keyboard
        [sender resignFirstResponder];
 
        //  Set up a predicate (or search criteria) for checking the username and password
        NSPredicate *pred = [NSPredicate predicateWithFormat:@"(username == %@ && password == %@)", [usernameField text], [passwordField text]];
 
        //  Actually run the query in Core Data and return the count of found users with these details
        //  Obviously if it found ANY then we got the username and password right!
        if ([CoreDataHelper countForEntity:@"Users" withPredicate:pred andContext:managedObjectContext] > 0)
 
            //  We found a matching login user!  Force the segue transition to the next view
            [self performSegueWithIdentifier:@"LoginSegue" sender:sender];
 
        else
 
            //  We didn't find any matching login users. Wipe the password field to re-enter
            [passwordField setText:@""];
    }
}
 
//  When the view reappears after logout we want to wipe the username and password fields
- (void)viewWillAppear:(BOOL)animated
{
    [usernameField setText:@""];
    [passwordField setText:@""];
}
 
@end

So if we first look at the .H file, you’ll see it’s pretty standard stuff. We’re creating IBOutlets for two UITextFields (our username and password entry fields). We are also adding a property to hold the managedObjectContext (MOC) which we will pass in later (remember, our login class will need to check in Core Data whether the user exists or not – so we need access to the MOC in order to perform that query).

Having a quick look in the .M file holds very little excitement either! There are only two methods here. The ‘resignAndLogin’ method is called whenever editing ends in one of the two UITextFields. I’ve used the ‘tag’ field to identify which box is which – we will set this in the storyboard later. We will use this tag to either jump to the next text field or if it’s the password field already, to attempt a login. At this stage, the code checks in Core Data to see if there is a user with the credentials that match those entered. If there is – we transition to the next view. Otherwise, login was denied. The second method here is simply to wipe the contents of the two login fields when the login page reappears after a logout.

Remember earlier I said we would need to pass our MOC to the loginViewController? We should do that now before we forget (or more accurately, before I forget). Open up your AppDelegate.m file and add an #import for LoginViewController.h to the top. Then add the following code to the start of the didFinishLaunchingWithOptions: method…

// Pass the managed object context to the root view controller (the login view)
LoginViewController *rootView = (LoginViewController *)self.window.rootViewController;
rootView.managedObjectContext = self.managedObjectContext;

What we’re doing here is grabbing a reference to our root view controller (which will be our login page) and passing the managed object context to it.

 

Getting the login page working

Right, lets open the picture list storyboard file. Drag in a new View Controller from the object library. This will automatically become the root view, shown by the arrow pointing to it. Now select the View Controller icon in the black bar at the bottom and under Identity Inspector in the utilities panel, change the Class to LoginViewController.

Now drag in two Text Fields and put them somewhere near the top of the view. Also drag in two UILabels and put them beside your text fields. Now use your creative ability to arrange them however you like, to form a login page. I’ve made mine look like this…

Now we need to connect up our text field outlets from the Login Class. Right-click drag (or CTRL and drag) from the Login View Controller icon in the black bar, to the Username field. Choose usernameField from the context list. Now do the same for the Password field, choosing passwordField from the list.

Our login method needs to know which of these text fields is which and if you recall from earlier, we are using a ‘tag’ number to keep track. Click the Username text field and under the Attributes Inspector of the utility panel, change the Tag field to 1. While we’re here, we should switch off Capitalization and correction, and set the return key to Done (all of which are dropdown options in the attributes inspector). Now do the same for the Password text field. Set the Tag field to 2 and make all the same changes as the previous text field. Also though, as this is a password field we should check the secure box to mask the password as it’s being entered.

We also need to tell the text views that when editing ends in either of the boxes, to call the method we wrote earlier – the one which handles the login. So right-click the Username text field and drag from the little circle to the right of Did end on Exit down to the Login View Controller icon in the black bar. Choose resignAndLogin from the context menu. Repeat this for the Password text field as both fields will point to the same class.

Finally, before we can see whether our login page is working we need a view to move to after a successful login. Drag in a Table View Controller to the right of the login view controller. Right-click and drag from the Login View Controller icon in the black bar under our login page (you may need to click it to reveal the icons) to the new Table View. Select modal from the context menu. This will create a segue link between the two. Now click on the segue we just created (the connector thingy) and under the Attributes Inspector, change the Identifier to LoginSegue (case is important). This should match exactly the label we used in the LoginViewController class and will be used to reference the transition to the next view from our login view.

That’s it! Go and test the application. You should find a working login page. Try out the login credentials (if you’ve been following the tutorial exactly, the username will be ‘admin’ and the password will be ‘password’. Note that both the name and password are case sensitive! On successfully entering the details you should be taken to a blank table view. Don’t worry about the project warning – this is just because our table isn’t yet configured.

 

Adding a navigation controller and logout

Before we start on our main table view we’re going to need a navigation controller to manage the flow of pages from the table onward. Select Table View Controller icon in the black bar under the table view you we just created and then go to the Editor menuEmbed InNavigation Controller. Xcode should slot a navigation controller between your login page and the table view. That was nice and easy!

Just to complete the login / logout part of this tutorial, lets add a button for logging out of the application. Drag a Bar Button Item from the objects list onto the left of the Table View navigation bar. Double-click it and change the name to Logout. We will wire this up later when we’ve made the class for this table view controller. Let’s also drag a Bar Button Item onto the right of the navigation bar and in the attributes inspector, change the identifier to Add. We will use this for adding new records.

 

Creating the PictureList Table

We’ve already added a Table View for our picture list so now we need to create a controller to do the heavy lifting. Right-click your PictureList group and choose New File. On the left, choose Cocoa Touch and on the right choose UIViewController subclass. Let’s call this one PictureListMainTable and make it a subclass of UITableViewController. Edit the .h and .m files to have the following content…

PictureListMainTable.h

#import <UIKit/UIKit.h>
 
@interface PictureListMainTable : UITableViewController
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSMutableArray *pictureListData;
 
- (void)readDataForTable;
 
@end

PictureListMainTable.m

#import "PictureListMainTable.h"
#import "CoreDataHelper.h"
#import "Pictures.h"
 
@implementation PictureListMainTable
 
@synthesize managedObjectContext, pictureListData;
 
//  When the view reappears, read new data for table
- (void)viewWillAppear:(BOOL)animated
{
    //  Repopulate the array with new table data
    [self readDataForTable];
}
 
//  Grab data for table - this will be used whenever the list appears or reappears after an add/edit
- (void)readDataForTable
{
    //  Grab the data
    pictureListData = [CoreDataHelper getObjectsForEntity:@"Pictures" withSortKey:@"title" andSortAscending:YES andContext:managedObjectContext];
 
    //  Force table refresh
    [self.tableView reloadData];
}
 
#pragma mark - Actions
 
//  Button to log out of app (dismiss the modal view!)
- (IBAction)logoutButtonPressed:(id)sender
{
    [self dismissModalViewControllerAnimated:YES];
}
 
#pragma mark - Table view data source
 
//  Return the number of sections in the table
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}
 
//  Return the number of rows in the section (the amount of items in our array)
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [pictureListData count];
}
 
//  Create / reuse a table cell and configure it for display
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
 
    // Get the core data object we need to use to populate this table cell
    Pictures *currentCell = [pictureListData objectAtIndex:indexPath.row];
 
    //  Fill in the cell contents
    cell.textLabel.text = [currentCell title];
    cell.detailTextLabel.text = [currentCell desc];
 
    //  If a picture exists then use it
    if ([currentCell smallPicture])
    {
        cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
        cell.imageView.image = [UIImage imageWithData:[currentCell smallPicture]];
    }
 
    return cell;
}
 
//  Swipe to delete has been used.  Remove the table item
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete)
    {
        //  Get a reference to the table item in our data array
        Pictures *itemToDelete = [self.pictureListData objectAtIndex:indexPath.row];
 
        //  Delete the item in Core Data
        [self.managedObjectContext deleteObject:itemToDelete];
 
        //  Remove the item from our array
        [pictureListData removeObjectAtIndex:indexPath.row];
 
        //  Commit the deletion in core data
        NSError *error;
        if (![self.managedObjectContext save:&error])
            NSLog(@"Failed to delete picture item with error: %@", [error domain]);
 
        // Delete the row from the data source
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }   
}
 
@end

Now this is a fairly big chunk of code, so let’s take a look at what’s going on in here. First of all in the .H file we can see that we’re declaring a couple of properties. One will hold a reference to the managedObjectContext and one is an array which will hold our picture list data. We also declare a method for reading in our objects from Core Data into the array.

In the .M file we first overrride the viewWillAppear method and call our method to pull in our objects from Coe Data. ViewWillAppear is fired off any time the view is about to appear, so when the picture list first appears but also when we have finished adding or editing an item. Next we have the method for reading in the data using the CoreDataHelper followed by a forced refresh of the table.

Under this we have a method called ‘logoutButtonPressed’ which will be called when the logout button is pressed (no, really?). This simply dismisses the modal view and goes back to the login page.

Next are the usual tableview methods for returning the number of sections and rows in the table. The cellForRowAtIndexPath method generates or reuses a table cell and configures the content based on what’s required. Here we are first of all grabbing a reference to our Core Data object stored in the array (based on the table index). We then use that data to write the textLabel and detailTextLabel for the cell (we will be choosing a specific type of cell which has these labels built in already). Underneath this we check to see if a picture has been set for this table row. If it has, we change the cell image to use the picture.

The commitEditingStyle method handles deletions of cells in the table by first grabbing the object we want to delete and then removing it from Core Data, from the array and finally deleting the table row.

That’s all we need for now. A little later we will add an override for the prepareForSegue: method to handle passing the selected object to our editing page. All the code is well commented if you care to read through it for a better understanding, line by line.

Now lets hop back to our Storyboard and wire some stuff up. First of all, select the Table View Controller icon on the black bar under the Table View and in the Identity Inspector, change the class to PictureListMainTable. Then go to the Attributes Inspector and select the Table View itself. Change the style to Grouped and the Separator to Single Line. Now select the prototype cell and in the attributes inspector, change the style to Subtitle. This allows us to use a title, subtitle and an image without having to create those things ourselves. Also set the Identifier to ‘Cell’ and change the height of the cell to 84 (in the size inspector). Finally, right-click drag from the Logout button to the Picture List Main Table icon (in the black bar) and choose logoutButtonPressed from the context menu. This will get our logout working.

If we were to run the project at this stage, we’d get an error about the NSManagedObjectModel. If you remember, we need to pass the managedObjectContext to each view which will use it and so far, we haven’t done that for this table. So open up the LoginViewController.m file and add the following method to the bottom (above @end)…

//  When we have logged in successfully, we need to pass the managed object context to our table view (via the navigation controller)
//  so we get a reference to the navigation controller first, then get the last controller in the nav stack, and pass the MOC to it
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    UINavigationController *navController = (UINavigationController *)[segue destinationViewController];
    PictureListMainTable *piclist = (PictureListMainTable *)[[navController viewControllers] lastObject];
    piclist.managedObjectContext = managedObjectContext;
}

Also remember to add an #import for PictureListMainTable.h to the top of the file! Now go test your project. You should be able to login and see the picture list table and your logout button will work too! But wait… why is the table empty? Well, we haven’t yet added any records to view. Rest assured that when we do, they will start appearing. In fact, lets do it now!

 

Adding the detail view

When a row in our picture list table is selected, we need to be able to view the details of the item. We also need to be able to add and edit, so we will use the same view for this. We will create a static table for doing all of this, but first lets create the subclass that will be running the show.

Right-click the PictureList group and choose New File. Select Cocoa Touch on the left and choose UIViewController subclass on the right. We will call it PictureListDetail and make it a subclass of UITableViewController. Replace the code in the .H and .M files with this…

PictureListDetail.h

#import <UIKit/UIKit.h>
#import "Pictures.h"
 
@interface PictureListDetail : UITableViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate>
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
 
@property (strong, nonatomic) Pictures *currentPicture;
 
@property (strong, nonatomic) IBOutlet UITextField *titleField;
@property (strong, nonatomic) IBOutlet UITextField *descriptionField;
@property (strong, nonatomic) IBOutlet UIImageView *imageField;
 
@property (strong, nonatomic) UIImagePickerController *imagePicker;
 
@end

PictureListDetail.m

#import "PictureListDetail.h"
 
@implementation PictureListDetail
 
@synthesize managedObjectContext;
@synthesize currentPicture;
@synthesize titleField, descriptionField, imageField;
@synthesize imagePicker;
 
#pragma mark - View lifecycle
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // If we are editing an existing picture, then put the details from Core Data into the text fields for displaying
    if (currentPicture)
    {
        [titleField setText:[currentPicture title]];
        [descriptionField setText:[currentPicture desc]];
        if ([currentPicture smallPicture])
            [imageField setImage:[UIImage imageWithData:[currentPicture smallPicture]]];
    }
}
 
#pragma mark - Button actions
 
- (IBAction)editSaveButtonPressed:(id)sender
{
    // If we are adding a new picture (because we didnt pass one from the table) then create an entry
    if (!currentPicture)
        self.currentPicture = (Pictures *)[NSEntityDescription insertNewObjectForEntityForName:@"Pictures" inManagedObjectContext:self.managedObjectContext];
 
    // For both new and existing pictures, fill in the details from the form
    [self.currentPicture setTitle:[titleField text]];
    [self.currentPicture setDesc:[descriptionField text]];
 
    if (imageField.image)
    {
        // Resize and save a smaller version for the table
        float resize = 74.0;
        float actualWidth = imageField.image.size.width;
        float actualHeight = imageField.image.size.height;
        float divBy, newWidth, newHeight;
        if (actualWidth > actualHeight) {
            divBy = (actualWidth / resize);
            newWidth = resize;
            newHeight = (actualHeight / divBy);
        } else {
            divBy = (actualHeight / resize);
            newWidth = (actualWidth / divBy);
            newHeight = resize;
        }
        CGRect rect = CGRectMake(0.0, 0.0, newWidth, newHeight);
        UIGraphicsBeginImageContext(rect.size);
        [imageField.image drawInRect:rect];
        UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
 
        // Save the small image version
        NSData *smallImageData = UIImageJPEGRepresentation(smallImage, 1.0);
        [self.currentPicture setSmallPicture:smallImageData];
    }
 
    //  Commit item to core data
    NSError *error;
    if (![self.managedObjectContext save:&error])
        NSLog(@"Failed to add new picture with error: %@", [error domain]);
 
    //  Automatically pop to previous view now we're done adding
    [self.navigationController popViewControllerAnimated:YES];
}
 
//  Pick an image from album
- (IBAction)imageFromAlbum:(id)sender
{
    imagePicker = [[UIImagePickerController alloc] init];
    imagePicker.delegate = self;
    imagePicker.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
    [self presentViewController:imagePicker animated:YES completion:nil];
}
 
//  Take an image with camera
- (IBAction)imageFromCamera:(id)sender
{
    imagePicker = [[UIImagePickerController alloc] init];
    imagePicker.delegate = self;
    imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
    imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
    [self presentViewController:imagePicker animated:YES completion:nil];
}
 
//  Resign the keyboard after Done is pressed when editing text fields
- (IBAction)resignKeyboard:(id)sender
{
    [sender resignFirstResponder];
}
 
#pragma mark - Image Picker Delegate Methods
 
//  Dismiss the image picker on selection and use the resulting image in our ImageView
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo
{
    [imagePicker dismissModalViewControllerAnimated:YES];
    [imageField setImage:image];
}
 
//  On cancel, only dismiss the picker controller
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    [imagePicker dismissModalViewControllerAnimated:YES];
}
 
@end

Now lets see what’s going on here. First in the .H file, we are making our subclass the delegate for UINavigationController and UIImagePickerController. This is necessary because later we will be using the camera and we are going to use our table class to control what happens after the camera has been used (or cancelled). We’ll get back to that a bit later.

Next in the .H file we have a property to hold the managedObjectContext which is required to query Core Data (notice how we are passing this from view to view). Then we have a property which will hold the item we select in our table. We then have three properties for our two text fields (title and description) and an image view. Finally, we have a property to access our image picker.

Open up the .M file. It looks horrible but it’s really not too bad. We start with the viewDidLoad method in which we check to see if we have passed an object from our table (i.e. we are checking to see if we selected an item in the table). If we did, then we try to populate the text fields with the existing data and set the image. If we didn’t then we are adding a new record, so it’s not necessary.

Next we have the editSaveButtonPressed: method which will be called when our Save button is pressed. Stepping through this we first create a new Core Data object if we haven’t passed one from the table. We then set the objects properties to the contents of our text fields (or overwrite the existing text if we are editing a record). We then check to see if an image is being held in the UIImageView. The next bit looks complicated but all we are doing is resizing the image so it’s a thumbnail size. If we used the full sized image it would grind our table view to a halt. After resizing, we save the image to Core Data. Ordinarily you probably wouldn’t want to store images in Core Data but it serves the purpose for this tutorial. Note that the data is converted to an NSData object before it’s stored and likewise when it’s used in the table. Finally in this method, we pop the view controller to return to our table.

The next two methods (imageFromAlbum: and imageFromCamera:) are outlets for buttons we will be creating shortly. They will allow us to either use the camera to take a photo or choose one from an album. Next we have a resignKeyboard: method which will put away the keyboard when the Done button has been pressed (similar to the Login controller).

Finally we have two Image Picker delegate methods. One for when an image was selected, and another for when it was cancelled. Both methods dismiss the image picker as this is up to the coder to take care of. However when an image WAS selected, we also put it straight into our UIImageView so it can be saved later.

One more small change to make here. At the moment, our class doesn’t know about the managedObjectContext. We need to pass this from our previous table view, so we will use prepareForSegue: method to do it. Open up PictureListMainTable.m and add the following method to the bottom…

//  When add is pressed or a table row is selected
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    //  Get a reference to our detail view
    PictureListDetail *pld = (PictureListDetail *)[segue destinationViewController];
 
    //  Pass the managed object context to the destination view controller
    pld.managedObjectContext = managedObjectContext;
 
    //  If we are editing a picture we need to pass some stuff, so check the segue title first
    if ([[segue identifier] isEqualToString:@"EditPicture"])
    {
        //  Get the row we selected to view
        NSInteger selectedIndex = [[self.tableView indexPathForSelectedRow] row];
 
        //  Pass the picture object from the table that we want to view
        pld.currentPicture = [pictureListData objectAtIndex:selectedIndex];
    }
}

Also remember to add an #import for PictureListDetail.h at the top.

What we’re doing here is fairly straight forward. When an item is selected in the table, we will grab a reference to the PictureListDetail controller and pass the managedObjectContext. We also check the segue identifier to see if we added a record, or selected one. If we selected one, we pass the selected object to our controller so we can edit the details.

 

The final storyboarding

Okay, lets finish this! Open up your storyboard again and drag in a TableView Controller to the right of the existing one. Select the Table View Controller icon on the black bar underneath it and in the Identity Inspector, change the class to PictureListDetail. Now select the table view itself and in the attributes inspector, change the content dropdown to Static Cells. Also change the sections to 2 and the style to Grouped.

Lets connect our two tables. Right-click drag from the Add button to the Picture List Detail table view and choose Push from the context menu. Single-click the Segue and change the identifier to AddPicture (note the case!). Next, select the single cell in our Picture List Main Table and right-click drag it to our Picture List Detail table and select Push from the menu. This has wired up both the add and cell selection to our new table (remember, the code will take care of figuring out which one was selected using the identifier). Select the segue for the second segue and change the identifier to EditPicture. You can tell which segue is which by selecting it. It will highlight what the segue connects by outlining it.

Now – over to our new table view. Delete two of the cells in the top section leaving only one and change the height of it to around 95. You can do this either by dragging the cell edge at the bottom or using the Size Inspector, checking the Custom box and entering 95 in the Row Height box.

Next, drag an Image View into the cell and put it on the left hand side. Make it’s size around 80 x 80 pixels (in the size inspector). Then drag two buttons to the right of it. Name one Pick from album and the other Take a photo.

For the second section, delete one of the cells leaving two. Drag a label into the left of the first cell and change it to Title:. Do the same for the second cell and change the label to Description:. I right-justified mine and changed the size and colour as you’ll see in the image. Next, drag a Text Field into the first cell to the right of the label. Under the Attributes Inspector, change the text field properties so that it has an invisible border and give it a place holder description. Do the same for the Description cell.

Now drag a Bar Button Item onto the right hand side of the navigation bar. In the Attributes Inspector, change the Identifier to Save. Also double click the navigation bar and change the title to Picture Details. Your finished layout should look like the one on the left.

Now lets do the final wiring and we can test this out. First lets wire up the Save button. Right-click drag it to the Picture List Detail icon in the black bar and choose editSaveButtonPressed.

Now right-click drag from the Picture List Detail icon in the black bar, to the Image View and select imageField from the menu. Do the same again for the two Text Fields and select the appropriate property (titleField and descriptionField). Right-click the first Text Field and drag the circle to the right of Did end on exit to the Picture List Detail icon and select resignKeyboard. Do the same for the second Text Field again choosing resignKeyboard.

Finally, right-click drag from the Pick from Album button to the Picture List Detail icon and choose imageFromAlbum. Do the same for the Take a photo button and choose imageFromCamera.

Your finished storyboard should look similar to this…

 

And we’re finally there!

That’s about all there is! I did other small cosmetic changes to make my app look nice, such as changing font sizes and the way images appear in the image views – all of which you can do yourself by messing with some attributes in the inspector. I should also mention that there seems to be a bug in the storyboard editor with changing table row height. If you set it in the Size inspector it doesn’t work. You’ll need to drag the cell to set the size. I’ll have to confirm whether this is correct but certainly setting the size directly didn’t work for me!

Anyway, what you waiting for? Go fire up your new app! You can login in, add items to your list, delete items (by swiping a row), take pictures with your camera or choose from an album and it’s all stored in Core Data.  Nice work.  Remember, if you don’t have a camera and you test this on your device – it will crash if you try to use the Take a picture button! Now go have a nice cup of tea and pat yourself on the back for the mammoth effort you’ve put in.

You can get the final project file here.

PictureList tutorial finished project


Comments are now closed for this tutorial but I’d still love to hear from you. You can post in our integrated forum by clicking this link (or by going to the Discussion Forum tab at the top of the page)

12 Comments.

  1. Saving data to be shown later - pingback on January 16, 2012 at 12:26 am
  2. Use Storyboards in iOS Empty Application (Xcode 4.3) | Rick's Blog - pingback on February 25, 2012 at 10:15 pm
  3. 10 Fresh iPhone Tutorials For Developers – Script Tutorials - pingback on April 3, 2012 at 5:17 pm
  4. Github és társai avagy hasznos források « PecsXcode - pingback on April 18, 2012 at 3:49 pm
  5. XCode4.2 For Snow Leopard - Glob Studio - pingback on April 18, 2012 at 6:12 pm
  6. 分享10个iphone开发教程(英文) | iFanBar - pingback on December 15, 2012 at 1:31 pm
  7. Coredata helper | BlogoSfera - pingback on October 2, 2013 at 7:02 am
  8. Coredata helper - QueryPost.com - pingback on October 2, 2013 at 7:54 am