iPhone SDK Articles

Wednesday, February 11, 2009

SQLite Tutorial - Saving images in the database


There maybe times when you need to save more than just text in the database, like a file or an image. SQLite versions 3.0 and later allows you to store BLOB data in a column. Using the data type BLOB we can store large objects like an image or a file. Read on to learn more.
Introduction
Using BLOB data type we can store an image in the SQLite database. The data that actually gets stored in the database is the bytes that make up an image or a file. The is the sixth tutorial in SQLite tutorial series and borrows its source code from the previous one. We will change the "DetailView" by adding a new section which will be used to display or change the image. The image can only be changed when the view is in the edit mode. We will use a UIImagePickerController to display the photo album and to select an image.

Changing the database
Let's start by adding a new column to our "Coffee" database. Name the column "CoffeeImage" and set its data type to BLOB. This is how the database schema looks like in SQLite manager


Changing the Coffee Class
Add a new property to the "Coffee" class which will hold the image from the database. This is how the header and the implementation files look like

//Coffee.h
//Complete code listing not shown
@interface Coffee : NSObject {

NSInteger coffeeID;
NSString *coffeeName;
NSDecimalNumber *price;
UIImage *coffeeImage;
//Complete Code listing not shown ...
}
@property (nonatomic, readonly) NSInteger coffeeID;
@property (nonatomic, copy) NSString *coffeeName;
@property (nonatomic, copy) NSDecimalNumber *price;
@property (nonatomic, retain) UIImage *coffeeImage;
//Complete Code listing not shown ...

The new property is synthesized and released in the dealloc method

//Coffee.m
//Complete code listing now shown
@synthesize coffeeID, coffeeName, price, isDirty, isDetailViewHydrated, coffeeImage;

- (void)setCoffeeImage:(UIImage *)theCoffeeImage {

self.isDirty = YES;
[coffeeImage release];
coffeeImage = [theCoffeeImage retain];
}

- (void) dealloc {

[coffeeImage release];
[price release];
[coffeeName release];
[super dealloc];
}

Changing the Detail View
Let's add a new row to the detail view, which will be used to display the image or to change the image in edit mode. Since the style of the table view placed on the detail view is set to "Grouped", we will return three sections in numberOfSectionsInTableView and this is how the source code looks like

//DetailViewController.h
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tblView {
return 3;
}

Let's also display a title for the new section in tableView:titleForHeaderInSection. The code changes like this

//DetailViewController.m
- (NSString *)tableView:(UITableView *)tblView titleForHeaderInSection:(NSInteger)section {

NSString *sectionName = nil;

switch (section) {
case 0:
sectionName = [NSString stringWithFormat:@"Coffee Name"];
break;
case 1:
sectionName = [NSString stringWithFormat:@"Price"];
break;
case 2:
sectionName = [NSString stringWithFormat:@"Coffee Image"];
break;
}

return sectionName;
}

Displaying the photo album
Now we need to display a photo album when the section is selected in edit mode. From this album we can select a image to be saved in the database. Using a UIImagePickerController we can capture an image from the camera or the photo library on the device. UIImagePickerController class manages all the user interactions and all we have to do is display and dismiss it. Before we can display the view, we need to check if the interface is supported by calling isSourceTypeAvailable class method. The delegate of the UIImagePickerController should also confirm to UINavigationControllerDelegate and UIImagePickerControllerDelegate. In this case we will set the delegate of UIImagePickerController to "DetailViewController" and this is how the header and implementation file changes

//DetailViewController.h
@class Coffee, EditViewController;

@interface DetailViewController : UIViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITableViewDataSource, UITableViewDelegate> {

IBOutlet UITableView *tableView;
Coffee *coffeeObj;
NSIndexPath *selectedIndexPath;
EditViewController *evController;

UIImagePickerController *imagePickerView;
}

@property (nonatomic, retain) Coffee *coffeeObj;

@end

The variable "imagePickerView" is released in the dealloc method and the code listing is now shown here. We will initialize "imagePickerView" in viewDidLoad method and this is how the code changes

//DetailViewController.m
- (void)viewDidLoad {
[super viewDidLoad];

self.navigationItem.rightBarButtonItem = self.editButtonItem;

//Configure the UIImagePickerController
imagePickerView = [[UIImagePickerController alloc] init];
imagePickerView.delegate = self;
}

From the above code, "imagePickerView" is initialized and its delegate is set to self. This way "DetailViewController" will be notified of all the user actions on the picker controller. First, we need to display the view and it is done in tableView:didSelectRowAtIndexPath and this is how the code looks like

//DetailViewController.m
- (void)tableView:(UITableView *)tblView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

//Keep track of the row selected.
selectedIndexPath = indexPath;

if(evController == nil)
evController = [[EditViewController alloc] initWithNibName:@"EditView" bundle:nil];

//Find out which field is being edited.
switch(indexPath.section)
{
case 0:
evController.keyOfTheFieldToEdit = @"coffeeName";
evController.editValue = coffeeObj.coffeeName;

//Object being edited.
evController.objectToEdit = coffeeObj;

//Push the edit view controller on top of the stack.
[self.navigationController pushViewController:evController animated:YES];
break;
case 1:
evController.keyOfTheFieldToEdit = @"price";
evController.editValue = [coffeeObj.price stringValue];

//Object being edited.
evController.objectToEdit = coffeeObj;

//Push the edit view controller on top of the stack.
[self.navigationController pushViewController:evController animated:YES];
break;
case 2:
if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
[self presentModalViewController:imagePickerView animated:YES];
break;
}
}

First confirm if the photo library is available on the device or not, if it is then present the view to the user. The method imagePickerController:didFinishPickingImage:editingInfo is called when an image is selected. This is how the code looks like

//DetailViewController.m
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)dictionary {

coffeeObj.coffeeImage = image;
[tableView reloadData];
[picker dismissModalViewControllerAnimated:YES];
}

We get the selected image and set the "coffeeImage" property of the selected coffee. The table view is then reloaded and the picker view is dismissed.

Display the coffee image
We will display the coffee image in tableView:cellForRowAtIndexPath and this is how the code looks like

//DetailViewController.m
- (UITableViewCell *)tableView:(UITableView *)tblView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
}

switch(indexPath.section) {
case 0:
cell.text = coffeeObj.coffeeName;
break;
case 1:
cell.text = [NSString stringWithFormat:@"%@", coffeeObj.price];
break;
case 2:
cell.text = @"Change Image";
if(coffeeObj.coffeeImage != nil)
cell.image = coffeeObj.coffeeImage;
break;
}

return cell;
}

If the index of the section is 2 then we set the title of the cell to "Change Image" and the image to the coffee image.

Saving image in the SQLite database
Until now we haven't saved the image in the database because we only save data when the application is being terminated or if a memory warning is received. The data is saved in "saveAllData" method as seen in the previous tutorial. Let's change this method to save the image in the database which now has a new column called "CoffeeImage" to hold the image as bytes. This is how the code looks like

//Coffee.m
- (void) saveAllData {

if(isDirty) {

if(updateStmt == nil) {
const char *sql = "update Coffee Set CoffeeName = ?, Price = ?, CoffeeImage = ? Where CoffeeID = ?";
if(sqlite3_prepare_v2(database, sql, -1, &updateStmt, NULL) != SQLITE_OK)
NSAssert1(0, @"Error while creating update statement. '%s'", sqlite3_errmsg(database));
}

sqlite3_bind_text(updateStmt, 1, [coffeeName UTF8String], -1, SQLITE_TRANSIENT);
sqlite3_bind_double(updateStmt, 2, [price doubleValue]);

NSData *imgData = UIImagePNGRepresentation(self.coffeeImage);

int returnValue = -1;
if(self.coffeeImage != nil)
returnValue = sqlite3_bind_blob(updateStmt, 3, [imgData bytes], [imgData length], NULL);
else
returnValue = sqlite3_bind_blob(updateStmt, 3, nil, -1, NULL);

sqlite3_bind_int(updateStmt, 4, coffeeID);

if(returnValue != SQLITE_OK)
NSLog(@"Not OK!!!");

if(SQLITE_DONE != sqlite3_step(updateStmt))
NSAssert1(0, @"Error while updating. '%s'", sqlite3_errmsg(database));

sqlite3_reset(updateStmt);

isDirty = NO;
}

//Reclaim all memory here.
[coffeeName release];
coffeeName = nil;
[price release];
price = nil;

isDetailViewHydrated = NO;
}

We first rewrite the update query to include the CoffeeImage column. We also need a way to get the image data as bytes and this is where UIImagePNGRepresentation method helps us. We then bind the BLOB parameter using sqlite3_bind_blob method. The first parameter takes the update statement, the second parameter takes the index of the parameter value, the third one is the actual data itself, the fourth one is the length of the data which is being saved, and a pointer to a method which is responsible to clean up the data. If sqlite3_bind_blob method does not return SQLITE_OK then we display an error in NSLog. If there is no error while saving the data then we have successfully saved the image in the SQLite database on the iPhone/iPod.

Getting an image from the SQLite database
We now have to get the data from the database when the detail view is loaded. This is done in "hydrateDetailViewData" method and this is how the code changes

//DetailViewController.m
- (void) hydrateDetailViewData {

//If the detail view is hydrated then do not get it from the database.
if(isDetailViewHydrated) return;

if(detailStmt == nil) {
const char *sql = "Select price, CoffeeImage from Coffee Where CoffeeID = ?";
if(sqlite3_prepare_v2(database, sql, -1, &detailStmt, NULL) != SQLITE_OK)
NSAssert1(0, @"Error while creating detail view statement. '%s'", sqlite3_errmsg(database));
}

sqlite3_bind_int(detailStmt, 1, coffeeID);

if(SQLITE_DONE != sqlite3_step(detailStmt)) {

//Get the price in a temporary variable.
NSDecimalNumber *priceDN = [[NSDecimalNumber alloc] initWithDouble:sqlite3_column_double(detailStmt, 0)];

//Assign the price. The price value will be copied, since the property is declared with "copy" attribute.
self.price = priceDN;

NSData *data = [[NSData alloc] initWithBytes:sqlite3_column_blob(detailStmt, 1) length:sqlite3_column_bytes(detailStmt, 1)];

if(data == nil)
NSLog(@"No image found.");
else
self.coffeeImage = [UIImage imageWithData:data];

//Release the temporary variable. Since we created it using alloc, we have own it.
[priceDN release];
}
else
NSAssert1(0, @"Error while getting the price of coffee. '%s'", sqlite3_errmsg(database));

//Reset the detail statement.
sqlite3_reset(detailStmt);

//Set isDetailViewHydrated as YES, so we do not get it again from the database.
isDetailViewHydrated = YES;
}

The select query is changed to include CoffeeImage column and we use sqlite3_column_blob method to load the BLOB data into variable of type NSData. WE create an image of type NSData from imageWithData class method. The rest of the code works as described above.

This tutorial wasn't designed to display imags anywhere and is a direct result of all the emails I got asking, how to save images in the SQLite database. So if you choose an image which does not fit in the section, it will take up all the space on the view.

Conclusion
This tutorial explains how to save and read images from a SQLite database. We can use the same functionality to save files in the database. I hope you had fun and learnt something new with this tutorial.

Happy Programming,
iPhone SDK Articles



Attachments
Suggested Readings


14 comments:

Anonymous said...

Did you mean to completely leave out the EditViewController class set-up?

It is in the .zip file, but it would make the tutorial easier to follow if it were illustrated.

iPhone SDK Articles said...

The edit view controller is explained here http://www.iphonesdkarticles.com/2008/11/sqlite-tutorial-updating-data.html

Happy Programming,
iPhone SDK Articles

Americo Papaleo said...

I love the articles ... I was wondering how you would scale the image to fit on the screen better or present the image in a manner like they are in the contact list application ... Thanks

Raj said...

hey thanks for this. i have bookmarked it at http://www.iphonekicks.com/database/SQLite_Tutorial_Saving_images_in_the_database

Kevin, twitter @RyeMAC3 said...

First of, what an amazing tutorial and an amazing project! I am having a lot of fun playing around with this project and learning so much at the same time.

One thing I am trying to do is this: I have removed the coffeeImage from the table, made the table a little smaller in the view and have created a UIbutton at the top of the View that when pressed, opens up the Image Picker/camera View. So it looks similar to the Contact list. (I'd rather the coffeeImage be at the top in a square rather than at the bottom of the view in a cell.) Anyway, once the UIButton is pressed it launches my camera view. I take a picture, and was hoping that the new coffeeImage would go into the UIButton I created.

So my question is this, how do I set the background image of a UIButton to be my coffeeImage instead of putting it into a table cell like you did?

Thanks for your help.

iPhone SDK Articles said...

@Americo Papaleo

You can try something like this

CGSize size = CGSizeMake(20, 20);
UIGraphicsBeginImageContext(size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0, 20);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, 20, 20), image.CGImage);
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

FYI: This is not my solution but it does work.

Hope this helps.

Happy Programming,
iPhone SDK Articles

iPhone SDK Articles said...

@Kevin You can set the background of a button like this
[button setBackgroundImage:image forState:UIControlStateNormal];

This page http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIButton_Class/UIButton/UIButton.html#//apple_ref/occ/instm/UIButton/setBackgroundImage:forState: might help you.

Happy Programming,
iPhone SDK Articles

Patrick said...

I put in an image of type png into the database for the Latte coffee name, but when I press the Latte field, I get the following error: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error while creating detail view statement. 'no such column: CoffeeImage''

Not sure why this is happening as the column exists in the SQLite database and there is even a value in that column for that field. Can someone help?

Patrick said...

Please disregard the previous comment. The problem was with the iphone Simulator keeping a previous database in its memory from one of the previous SQL examples.

krye said...

I too was looking for a way to fix the way the image in displayed in the cell. Right now it shows up all wrong.

You gave an answer Americo Papaleo, but where would you put that code?

CGSize size = CGSizeMake(20, 20);
UIGraphicsBeginImageContext(size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0, 20);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, 20, 20), image.CGImage);
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

Thanks,
Kevin

Anonymous said...

This was one of my first learning examples and it help me a lot. Great job.

Just one question.

How to drop table from Xcode? I need to find way to delete all data and replace them with new one but.

Anonymous said...

Firstly, great articles - thanks so much.

I'm not sure if I've missed something but in the code listing under "Saving image in the SQLite database", towards the end we "//Reclaim all memory here.". Should we also be releasing the coffeeImage here - is this a memory leak?

Paul said...

Hi

First of all these tutorials are great, I have followed all of them and got to this final one. But all I get is TERMINATED DUE TO UNCAUGHT EXCEPTION when I click on any of the coffee items to get to the detail view.

I also downloaded your source code but again got the same error.

Please can you help

Paul said...

Thanks for these tutorials they are great!!

I am trying to complete this last one but all I get is TERMINATED DUE TO UNCAUGHT EXCEPTION everytime I run the application and click any of the coffee items. Please help!

I have tried downloading your source code and running it but all I get is the same error.