iPhone SDK Articles

Sunday, March 29, 2009

Drill down table view with a detail view


Since I published my drill down table view tutorial, I got a lot of positive feedback but with one common request. How do I load a different detail view for an item or how do I load a UITabBarController in the detail view. In this tutorial I will show how to load a different detail view.
Introduction
Using a drill down table view we can display hierarchical data where the last view is responsible of displaying some detail information. This view in which the detail information is displayed can be the same for all the data in the table view (like my last tutorial) or it can be different based on the path that the user took. In this tutorial I will show you how to add a different detail view based on the path the user took to get to the last item. This tutorial is based on the UITableView - Drill down table view tutorial and borrows its source code.

This is how one of the detail view looks like
Loading a detail view
Let's take a step back and think about the browser and the HTML it renders. The browser doesn't know what it is going to display and it does not even know the style it should set for the HTML. All that information is present in the HTML and the browser simply renders everything on the screen.

Does this mean that to display different data, I need different views even though the view looks the same? No, if the view looks the same you can reuse the same view again to display different data.

Let us think about the navigation controller as the browser and the data in the plist file as the HTML. If we want to tell the browser to style certain elements of the page, we would put that information in the HTML. In a similar fashion, we can put the detail view information in the plist file which the navigation controller will display. You only need to set this information in the last node of the plist file. When it is time to display the detail view, the code can look at this field and show the right detail view to the user.

Let us change the plist file from the last tutorial to include some information about the detail view.

Changing the plist file
Here we will add a new item to the last child; the title that gets displayed before the detail view is shown. The item will have a title of "View", type will be number and the value will be 1, 2, or 3. Where 1 = display a UITabBarController in the detail view, 2 = display a detail view with an image in it, and 3 = display a simple detail view.

Your plist file should look like this for the four items







Recap
We have added an extra item at the end of called "View" whose type is integer and value is either 1, 2, or 3. Where 1 = load a detail view UITabBarController, 2 = load a detail view with an image and 3 = load a simple detail view. Let's see how to add a detail view with a UITabBarController

Adding a UITabBarController to the detail view
Let's add a UITabBarController to the detail view. We will need an outlet of type UITabBarController, so lets create that in the header file of RootViewController. The code changes like this

//RootViewController.h
@interface RootViewController : UITableViewController {

NSArray *tableDataSource;
NSString *CurrentTitle;
NSInteger CurrentLevel;
IBOutlet UITabBarController *tbController;
}

@property (nonatomic, retain) NSArray *tableDataSource;
@property (nonatomic, retain) NSString *CurrentTitle;
@property (nonatomic, readwrite) NSInteger CurrentLevel;

@end

The dealloc method changes like this

//RootViewController.m
- (void)dealloc {
[tbController release];
[CurrentTitle release];
[tableDataSource release];
[super dealloc];
}

Launch Interface Builder by double clicking "RootViewController.xib" file and drag and drop a UITabBarController in the nib file. Select File's Owner and select the outlet "tbController" and drag it over to the UITabBarController and release. Now we have connected the outlet tbController to the UITabBarController. Add two views in the UITabBarController and change the title of the UITabBarItems to say "View One" and "View Two". Also add a label to both the views which would say "View One" for the first and "View Two" for the second. This is all you have to do in the Interface Builder.

Display the detail view
We now have to display the UITabBarController as a detail view when an item is selected in the UITableView. Before we do that let's stop for a while and see what we did. We added a UITabBarController in the same nib file as the RootViewController instead of creating a different nib file. When displaying a detail view, normally we would initialize the view controller and push it on top of the navigation controller. If we try to do the same here we would see the same table view again, which we do not want. The trick here is to switch the view of the RootViewController from the table view to the view of the tbController. Let's see how to do this; the code for tableView:didSelectRowAtIndexPath changes like this

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

//Get the dictionary of the selected data source.
NSDictionary *dictionary = [self.tableDataSource objectAtIndex:indexPath.row];

//Get the children of the present item.
NSArray *Children = [dictionary objectForKey:@"Children"];

if([Children count] == 0) {
NSInteger ViewNumber = [[dictionary objectForKey:@"View"] integerValue];
switch (ViewNumber) {
case 1: {
RootViewController *rvc = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:[NSBundle mainBundle]];
//Switch the view here
rvc.view = tbController.view;
[self.navigationController pushViewController:rvc animated:YES];
[rvc release];
}
break;
case 2:
break;
case 3: {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
break;
default: {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
break;
}
}
else {

//Prepare to tableview.
RootViewController *rvController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:[NSBundle mainBundle]];

//Increment the Current View
rvController.CurrentLevel += 1;

//Set the title;
rvController.CurrentTitle = [dictionary objectForKey:@"Title"];

//Push the new table view on the stack
[self.navigationController pushViewController:rvController animated:YES];

rvController.tableDataSource = Children;

[rvController release];
}
}

That is the complete code listing for tableView:didSelectRowAtIndexPath method but it does a lot of things and we only need to concentrate on some of the code, so teh condensed version is listed below

//RootViewController.m - tableView:didSelectRowAtIndexPath
if([Children count] == 0) {
NSInteger ViewNumber = [[dictionary objectForKey:@"View"] integerValue];
switch (ViewNumber) {
case 1: {
RootViewController *rvc = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:[NSBundle mainBundle]];
//Switch the view here
rvc.view = tbController.view;
[self.navigationController pushViewController:rvc animated:YES];
[rvc release];
}
break;
case 2:
break;
case 3: {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
break;
default: {
DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
break;
}
}

The above code first checks if the present item has any children or not, if it doesn't then we look for the value of the item "View". If it is 1, we know that we have to display a detail view with a UITabBarController. As mentioned before we need to switch the view property of the controller to the view of the tab bar controller, which we do after initializing the controller. At last we ask the navigation controller to display our view which is connected to the controller.

Test it out to see how it works.

We also mentioned that if the "View" has a value of 3 then we would display the simple detail view. So drill down Item 3/4 and see the result. Don't do that with Item 2 yet because it will not work.

Adding a detail view which will display an image.

Hopefully by now you have a clear idea of how to display different detail views when working with a drill down app.

To display a view with an image create a new view and a new view controller and name the view "ImageView" and the view controller "ImageViewController". Open the ImageView in IB and set the class to be "ImageViewController" and connect the view outlet from the File's Owner to the view. Drag and drop a UIImageView from the library on the view. We will set the image of this UIImageView when the view loads and the name of the image will be passed from the root view controller. Since we need to display an image in the image view from Xcode we need an outlet of type UIImageView in the Interface Builder. The header file of "ImageViewController" changes like this

//ImageViewController.m
@interface ImageViewController : UIViewController {

IBOutlet UIImageView *imgView;
NSString *ImageName;
}

@property (nonatomic, retain) NSString *ImageName;

@end

We have also declared a property called "ImageName" which the root view controller will use to pass the name of the image to the image view controller and the image view controller will display the image in the image view.

This is how the viewDidLoad method changes

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

NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *ImagePath = [Path stringByAppendingPathComponent:ImageName];
UIImage *tempImg = [[UIImage alloc] initWithContentsOfFile:ImagePath];
[imgView setImage:tempImg];
[tempImg release];
}

The dealloc method looks like this

//ImageViewController.m
- (void)dealloc {
[ImageName release];
[imgView release];
[super dealloc];
}

Displaying the detail view
Let's go back to the RootViewController.m file and import "ImageViewController.h" file at the top and it should look like this

//RootViewController.m
#import "RootViewController.h"
#import "DrillDownAppAppDelegate.h"
#import "DetailViewController.h"
#import "ImageViewController.h"

@implementation RootViewController
...

In tableView:didSelectRowAtIndexPath we will now display the image detail view when the "ViewNumber" is set to 2. This is how the code looks like

//RootViewController.m - tableView:didSelectRowAtIndexPath method
case 2: {
ImageViewController *ivc = [[ImageViewController alloc] initWithNibName:@"ImageView" bundle:[NSBundle mainBundle]];
ivc.ImageName = @"Image.jpg";
[self.navigationController pushViewController:ivc animated:YES];
[ivc release];
}
break;

In the above code we initialize the "ImageViewController" and set the name of the image that it should display and ask the navigation controller to display it. Do not forget to place an image in the "Resources" folder.

Build and Run to see how it works.

Conclusion
I hope this helps you in some way on how to load a detail view with a UITabBarController. If you have any questions on loading a different detail view please take a look at this tutorial. Comments are most welcome and if you have any questions please send me an email at iphonearticles[@]gmail[dot]com.

Happy Programming,
iPhone SDK Articles



Attachments
Suggested Readings


Read more...

Friday, March 27, 2009

SQLite and CoreData


Yes, Apple has finally ported Core Data to the iPhone which means using SQLite in an application is not the best approach. Once the OS 3.0 is officially released, I will be posting more tutorials on using Core Data. Thank you for all your continued support.

Yes, Apple has finally ported Core Data to the iPhone which means using SQLite in an application is not the best approach. Once the OS 3.0 is officially released, I will be posting more tutorials on using Core Data. Thank you for all your continued support. continued support.

Happy Programming,
iPhone SDK Articles


Read more...

Sunday, March 8, 2009

UITableView - Drill down table view tutorial


Until now we saw how to create a simple table but there are times when we need to show another table view when a row is selected. This kind of a flow is called "drill down" and in this tutorial we learn how to create a drill down table view. Click on "Read more..." to learn moreIntroduction
Using a drill down table view we can display some primary level of items, where a selected row will result in displaying a new table view with data related to the item selected. The process is repeated until an item has no sub level categories and it is then we display a detail view which is not a UITableView. This is the seventh tutorial in the UITableView series and does NOT inherit its source code from the previous tutorial.

Creating the project
Create a new project in XCode by selecting "Navigation-Based Application", I have named my project "DrillDownApp" which is used in this tutorial.

Using nothing but a single UITableView
Instead of creating 3 different UITableView's to display three different levels, we will use the same table view and the same table view controller. This philosophy is borrowed from web programming, where we only have one page to display a list of products.

Designing the flow
Before we start writing any code, let's decide the flow of the application with some specifics. Below is a table of how the data movies when we select an item.

First LevelSecond LevelThird LevelFourth LevelFifth LevelSixth Level
Item 1Screen ADetail ViewN/AN/AN/A
Item 2Screen BScreen CDetail ViewN/AN/A
Item 3Screen DScreen EScreen FDetail ViewN/A
Item 4Screen GScreen HScreen IScreen JDetail View

When the application is launched we will see the following items; "Item 1", "Item 2", "Item 3", and "Item 4". If "Item 1" is selected then we see the table view display "Screen A" and upon selecting "Screen A" we will see the detail view load. The same goes for "Item 2", which will load a table view with one value called "Screen B" -> "Screen C" -> Detail view and so on so forth for the remaining values in the first table view.

Creating the data source
The data source for this tutorial will come from an XML file or a plist file. Create a new file under the "Resource" folder by clicking on File -> New File -> (Under MAC OS X) select Other -> Property List and save this new file by giving it a name of "Data.plist". Let's start by adding values to this property list file.

Select the "Root" element and create a new item by clicking on the arrows at the right.


Name the item "Rows" and change its type to "Array". This is the array which will contain all the primary level items with its children. The user should be able to select an item in a table view and load another table view with all items under the selected item in the first table view. For us to achieve this, our array should contain a dictionary with at least two types of fields; string and a array. The string type will hold the value of the cell and the array will hold the children rows, which again will contain a n number of dictionaries where n is the number of children rows an item will have.

Create a new item "Rows" and set its type to "Dictionary", you do not have to change the key of the dictionary. Select the dictionary item and create a new item and set its data type to string and name the key to "Title". The name of the key which should not be changed throughout the property list file, since this is the key which will contain the value of the cell which shows up in the UITableViewCell. Set its value to "Item 1" since this is the first item that shows up in the table view. From the above table we know that selecting "Item 1" should reload the table view to display "Screen A". Create another field under "Item 1" and change its type to an Array and set its key to "Children". Under the "Children" item create another item of type "Dictionary" which will only contain a string value called "Screen A" with its key set to "Title". If "Screen A" is selected we load the detail view; so we do not have to add any children at this level. Add the remaining items from the table in the property list and when you are done the property list should look like this.

Populating the data source
Now that we have created the data source, the next step is to populate it which will be done in the application delegate. Since the root element of data.plist is of type "Dictionary", we will create a variable of type NSDictionary to hold all the data from the property list file. The header file of the application delegate changes like this

//DrillDownAppDelegate.h
@interface DrillDownAppAppDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;
UINavigationController *navigationController;

NSDictionary *data;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@property (nonatomic, retain) NSDictionary *data;

@end

The variable "data" will hold our data source which is also synthesized in the implementation file and released in the dealloc method (code not shown here).

This is how the data is populated in applicationDidFinishLaunching method

//DrillDownAppDelegate.m
- (void)applicationDidFinishLaunching:(UIApplication *)application {

NSString *Path = [[NSBundle mainBundle] bundlePath];
NSString *DataPath = [Path stringByAppendingPathComponent:@"data.plist"];

NSDictionary *tempDict = [[NSDictionary alloc] initWithContentsOfFile:DataPath];
self.data = tempDict;
[tempDict release];

// Configure and show the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
}

We read the "data.plist" file and read all of its data in a dictionary object using initWithContentsOfFile message which is present in the NSDictionary class.

Display the data in the table view
Since we will be using the same table view to display data at level, we need; an array to hold the data for the current level, a string to hold the item selected in the parent level and an integer value to tell us if this is the first level or not. This is how the header file for RootViewController changes

//RootViewController.h
@interface RootViewController : UITableViewController {

NSArray *tableDataSource;
NSString *CurrentTitle;
NSInteger CurrentLevel;
}

@property (nonatomic, retain) NSArray *tableDataSource;
@property (nonatomic, retain) NSString *CurrentTitle;
@property (nonatomic, readwrite) NSInteger CurrentLevel;

@end

The property "tableDataSource" contains the present level data source and mimicks the property list file. The array will contain n number of dictionary objects, where n is the number of rows at any given level. A dictionary will contain a title and an array containing the children of the present item. All the properties have been synthesized and the "tableDataSource" and "CurrentTitle" have been released in the dealloc method.

The method viewDidLoad changes like this

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

if(CurrentLevel == 0) {

//Initialize our table data source
NSArray *tempArray = [[NSArray alloc] init];
self.tableDataSource = tempArray;
[tempArray release];

DrillDownAppAppDelegate *AppDelegate = (DrillDownAppAppDelegate *)[[UIApplication sharedApplication] delegate];
self.tableDataSource = [AppDelegate.data objectForKey:@"Rows"];

self.navigationItem.title = @"Root";
}
else
self.navigationItem.title = CurrentTitle;
}

If the value of the "CurrentLevel" is zero (which it will be when the application is launched) then the array is initialized and populated from the application delegate. Here we get the array whose key is set to "Rows" which will contain the primary level data and the children of every item. The title of the navigation item is set to "Root" in this case, if not the title is set to the "CurrentTitle" variable and the "tableDataSource" is left untouched. These two properties are set in tableView:didSelectRowAtIndexPath method which will look at it later.

Displaying data
In tableView:numberOfRowsInSection return the count of the array like this

//RootViewController.m
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.tableDataSource count];
}

The method tableView:cellForRowAtIndexPath changes like this

//RootViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

static NSString *CellIdentifier = @"Cell";

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

// Set up the cell...
NSDictionary *dictionary = [self.tableDataSource objectAtIndex:indexPath.row];
cell.text = [dictionary objectForKey:@"Title"];

return cell;
}

From the data source that we created, we know that every array has at least one element of type "Dictionary" and that has at least one element with the key "Title" which is the string that gets displayed in the table view cell. After the UITableView cell is created, we get the dictionary object from the array and the value for the key "Title", which is set to the text property of the cell.

Select a row
When a row is selected tableView:didSelectRowAtIndexPath method is called and this is how it looks like

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

//Get the dictionary of the selected data source.
NSDictionary *dictionary = [self.tableDataSource objectAtIndex:indexPath.row];

//Get the children of the present item.
NSArray *Children = [dictionary objectForKey:@"Children"];

if([Children count] == 0) {


}
else {

//Prepare to tableview.
RootViewController *rvController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:[NSBundle mainBundle]];

//Increment the Current View
rvController.CurrentLevel += 1;

//Set the title;
rvController.CurrentTitle = [dictionary objectForKey:@"Title"];

//Push the new table view on the stack
[self.navigationController pushViewController:rvController animated:YES];

rvController.tableDataSource = Children;

[rvController release];
}
}

We first get dictionary object of the selected row and then get its children into an array. If the present level does not have any children we do not do anything for now, but if it does; we load the same table view with different set of data. We initialized the same "RootViewcontroller", increment the "CurrentLevel" value so it is not zero, set the selected cell's text as the "CurrentTitle", push the view controller on the top of the stack and set the new data source to the "tableDataSource" property.

Try it out and you will be able to drill down by selecting a row.

Loading the detail view
Create a new view (using IB) and name it "DetailView", remember the views get stored under the "Resources" folder. Create a new UIViewController in Xcode under the "Classes" folder and name it DetailViewController. Set the class of File's owner of the "DetailView" to "DetailViewController" and connect the view outlet to the object in the view in the nib file. Please refer to this tutorial on how to load a detail view.

In viewDidLoad method of the "DetailViewController" set the title of the view to "Detail View". This is how the code looks like

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

self.navigationItem.title = @"Detail View";
}

We will load the detail view when an item does not have any children. The tableView:didSelectRowAtIndexPath method changes like this

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

//Get the dictionary of the selected data source.
NSDictionary *dictionary = [self.tableDataSource objectAtIndex:indexPath.row];

//Get the children of the present item.
NSArray *Children = [dictionary objectForKey:@"Children"];

if([Children count] == 0) {

DetailViewController *dvController = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
}
else {

//Prepare to tableview.
RootViewController *rvController = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:[NSBundle mainBundle]];

//Increment the Current View
rvController.CurrentLevel += 1;

//Set the title;
rvController.CurrentTitle = [dictionary objectForKey:@"Title"];

//Push the new table view on the stack
[self.navigationController pushViewController:rvController animated:YES];

rvController.tableDataSource = Children;

[rvController release];
}
}

Conclusion

Using this design we can add any number of children to an item without changing the code. If we have to add data to the sixth level we need not create another table view, all we have to do is add another item in our data source and the code will handle the change. This design does use n number of table view objects like the example provided by Apple.

I hope you had fun reading this article as much as I had writing it. Please let me know what you think.

Happy Programming,
iPhone SDK Articles



Attachments
Suggested Readings



Read more...

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



Read more...