Wednesday, August 31, 2011

SQL Lite - swapping out the XML



Ok, now that we've got a read loop going on, our next and hopefully final step will be to substitute the current XML read for the SQL database read. We've got a lot of the same logic in the program. Actually, there is a hitch. The new database doesn't have the answers set up, unlike the XML file. So, for now what we'll do is just substitute in some dummy answers. But the big step after that will be to pull random answers from the database. We've already done that with the android app, so it should lessent the work. But for now, let's hurry up and get the questions loaded. We'll just pull logic from the XML routine.

This is the current parsing code, invoked when a "Question" tag is encountered in the data stream:

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{

if ([elementName isEqualToString:XML_TAG_QUESTION]){

// answer counter - initialize for new question
cntr = 1;

// allocate the question
question = [[Question alloc] init];

// get the question text
question.questionTxt = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_TEXT]; // XML_TAG_QUESTION_ATTRIBUTE_TEXT

// get get the question number
NSString *text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_NUMBER];

// convert from the text value
question.number = [text intValue];

// Now get the question answer
text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER];

// add the correct answer after converting it to an int
question.correctAnswer = [text intValue];



// add it to the question
[appState addQuestion: question];

[question release];

}



Ok, this is a little bit different than it will need to be. It's set up with the assumption that the answer part of the data structure exists. Also, the correct answer is specified as a number in the question logic, whereas the approach we will be using when we're choosing possible answers at random will be simple to compare the string of the selected answer with the hiragana.

So, what's the best way to get this moving? Maybe for now we can just set a dummy number, like 1. We're just trying to advance a little bit here. And the dummy answers will always be the same anyway, until we implement the population of the answers.

So, we have our dummy answers in the XML file - they look like this:



<answers>
<answer text="おおきい" />
<answer text="おおきな" />
<answer text="ちかてつ" />
<answer text="ちゃわん" />
</answers>


So, for now we'll need to just hardcode them.

Here's the updated code:


-(void) selectAll
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{

int cntr_question = 0;

NSLog(@"select all, open ok");

//NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];


//NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words where _id = 1;"];
NSString *querySQL = [NSString stringWithFormat: @"SELECT _id, kanji, hiragana, english FROM all_words;"];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(jlptDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
NSLog(@"select all, prepare ok");

while (sqlite3_step(statement) == SQLITE_ROW)
{
cntr_question++;

NSLog(@"select all, row found");

NSString *kanji = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];

NSString *hiragana = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];

NSString *english = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 2)];

NSLog(@"kanji: %@, hiragana: %@, english: %@", kanji, hiragana, english);

// allocate the question
question = [[Question alloc] init];

question.number = cntr_question;
question.questionTxt = kanji;

// add the correct answer after converting it to an int
question.correctAnswer = 1;

[kanji release];
[hiragana release];
[english release];

// add it to the app state
[appState addQuestion: question];



int cntr =1;

// Get get the answer from the question
NSString *answer = @"おおきい";


// convert counter to an object for storage in dictionary
NSNumber *counter = [NSNumber numberWithInt:cntr];

[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;


// Get get the answer from the question
answer = @"おおきな";

// convert counter to an object for storage in dictionary
counter = [NSNumber numberWithInt:cntr];

[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;

// Get get the answer from the question
answer = @"ちかてつ";

// convert counter to an object for storage in dictionary
counter = [NSNumber numberWithInt:cntr];

[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;

// Get get the answer from the question
answer = @"ちゃわん";

// convert counter to an object for storage in dictionary
counter = [NSNumber numberWithInt:cntr];

[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;


[question release];
}
sqlite3_finalize(statement);
}
else {
NSLog(@"select all, prepare unssuccessful");
}
sqlite3_close(jlptDB);
}
else {
NSLog(@"select all, open failed");

}
}


Ok, and now let's update the app delegate for the new reader:

Not quite - I'm getting a "2" in the question field.

Oh, right, I change the select to add the _id column, then change my mind.

Here's the updated select:

NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words;"];


And we're good. Here's the screenshot:



SQLlite - the reading all the rows

In this post, we're going to set up up a loop to read *all* the rows in the database, instead of just one. To find how how to best do this, let's return to the excellent tutorial that set the stage for all this -

http://www.techotopia.com/index.php/An_Example_SQLite_based_iOS_4_iPhone_Application


Here's what they have to say about a loop in the select routine:


A return value of SQLITE_ROW indicates that at least one match has been located. In this case the first matching result data is extracted, assigned to NSString objects and displayed in the appropriate text fields. As an alternative, a while loop could have been constructed to display all matching results



Cool. I'm feeling just a little bit crazy today. Let's see if we can print the whole database ;)

Ok, it's just a couple of small adjustments - dump the where clause, substitue a "while" for the "if", and get rid of that if's else statement. Let run it...

2011-08-31 16:28:14.461 JlptQuizApp[5602:207] select all, row found
2011-08-31 16:28:14.462 JlptQuizApp[5602:207] kanji: 会う, hiragana: あう, english: to meet
2011-08-31 16:28:14.462 JlptQuizApp[5602:207] select all, row found
2011-08-31 16:28:14.463 JlptQuizApp[5602:207] kanji: 青, hiragana: あお, english: blue
2011-08-31 16:28:14.463 JlptQuizApp[5602:207] select all, row found
2011-08-31 16:28:14.464 JlptQuizApp[5602:207] kanji: 青い, hiragana: あおい, english: blue
2011-08-31 16:28:14.464 JlptQuizApp[5602:207] select all, row found
2011-08-31 16:28:14.465 JlptQuizApp[5602:207] kanji: 赤, hiragana: あか, english: red
2011-08-31 16:28:14.465 JlptQuizApp[5602:207] select all, row found
2011-08-31 16:28:14.466 JlptQuizApp[5602:207] kanji: 赤い, hiragana: あかい, english: red
2011-08-31 16:28:14.582 JlptQuizApp[5602:207] select all, row found
2
....

Yup. It work. Ok - in the next post, we'll *really* bring the data into the app, instead of just printing it.





SQLLite - reading the databse

Ok, now that we've implemented opening the database, the next step is actually reading it. We only have a couple of tables in the db, and we're not concerned with one of them yet. The main table is a list of all the words needed to be studied for the JLPT test.

So, let's take a look at the find method from yesterday's tutorial:


-(void) findContact
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];
address.text = addressField;

NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];
phone.text = phoneField;

status.text = @"Match found";

[addressField release];
[phoneField release];
} else {
status.text = @"Match not found";
address.text = @"";
phone.text = @"";
}
sqlite3_finalize(statement);
}
sqlite3_close(contactDB);
}
}


We can change the file name and select statement, but the problem is it's only selecting a single row. We'll go with that for now, just to get the structure in place, and then switch it off to a loop which reads all the rows.


Ok, here's the revised method:

-(void) selectAll
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{
//NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];


NSString *querySQL = [NSString stringWithFormat: @"Select v_all_words_by_level_freq._id, level, number, kanji, hiragana, english, freq, rating, last_review_date, next_review_date from v_all_words_by_level_freq where _id = 1"];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(jlptDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *kanji = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 3)];

NSString *hiragana = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 4)];

NSString *english = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 5)];

NSLog(@"kanji: %@, hiragana: %@, english: %@", kanji, hiragana, english);

[kanji release];
[hiragana release];
[english release];
} else {
NSLog(@"match not found");
}
sqlite3_finalize(statement);
}
sqlite3_close(jlptDB);
}
}


And add a call after the init in the app delegate:



MyDatabaseReader *myDatabaseReader = [[MyDatabaseReader alloc] init];

[myDatabaseReader selectAll];




Well, I'm still not getting the results I'm looking for. Here's the code with some debugging added:

-(void) selectAll
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{

NSLog(@"select all, open ok");

//NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];


NSString *querySQL = [NSString stringWithFormat: @"Select v_all_words_by_level_freq._id, level, number, kanji, hiragana, english, freq, rating, last_review_date, next_review_date from v_all_words_by_level_freq whre _id = 1"];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(jlptDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
NSLog(@"select all, prepare ok");

if (sqlite3_step(statement) == SQLITE_ROW)
{
NSLog(@"select all, row found");

NSString *kanji = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 3)];

NSString *hiragana = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 4)];

NSString *english = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 5)];

NSLog(@"kanji: %@, hiragana: %@, english: %@", kanji, hiragana, english);

[kanji release];
[hiragana release];
[english release];
} else {
NSLog(@"match not found");
}
sqlite3_finalize(statement);
}
else {
NSLog(@"select all, prepare unssuccessful");
}
sqlite3_close(jlptDB);
}
else {
NSLog(@"select all, open failed");

}
}



And when I run it, it shows "prepare unsuccessful" - so perhaps I've munged the syntax. If I copy and paste into RazorSQL - oh, yeah, I typo-ed the "where". Next error - there is no rating column. How did that old sql get in there?

Ok, let's just have RazorSQL generate a select statement:

SELECT
_id,
level,
number,
kanji,
hiragana,
english,
freq,
freq_sequence,
level_freq_sequence
FROM
all_words
where _id = 1;


Which, tailored for the test, looks like this:


NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words where _id = 1;"];


and there we go:

2011-08-31 16:06:08.822 JlptQuizApp[5516:207] select all, prepare ok
2011-08-31 16:06:08.823 JlptQuizApp[5516:207] select all, row found
2011-08-31 16:06:08.823 JlptQuizApp[5516:207] kanji: 会う, hiragana: あう, english: to meet


Ok, in our next post, we'll probably just make the changes for this class replace the XMLReader. Onward ho!

SQLLite - getting it into the app

Ok, we've now reached the point where we have a reasonable understanding of how Sqlite fits into an generic iOS app. We've now arrived at the point where we can think about integrating iOS into our app.

Our app is currently pulling data out from an XML file using an XML Parser. So, let's take a look at the class again.

These are in a couple of files called XMLReader.m and XMLReader.h. Here they are:

#import
#import
#import "AppState.h"


@interface XMLReader : NSObject {

AppState *appState;

Question *question;

// create the question
int cntr;

/* XML Tag Names */

NSString * XML_TAG_QUESTION_BLOCK;
NSString * XML_TAG_QUESTION;

NSString * XML_TAG_QUESTION_ATTRIBUTE_NUMBER;
NSString * XML_TAG_QUESTION_ATTRIBUTE_TEXT;
NSString * XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER;

NSString * XML_TAG_ANSWERS;
NSString * XML_TAG_ANSWER;
NSString * XML_TAG_ANSWER_ATTRIBUTE_TEXT;

}

@property (nonatomic, readonly ) AppState *appState;
//@property (nonatomic, retain ) Question question;
@property(nonatomic, readonly) NSString * XML_TAG_QUESTION_BLOCK;
@property(nonatomic, readonly) NSString * XML_TAG_QUESTION;

@property(nonatomic, readonly) NSString * XML_TAG_QUESTION_ATTRIBUTE_NUMBER;
@property(nonatomic, readonly) NSString * XML_TAG_QUESTION_ATTRIBUTE_TEXT;
@property(nonatomic, readonly) NSString * XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER;

@property(nonatomic, readonly) NSString * XML_TAG_ANSWERS;
@property(nonatomic, readonly) NSString * XML_TAG_ANSWER;
@property(nonatomic, readonly) NSString * XML_TAG_ANSWER_ATTRIBUTE_TEXT;


- (void)parseXMLData:(NSData *)data;
- (void)printAppState;

@end






//
// XMLReader.m
//

#import
#import
#import "XMLReader.h"
#import "AppState.h"
#import "Question.h"

@implementation XMLReader
@synthesize

appState,
//question,
XML_TAG_QUESTION_BLOCK,
XML_TAG_QUESTION,
XML_TAG_QUESTION_ATTRIBUTE_NUMBER,
XML_TAG_QUESTION_ATTRIBUTE_TEXT,
XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER,
XML_TAG_ANSWERS,
XML_TAG_ANSWER,XML_TAG_ANSWER_ATTRIBUTE_TEXT;


- (id)init
{
// Call the superclass's designated initializer
self = [super init];

// Did the superclass's initialization fail?
if (!self)
return nil;

appState = [[[AppState alloc] init] autorelease];

/* XML Tag Names */
XML_TAG_QUESTION_BLOCK = @"questions";

XML_TAG_QUESTION = @"question";

XML_TAG_QUESTION_ATTRIBUTE_NUMBER = @"number";
XML_TAG_QUESTION_ATTRIBUTE_TEXT = @"text";
XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER = @"correct_answer";

XML_TAG_ANSWERS = @"answers";
XML_TAG_ANSWER = @"answer";
XML_TAG_ANSWER_ATTRIBUTE_TEXT = @"text";

// Return the address of the newly initialized object
return self;
}

- (void)printAppState{
NSLog(@"printing app state");
NSLog(@"appState: %@", appState);
int ca = appState.chosenAnswer;
NSLog(@"chosen answer: ", ca);
}
- (void)parserDidStartDocument:(NSXMLParser *)parser{
NSLog(@"found file and started parsing");
}


- (void)parseXMLData:(NSData *)data //URL is the file path (i.e. /Applications/MyExample.app/MyFile.xml)
{

NSLog(@"in parseNSData");

NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];

// Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.
[parser setDelegate:self];

// Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser.
[parser setShouldProcessNamespaces:NO];
[parser setShouldReportNamespacePrefixes:NO];
[parser setShouldResolveExternalEntities:NO];

[parser parse];
[parser release];

}


- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{

if ([elementName isEqualToString:XML_TAG_QUESTION]){

// answer counter - initialize for new question
cntr = 1;

// allocate the question
question = [[Question alloc] init];

// get the question text
question.questionTxt = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_TEXT]; // XML_TAG_QUESTION_ATTRIBUTE_TEXT

// get get the question number
NSString *text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_NUMBER];

// convert from the text value
question.number = [text intValue];

// Now get the question answer
text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER];

// add the correct answer after converting it to an int
question.correctAnswer = [text intValue];

// add it to the question
[appState addQuestion: question];

[question release];

}

if ([elementName isEqualToString:XML_TAG_ANSWER]){

// convert counter to an object for storage in dictionary
NSNumber *counter = [NSNumber numberWithInt:cntr];

// Example of format string
// NSString *counter = [NSString stringWithFormat: @"%d", cntr];

// Get get the answer from the question
NSString *answer = [attributeDict valueForKey:XML_TAG_ANSWER_ATTRIBUTE_TEXT];

// example of NSLog
//NSLog(@"question, number is %i, text is %@, obj is %@", question.number, question.questionTxt, question);

// add the question to the answer dictionary
[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;

}

}

// other methods not used, left for doc

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
//NSLog(@"Found: %@", string);
}

- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
}

- (void)dealloc {
//[appState release];
[super dealloc];
}

@end



Looking through it, it's not the best fit. The event-based parsing will have to go. Also, there's the question of, since we're not long parsing an XML file, but rather reading a database, we have a bit more flexibility. We could for example retrieve the row on demand, rather than stroring it in memory. But, it's probably ok to stick with this for now, is there are certain advantages to storing it in memory, e.g. randomizing the reading sequence.

Let's take a look at the code in the app delegate that invokes the parsing logic:



NSString *xmlFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"questions.xml"];

NSString *xmlFileContents = [NSString stringWithContentsOfFile: xmlFilePath encoding:NSUTF8StringEncoding error:nil];

NSData *data = [NSData dataWithBytes:[xmlFileContents UTF8String] length:[xmlFileContents lengthOfBytesUsingEncoding: NSUTF8StringEncoding]];

XMLReader *xmlReader = [[XMLReader alloc] init];

[xmlReader parseXMLData: data];

self.appState = xmlReader.appState;




Ok, so, it looks like the usual craziness to set up access to the file, then invoking parseXMLData with the data as the parameter. So, maybe we should have something like "MyDatabaseReader.getData" or something.

First of all, should we make a copy of the database onto some kind of resource directory?


Let's review the code to access the SQLite database from yesterday:


//
// databaseViewController.h
// database
//

#import
#import "/usr/include/sqlite3.h"


@interface databaseViewController : UIViewController {

UITextField *name;
UITextField *address;
UITextField *phone;
UILabel *status;
NSString *databasePath;
sqlite3 *contactDB;

}

@property (retain, nonatomic) IBOutlet UITextField *name;
@property (retain, nonatomic) IBOutlet UITextField *address;
@property (retain, nonatomic) IBOutlet UITextField *phone;
@property (retain, nonatomic) IBOutlet UILabel *status;
- (IBAction) saveData;
- (IBAction) findContact;

@end



and:



//
// databaseViewController.m
// database
//

#import "databaseViewController.h"

@implementation databaseViewController

@synthesize name, address, phone, status;

- (void) saveData
{

sqlite3_stmt *statement;

const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSString *insertSQL = [NSString stringWithFormat: @"INSERT INTO CONTACTS (name, address, phone) VALUES (\"%@\", \"%@\", \"%@\")", name.text, address.text, phone.text];

const char *insert_stmt = [insertSQL UTF8String];

sqlite3_prepare_v2(contactDB, insert_stmt, -1, &statement, NULL);

if (sqlite3_step(statement) == SQLITE_DONE)
{
status.text = @"Contact added";
name.text = @"";
address.text = @"";
phone.text = @"";
} else {
status.text = @"Failed to add contact";
}
sqlite3_finalize(statement);
sqlite3_close(contactDB);
}
}

-(void) findContact
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];
address.text = addressField;

NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];
phone.text = phoneField;

status.text = @"Match found";

[addressField release];
[phoneField release];
} else {
status.text = @"Match not found";
address.text = @"";
phone.text = @"";
}
sqlite3_finalize(statement);
}
sqlite3_close(contactDB);
}
}

- (void)dealloc
{

[name release];
[address release];
[phone release];
[status release];
[super dealloc];
}

- (void)didReceiveMemoryWarning
{
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle


// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{

NSString *docsDir;
NSArray *dirPaths;

// Get the documents directory
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

docsDir = [dirPaths objectAtIndex:0];

// Build the path to the database file
databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: @"contacts.db"]];

NSFileManager *filemgr = [NSFileManager defaultManager];

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
char *errMsg;
const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) != SQLITE_OK)
{
status.text = @"Failed to create table";
}

sqlite3_close(contactDB);

} else {
status.text = @"Failed to open/create database";
}
}

[filemgr release];

[super viewDidLoad];
}


- (void)viewDidUnload
{
self.name = nil;
self.address = nil;
self.phone = nil;
self.status = nil;
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

@end


Ok, this looks pretty good. It might be easier just to copy these into our project and pare them down.

Ok, first of all, where should the database be kept? Where did we decide to create it?

A printout of the path reveals this:

/Users//Library/Application Support/iPhone Simulator/4.3.2/Applications/8021582F-0ECC-4873-965D-2D851E5CC8B2/Documents/contacts.db

Obviously, we're not going to go with this as the path. We'll worry about the placement when we deploy later. For now, lit's just put in the project's first level.

Ok, that's there. Now, let's add the classes. Let's just try copying for the database app, just to get moving on this. No, we're better of copying from the XMLReader classes - they're more aligned with the current app.

Ok. Now, let's see if we can open the db. We'll copy the code from the example database to open the db, then modify it:

Ok, here's what we have so far in the init:


- (id)init
{
// Call the superclass's designated initializer
self = [super init];

// Did the superclass's initialization fail?
if (!self)
return nil;


// Get last state if existing
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

// only needs 2 variables

correctAnswers = [prefs integerForKey:@"correctAnswers"];
currentQuestionNumber = [prefs integerForKey:@"currentQuestionNumber"];

// question number starts at 1
if (0 == currentQuestionNumber) {
currentQuestionNumber = 1;
}


questionHash = [[NSMutableDictionary alloc] init];

NSString *docsDir;
NSArray *dirPaths;

// Get the documents directory
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

docsDir = [dirPaths objectAtIndex:0];

// Build the path to the database file
databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: @"jlpt.db"]];


NSLog(@"database path: = %@", databasePath);


NSFileManager *filemgr = [NSFileManager defaultManager];

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSLog(@"database opened ok");

sqlite3_close(contactDB);

} else {
NSLog(@"database opene error");
}
}

[filemgr release];


// Return the address of the newly initialized object
return self;
}



This won't work yet, as we haven't adjusted for the new path. However, this is easily remedied by pulling for the code for locating the xml file, which was in fact at the same level previously. We'll move it from the app delegate to the init statement.

Actually, since we don't need to pass the data, just the path, we can drop a couple of statements, and just use this:

NSString *xmlFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"questions.xml"];


Although, to use this, you have to be sure there's a "resources" group in your project and that you've copied your db into that. Since this is a legacy project, it has resource by default - which doesn't seem to be there for new projects created in Xcode4. Still, it'll do for this app.

Ok, good. Here's the updated init:




- (id)init
{

NSLog(@"in mydatabase init");


// Call the superclass's designated initializer
self = [super init];

// Did the superclass's initialization fail?
if (!self)
return nil;


NSLog(@"in mydatabase init 2");

// Get last state if existing
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];

// only needs 2 variables

correctAnswers = [prefs integerForKey:@"correctAnswers"];
currentQuestionNumber = [prefs integerForKey:@"currentQuestionNumber"];

// question number starts at 1
if (0 == currentQuestionNumber) {
currentQuestionNumber = 1;
}


questionHash = [[NSMutableDictionary alloc] init];

databasePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"jlpt.db"];

NSLog(@"database path: = %@", databasePath);


NSFileManager *filemgr = [NSFileManager defaultManager];

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{
NSLog(@"database opened ok");

sqlite3_close(jlptDB);

} else {
NSLog(@"database open error");
}
}

[filemgr release];


// Return the address of the newly initialized object
return self;
}


Before I forget, let's add the SQL dynamic library to the project. Hmm...seems I've already done it.

And instantiate the object and invoke the read from the app delegate:

MyDatabaseReader *myDatabaseReader = [[MyDatabaseReader alloc] init];


Ok, let's run it and check the log to see if it successfully opens the db:

We'll, it doesn't say whether it successfully opened or not , I'm not understanding how the simulator works. It's an emulator for the phone. Ah, I get it. I'm checking for the open, not for the exists. Let's try it again, with this code:



NSFileManager *filemgr = [NSFileManager defaultManager];

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{
NSLog(@"database opened ok");

sqlite3_close(jlptDB);

} else {
NSLog(@"database open error");
}
}
else {
NSLog(@"database found");
}



There it is:

2011-08-31 14:56:10.169 JlptQuizApp[5147:207] database found


Will it open it?

Let's try this:

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
NSLog(@"database not found");

}
else {
NSLog(@"database found");

const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{
NSLog(@"database opened ok");

sqlite3_close(jlptDB);

} else {
NSLog(@"database open error");
}

}


Yes:

2011-08-31 14:58:47.639 JlptQuizApp[5175:207] database found
2011-08-31 14:58:47.640 JlptQuizApp[5175:207] database opened ok


Ok. That's good for this post. Let's start reading the db in the next.

Tuesday, August 30, 2011

IOS and SQLite - Part 2


Ok, we're going to jump right into tackling part 2 of incorporating SQlite into our iOS app. We'll tackle the next chapter in an online tutorial, and see how far we can get. Note that though the heading states it's using Xcode4, so far all we've seen is Xcode 3.

Here's the URL again:

http://www.techotopia.com/index.php/An_Example_SQLite_based_iOS_4_iPhone_Application#About_the_Example_SQLite_iPhone_Application

About the Example SQLite iPhone Application

The focus of this chapter is the creation of a somewhat rudimentary iPhone iOS application that is designed to store contact information (names, addresses and telephone numbers) in a SQLite database. In addition to data storage, a feature will also be implemented to allow the user to search the database for the address and phone number of a specified contact name. Some knowledge of SQL and SQLite is assumed throughout the course of this tutorial. Those readers who are unfamiliar with these technologies in the context of iPhone application development are encouraged to first read the previous chapter before proceeding.



Hmm...I wonder if this covers an insert? Doesn't sound like it. That's ok - we don't need it yet.

Creating and Preparing the SQLite Application Project

Begin by launching the Xcode environment and creating a new iOS iPhone View-based application project called database. Once the project has been created, the next step is to configure the project to include the SQLite dynamic library (libsqlite3.dylib) during the link phase of the build process. Failure to include this library will result in build errors.


Good, created.



To include this library in the build directives, hold down the Ctrl key and click with the mouse on the Frameworks entry of the Groups and Files panel located on the left hand side of the Xcode project window. From the resulting menu, select the Add -> Existing Frameworks… option. Within the list of framework options, scroll down to the libsqlite3.dylib entry, select it and click on the Add button.



Ok, there's the first problem. We know the new way, except there were are libraries to choose from. One had a "0" in it somewhere. I wonder if Xcode 3 gives the same choice? Ah here we go:

http://stackoverflow.com/questions/4314866/what-is-the-difference-between-libsqlite3-dylib-and-libsqlite3-0-dylib

shows it's just a symlink. I could've gotten that - use finder to find the file, then ls -l, I think. Still - sometimes it's just faster to google.

Ok, added the one without the 0. (See previous post for how to do it).

Next?

Importing sqlite3.h and Declaring the Database Reference

Before we can create a database we need to declare a variable pointer to a structure of type sqlite3 that will act as the reference to our database. Since we will be working with the database in the view controller for our application the best place to declare this variable is in the databaseViewController.h file. Since we also need to import the sqlite3.h header file into any files where we make use of SQLite this is also an ideal place to include the file. Within the main Xcode project window, select the databaseViewController.h file and modify it as follows:

#import
#import "/usr/include/sqlite3.h"

@interface databaseViewController : UIViewController {
sqlite3 *contactDB;
}


Done. I feel more comfortable with the whole view controller concept since going through the exercise of replacing the default view controller.

Creating the Outlets and Actions

The application user interface is going to consist of three text fields
(for the name, address and phone number), a label object to display the status of the database activities, a Save button and a Find button.


When clicked, the save button will write the data entered into the text fields into a database table. The find button, on the other hand, will search the database for a row that matches the currently entered contact name and display the address and phone number for the matching record in the address and phone text fields.


Sounds good...


Select the databaseViewController.h file once again and further modify the interface file to add the appropriate outlets and actions:

#import
#import "/usr/include/sqlite3.h"

@interface databaseViewController : UIViewController {
UITextField *name;
UITextField *address;
UITextField *phone;
UILabel *status;
NSString *databasePath;

sqlite3 *contactDB;
}
@property (retain, nonatomic) IBOutlet UITextField *name;
@property (retain, nonatomic) IBOutlet UITextField *address;
@property (retain, nonatomic) IBOutlet UITextField *phone;
@property (retain, nonatomic) IBOutlet UILabel *status;
- (IBAction) saveData;
- (IBAction) findContact;
@end


No problem!



With the outlets and actions declared in the interface file, edit the databaseViewController.m file and add the @synthesize directives for the outlets and the template methods for the saveData and findContact actions:

#import "databaseViewController.h"

@implementation databaseViewController
@synthesize name, address, phone, status;

- (void) saveData
{
}

-(void) findContact
{
}
.
.
.
@end




So far so good.


Releasing Memory

Having allocated memory in implementing the above outlets, it is important that we add code to free up any resources that were allocated during execution of the application. To do so, edit the coreDataViewController.m file again and modify the viewDidUnload and dealloc methods as follows:

- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
self.name = nil;
self.address = nil;
self.phone = nil;
self.status = nil;
}

- (void)dealloc {
[name release];
[address release];
[phone release];
[status release];
[super dealloc];
}



Ok. Keep on moving...

Creating the Database and Table

When the application is launched it will need to check whether the database file already exists and, if not, create both the database file and a table within the database in which to store the contact information entered by the user. The code to perform this task can be placed in the viewDidLoad method of our view controller class. Select the databaseViewController.m file, scroll down to the viewDidLoad method and remove the comment markers from around it (/* and */) before editing the code for this method as follows:

- (void)viewDidLoad {
NSString *docsDir;
NSArray *dirPaths;

// Get the documents directory
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

docsDir = [dirPaths objectAtIndex:0];

// Build the path to the database file
databasePath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent: @"contacts.db"]];

NSFileManager *filemgr = [NSFileManager defaultManager];

if ([filemgr fileExistsAtPath: databasePath ] == NO)
{
const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
char *errMsg;
const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) != SQLITE_OK)
{
status.text = @"Failed to create table";
}

sqlite3_close(contactDB);

} else {
status.text = @"Failed to open/create database";
}
}

[filemgr release];
[super viewDidLoad];
}


The code in the above method performs the following tasks:

Identifies the application’s Documents directory and constructs a path to the contacts.db database file.
Creates an NSFileManager instance and subsequently uses it to detect if the database file already exists.
If the file does not yet exist the code converts the path to a UTF-8 string and creates the database via a call to the SQLite sqlite3_open() function, passing through a reference to the contactDB variable declared previously in the interface file.
Prepares a SQL statement to create the contacts table in the database.
Reports the success or failure of the operation via the status label.
Closes the database and performs a memory clean up.


Ok, looks good.


Implementing the Code to Save Data to the SQLite Database

The saving of contact data to the database is the responsibility of the saveData action method. This method will need to open the database file, extract the text from the three text fields and construct and execute a SQL INSERT statement to add this data as a record to the database. Having done this, the method will then need to close the database.

In addition, the code will need to clear the text fields ready for the next contact to be entered, and update the status label to reflect the success or otherwise of the operation.

In order to implement this behavior, therefore, we need to modify the template method created previously as follows:

- (void) saveData
{
sqlite3_stmt *statement;

const char *dbpath = [databasePath UTF8String];

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSString *insertSQL = [NSString stringWithFormat: @"INSERT INTO CONTACTS (name, address, phone) VALUES (\"%@\", \"%@\", \"%@\")", name.text, address.text, phone.text];

const char *insert_stmt = [insertSQL UTF8String];

sqlite3_prepare_v2(contactDB, insert_stmt, -1, &statement, NULL);
if (sqlite3_step(statement) == SQLITE_DONE)
{
status.text = @"Contact added";
name.text = @"";
address.text = @"";
phone.text = @"";
} else {
status.text = @"Failed to add contact";
}
sqlite3_finalize(statement);
sqlite3_close(contactDB);
}
}

The next step in our application development process is to implement the action for the find button.


Good, good. Keep on moving.


Implementing Code to Extract Data from the SQLite Database

As previously indicated, the user will be able to extract the address and phone number for a contact by entering the name and touching the find button. Ultimately, the Touch Up Inside event of the find button will be connected to the findContact method, the code for which is outlined below:

- (void) findContact
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
NSString *querySQL = [NSString stringWithFormat: @"SELECT address, phone FROM contacts WHERE name=\"%@\"", name.text];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *addressField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];
address.text = addressField;

NSString *phoneField = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];
phone.text = phoneField;

status.text = @"Match found";

[addressField release];
[phoneField release];
} else {
status.text = @"Match not found";
address.text = @"";
phone.text = @"";
}
sqlite3_finalize(statement);
}
sqlite3_close(contactDB);
}
}

This code opens the database and constructs a SQL SELECT statement to extract any records in the database that match the name entered by the user into the name text field. The SQL statement is then executed. A return value of SQLITE_ROW indicates that at least one match has been located. In this case the first matching result data is extracted, assigned to NSString objects and displayed in the appropriate text fields. As an alternative, a while loop could have been constructed to display all matching results. For the purposes of keeping this example simple, however, we will display only the first match. The code then updates the status label to indicate whether a match was found, releases memory where necessary and closes the database. Having implemented the appropriate methods be sure to save both the databaseViewController.h and databaseViewController.m files before moving on to the user interface design phase.


Ok. Save them all. Now, onto the dreaded construction of the user interface.

Designing the User Interface

The final step in developing our example SQLite iOS iPhone application involves the design of the user interface. In the main Xcode project window, begin by double clicking on the databaseViewController.xib file to launch the Interface Builder tool. If the Library window is not visible, invoke it by selecting Tools -> Library and then drag and drop components onto the canvas and edit properties so that the layout appears as illustrated in the following figure:





Ok, we know how to use the interface builder to add labels, text boxes and buttons. No problem here. Ok, actually, it's pretty straightforward by now to connect the text labels and buttons to the corresponding UILabels and methods. Ok. Not bad. Are we good to go?

Building and Running the Application

The final step is to build and run the application. Click on the Build and Run button located in the toolbar of the main Xcode project window. Assuming an absence of compilation errors, the application should load into the iOS Simulator environment. Enter details for a few contacts, pressing the Save button after each entry. Be sure to check the status label to ensure the data is being saved successfully. Finally, enter the name of one your contacts and click on the Find button. Assuming the name matches a previously entered record, the address and phone number for that contact should be displayed and the status label updated with the message “Match found”:


iPhone iOS 4
Development
Essentials eBook
$12.99

eBookFrenzy.com



Cool. Let's try it...


Uh..oh. Can't get at it due to the fact that the keyboard control won't go away. There's a solution here:

http://stackoverflow.com/questions/804563/how-to-hide-the-keyboard-when-empty-area-is-touched-on-iphone

But I think I'll just moves the buttons higher for now.

Sugar. It's not working. I save and the status says saved, but the find button is just returning blanks.

Doh! I didn't fill in the find method. Don't you love it when it's just an obvious error?

And voila - here's the "finished" app:






Adding an SQLlite database - part 1

Ok, here we are, already at the point where we're ready to start adding an SQLlite database. As I mentioned in the last post, there is an XMLReader class which parses XML and loads up the questions. Our goal is not to replace that with something which reads an XMLLite database which contains the same information. This database already exists - nice! It was created for the Android App, and which we can now leverage for this new, iOS app.

So, let's go ahead and google it. What else?

How does this sound:

An Example SQLite based iOS 4 iPhone Application

http://www.techotopia.com/index.php/An_Example_SQLite_based_iOS_4_iPhone_Application

About the Example SQLite iPhone Application

The focus of this chapter is the creation of a somewhat rudimentary iPhone iOS application that is designed to store contact information (names, addresses and telephone numbers) in a SQLite database. In addition to data storage, a feature will also be implemented to allow the user to search the database for the address and phone number of a specified contact name. Some knowledge of SQL and SQLite is assumed throughout the course of this tutorial. Those readers who are unfamiliar with these technologies in the context of iPhone application development are encouraged to first read the previous chapter before proceeding.



So far, so good.

Creating and Preparing the SQLite Application Project

Begin by launching the Xcode environment and creating a new iOS iPhone View-based application project called database. Once the project has been created, the next step is to configure the project to include the SQLite dynamic library (libsqlite3.dylib) during the link phase of the build process. Failure to include this library will result in build errors.

To include this library in the build directives, hold down the Ctrl key and click with the mouse on the Frameworks entry of the Groups and Files panel located on the left hand side of the Xcode project window. From the resulting menu, select the Add -> Existing Frameworks… option. Within the list of framework options, scroll down to the libsqlite3.dylib entry, select it and click on the Add button.



Let's give it a try...

Hmm...no add option.

It might be worth it to sort of just do a tutorial application which includes a database before modifying one. This will give us a little extra practice with creating an iOS app, and sandbox the app development.

Well, everything still seems to be Xcode3

Ok, here's a way to add sqlite using Xcode4.

http://www.ajaxapp.com/2011/08/30/how-to-add-sqlite-framework-into-xcode-4-for-your-ios-app/

Unfortunately, the above-said Xcode 3 way of doing it is no more available on Xcode 4. On Xcode 4, you’ll have to do the following:

1. Click on your project name in your left pane of Xcode 4. You’ll be shown with the above screen.

2. Then click on “Build Phases”, then get down to “Link Binary With Libraries”, and click the + button and add your SQlite framework (libsqlite3.0.dylib) as shown in the next screen. Voila, then you’re good to go to compile some code using SQLite functions.


Ok, great. That seems to work.

How to proceed now?

Let's try this tutorial:

http://www.icodeblog.com/2008/08/19/iphone-programming-tutorial-creating-a-todo-list-using-sqlite-part-1/#nav-based


If you have been following my tutorials, you know that we have been working primarily with UITableViews. This is mostly because SO many applications can be developed using this simple control. This final UITableView tutorial will be taking all of the skills learned from previous tutorials, putting them all together, and adding SQLite to create a prioritized To-Do list. I will also be showing you how to add multiple columns to your table cells and we will be exploring some of the other controls that the iPhone has to offer. What good would the tutorials be if we didn’t use them to create something useful.

I will move a little faster in this tutorial while still explaining the new stuff in detail. I will assume that you have completed the fruits tutorial and it’s prerequisites.

This tutorial will be a multipart series as it will be a little longer than my previous ones. In this first tutorial, you will learn:

Create a NavigationBased Application
Create a Database
Add the Database to Your Project
Add the SQLite3 Framework
Create a Todo Class Object
Initialize the Database

So let’s get started…

Open up X-Code and Select File->New Project… Select Navigation-Based Application and click Choose…




Hmm...navigation based. That's cool. I think I've started out with view-based in the past.


Name your project todo. Now let’s create the todo database that we will be using.


That's ok, I just had to put it in my Appdev/iphone folder. The project name is todo, and the whole thing with the package name is now called com.mycompany.todo.todo.


Open up the Terminal application on your Mac. This is located in Applications > Utilities.


Ok, no problem. I've done this about a million times.

If you have installed XCode, you should have mysqlite3 already on your computer. To check this, type:

sqlite3 into the Terminal and sqlite3 should start. Type .quit to exit. If sqlite3 is not installed, install all of the XTools from your Mac Installation Disk.


".q" will get you out, too.


Now that the terminal is open let’s create the database. This is done with the command:

sqlite3 todo.sqlite



SQLite3 will now start and load the todo.sqlite database. By default the database is empty and contains no tables. If you need a refresher on the basics of SQL databases Google It.


It's a good idea to pull your head up and explore a touch, every once in a while. If I do a ".help", it shows me all the commands available. That shows that if you do a ".databases" command, you'll see all the available databases. And sure enough, our "todo.sqlite" shows up. Nice! It's fun to learn.

S
ince our application is fairly simple, we only need to create one table. We will create a table called todo by typing the following statement:

CREATE TABLE todo(pk INTEGER PRIMARY KEY, text VARCHAR(25), priority INTEGER, complete BOOLEAN);

One thing to note here is the pk field. It is the primary key of the table. This adds functionality such that every time a row is added to the database, it auto-increments this field. This will be a unique identifier to identify each row. All of the other fields should be fairly self explanitory.


Ok, great. Cut an paste the command. And, a ".schema" command shows the record of the create statement:

.schema
CREATE TABLE todo(pk INTEGER PRIMARY KEY, text VARCHAR(25), priority INTEGER, complete BOOLEAN);


Now that our table has been created, let’s add some data. We will eventually be adding todo items within our app, but for now we will add some defaults. Type the following commands below.

INSERT INTO todo(text,priority,complete) VALUES('Take out the trash',3,0);
INSERT INTO todo(text,priority,complete) VALUES('Do Computer Science homework',1,0);
INSERT INTO todo(text,priority,complete) VALUES('Learn Objective C',1,0);
INSERT INTO todo(text,priority,complete) VALUES('DIGG this tutorial',2,0);


Cool. You can paste them all in at once, and the show what's in the table:

sqlite> INSERT INTO todo(text,priority,complete) VALUES('Take out the trash',3,0);
sqlite> INSERT INTO todo(text,priority,complete) VALUES('Do Computer Science homework',1,0);
sqlite> INSERT INTO todo(text,priority,complete) VALUES('Learn Objective C',1,0);
sqlite> INSERT INTO todo(text,priority,complete) VALUES('DIGG this tutorial',2,0);
sqlite> select * from todo;
1|Take out the trash|3|0
2|Do Computer Science homework|1|0
3|Learn Objective C|1|0
4|DIGG this tutorial|2|0
sqlite>




You can add as many todo items as you would like. For this tutorial, make sure you enter a priority between 1 and 3 (You’ll see why later). Now our database has been created and populated let’s exit out of SQLite3. Do this by typing .quit. Your terminal window should look something like this.
(pic omitted)


Ok, great. Let's keep going.

Now go back to XCode. Do a Control-Click (right click) on the folder named Resources. Click Add -> Existing Files… and browse to your todo.sqlite file and click Add. It will then prompt you with a screen like this.


Oops - no resources on this new app. Back to google!

Ok, here's another link - it looks like the same one we started out with, but I think it's right on the adding the library this time. Anyway, it gives an intro to sqlite similar to what we just walked through. Then it introduces the iOS code for dealing with sqlite databases:

http://www.techotopia.com/index.php/IOS_4_iPhone_Database_Implementation_using_SQLite

When running an iPhone application in the iOS Simulator environment, any database files will be created on the file system of the computer on which the simulator is running. This has the advantage that you can navigate to the location of the database file, load it into the sqlite3 interactive tool and perform tasks on the data to identify possible problems occurring in the application code. If, for example, an application creates a database file named contacts.db in its documents directory, the file will be located on the host system in the following folder:

/Users//Library/Application Support/iPhone Simulator//Applications//Documents

Where is the login name of the user running the iOS Simulator session, is the version of the iOS SDK used to build the application and is the unique ID of the application.


Ok, good to know. I'll most likely use finder anyway.


Preparing an iOS 4 Application Project for SQLite Integration

By default, the Xcode environment does not assume that you will be including SQLite in your application. When developing SQLite based applications a few additional steps are required to ensure the code will compile when the application is built. Firstly, the project needs to be configured to include the libsqlite3.dylib dynamic library during the link phase of the build process. To achieve this, hold down the Ctrl key and click with the mouse on the Frameworks entry of the Groups and Files panel located on the left hand side of the Xcode project window. From the resulting menu, select the Add -> Existing Frameworks… option. From the list of framework options, scroll down to the libsqlite3.dylib entry, select it and click on the Add button.

Secondly, the sqlite3.h include file must be imported into any files where references are made to SQLite definitions, declarations or functions. This file in located in the /usr/include directory and may be imported when needed as follows:

#import "/usr/include/sqlite3.h"




Well, it's still showing the wrong way to add the dyanlib, so this is probably useless to. But, we're gradually gaining knowledge. Let's continue.

Key SQLite Functions

When implementing a database using SQLite it will be necessary to utilize a number of C functions contained within the libsqlite3.dylib library. A summary of the most commonly used functions is as follows:

sqlite3_open() - Opens specified database file. If the database file does not already exist, it is created.
sqlite3_close() - Closes a previously opened database file.
sqlite3_prepare_v2() - Prepares a SQL statement ready for execution.
sqlite3_step() - Executes a SQL statement previously prepared by the sqlite3_prepare_v2() function.
sqlite3_column_() - Returns a data field from the results of a SQL retrieval operation where is replaced by the data type of the data to be extracted (text, blob, bytes, int, int16 etc).
sqlite3_finalize() - Deletes a previously prepared SQL statement from memory.
sqlite3_exec() - Combines the functionality of sqlite3_prepare_v2(), sqlite3_step() and sqlite3_finalize() into a single function call.

This, of course, represents only a small subset of the complete range of functions available with SQLite. A full list can be found at http://www.sqlite.org/c3ref/funclist.html.



Ok, a bit of an overview of the available statements. exec will certainly come in handy.

Declaring a SQLite Database

Before any tasks can be performed on database, it must first be declared. To do so it is necessary to declare a variable that points to an instance of a structure of type sqlite3 (the sqlite3 structure is defined in the sqlite3.h include file). For example:

sqlite3 *contactDB; //Declare a pointer to sqlite database structure


Ok, good. Nothing crazy about that.



Opening or Creating a Database

Once declared, a database file may be opened using the sqlite3_open() function. If the specified database file does not already exist it is first created before being opened. The syntax for this function is as follows:

int sqlite3_open(const char *filename, sqlite3 **database);

In the above syntax, filename is the path to the database file in the form of a UTF-8 character string and database is the reference to the database structure. The result of the operation is returned as an int. Various definitions for result values are defined in the include file such as SQLITE_OK for a successful operation.

For example, the code to open a database file named contacts.db in the Documents directory of an iPhone application might read as follows. Note that the code assumes an NSString variable named databasePath contains the path to the database file:

sqlite3 *contactDB; //Declare a pointer to sqlite database structure

const char *dbpath = [databasePath UTF8String]; // Convert NSString to UTF-8

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
//Database opened successfully
} else {
//Failed to open database
}

The key point to note in the above example is that the string contained in the NSString object was converted to a UTF-8 string before being passed to the function to open the database. This is a common activity that you will see performed frequently when working with SQLite in Objective-C


It would be better to show the declaration of databasePath, but ok.

Preparing and Executing a SQL Statement

SQL statements are prepared and stored in a structure of type

sqlite3_stmt using the

sqlite3_prepare_v2() function.

For example:

sqlite3_stmt *statement;

NSString *querySQL = @"SELECT address, phone FROM contacts”;

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
//Statement prepared successfully
} else {
//Statement preparation failed
}


A prepared SQL statement may subsequently be executed using a call to the sqlite3_step() function, passing through the sqlite3_stmt variable as the sole argument:

sqlite3_step(statement);
sqlite3_finalize(statement);

Note that the sqlite3_step() function also returns a result value. The value returned, however, will depend on the nature of the statement being executed. For example, a successful insertion of data into a database table will return a SQLITE_OK result, whilst the successful retrieval of data will return SQLITE_ROW.

Alternatively, the same results may be achieved with a single call to the sqlite3_exec() function as illustrated in the following section.



This is all good info. Let's keep going.

Creating a Database Table

Database data is organized into tables. Before data can be stored into a database, therefore, a table must first be created. This is achieved using the SQL CREATE TABLE statement. The following code example illustrates the creation of a table named contacts, the preparation and execution of which is performed using the sqlite3_exec() function:

const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) == SQLITE_OK)
{
// SQL statement execution succeeded
}


Ok, although we don't really need to create a table.

Extracting Data from a Database Table

Those familiar with SQL will be aware that data is retrieved from databases using the SELECT statement. Depending on the criteria defined in the statement, it is typical for more than one data row to be returned. It is important, therefore, to learn how to retrieve data from a database using the SQLite C function calls.

As with previous examples, the SQL statement must first be prepared. In the following code excerpt, a SQL statement to extract the address and phone fields from all the rows of a database table named contacts is prepared:

sqlite3_stmt *statement;

NSString *querySQL = @"SELECT address, phone FROM contacts”;

const char *query_stmt = [querySQL UTF8String];

sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL);



Ok, I'm liking this. These statements are the key to using sqlite in an iOS app.



The statement subsequently needs to be executed. If a row of data matching the selection criteria is found in the database table, the sqlite3_step() function returns a SQLITE_ROW result. The data for the matching row is stored in the sqlite3_stmt structure and may be extracted using the sqlite3_column_() function call, where is replaced by the type of data being extracted from the corresponding field of data. Through the implementation of a while loop, the sqlite3_step() function can be called repeatedly to cycle through multiple matching rows of data until all the matches have been extracted. Note that the sqlite_column_() function takes as its second argument the number of the column to be extracted. In the case of our example, column 0 contains the address and column 1 the phone number:

while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *addressField = [[NSString alloc] initWithUTF8String:
(const char *) sqlite3_column_text(statement, 0)];

NSString *phoneField = [[NSString alloc] initWithUTF8String:
(const char *) sqlite3_column_text(statement, 1)];

// Code to do something with extracted data here

[phoneField release];
[addressField release];
}
sqlite3_finalize(statement);


Ok. I would be interested in how to handle numeric data.

Closing a SQLite Database

When an application has finished working on a database it is important that the database be closed. This is achieved with a call to the sqlite3_close() function, passing through a pointer to the database to be closed: sqlite3_close(contactDB);
Summary

In this chapter we have looked at the basics of implementing a database within an iPhone application using the embedded SQLite relational database management system. In the next chapter we will put this theory into practice and work through an example that creates a functional iPhone application that is designed to store data in a database.



Ok. Good. We've learned something. We'll tackle the next chapter in the next post. Let's take quick break, and get right back to it.




Monday, August 29, 2011

Figuring out the next step

Ok, so, we're attempting to salvage something out of today after the nightmarish white table view issue. Really need to figure that one out. But, we've redone our changes and are back to where we started earlier today. That is, we now have an initial display which calls the second "quiz" display. That second quiz display is kind of a mockup at this point - it's displaying random data that doesn't have a whole lot to do with the problem.

All right. Well, the first order of business really has got to be figuring how the data is getting loaded into the table view.

This method might give an indication:


- (void) viewWillAppear:(BOOL)animated{

// NSLog(@"view will appear was called");

// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}

currentQuestion = appState.currentQuestion;

answers = [currentQuestion answerArray];

// triggers reload, see veiwDidLoad method
[tableView reloadData];
}


So, the question and answers will somehow be pulled in into the question display at the top and the answers. Let's keep going.

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {

QuizAppDelegate *delegate =
(QuizAppDelegate *)[[UIApplication sharedApplication] delegate];

// get answers

appState = delegate.appState;

currentQuestion = appState.currentQuestion;

answers = [currentQuestion answerArray];

// change this to a lable above the table
self.title = @"JlptQuizApp";

tableView.rowHeight = 200;

[super viewDidLoad];
}


I'm not sure why I loaded it twice. Either it's a mistake, or one of them is a one-off.

This one looks interesting:

- (UITableViewCell *)tableView:(UITableView *)tv
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

// get the table veiw cell. Alwasy use the dequeue method
UITableViewCell *cell =
[ tv dequeueReusableCellWithIdentifier:@"cell"];


if( nil == cell ) {

// this sets up a resusable table cell
cell = [ [[UITableViewCell alloc]
initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

// support line break mode for multiline
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;

// 0 means any number of lines - necessary for multiline
cell.textLabel.numberOfLines = 0;

// set it up with a consistent font with the height calculation (see below)
cell.textLabel.font = [UIFont fontWithName:@"Helvetica" size:30.0];
}


// set the answer

if (indexPath.row < answers.count ) {

NSString *answer = [answers objectAtIndex:indexPath.row];
cell.textLabel.text = answer;
}

return cell;

}


So, if I were to make a rough estimate, it looks like it's using the index of the row to access the corresponding row in the array, and then sets it on the table display.

How about the question?


Ah - here it is:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

// Create label with section title
headerLabel = [[[UILabel alloc] init] autorelease];
headerLabel.textAlignment = UITextAlignmentCenter;
headerLabel.frame = CGRectMake(20, 6, 300, 30);
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.textColor = [UIColor whiteColor];
headerLabel.font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:45.0];
// support line break mode for multiline
headerLabel.lineBreakMode = UILineBreakModeWordWrap;

// 0 means any number of lines - necessary for multiline
headerLabel.numberOfLines = 0;
headerLabel.text = currentQuestion.questionTxt; <==== set here

// very important!
[headerLabel sizeToFit];

// Create header view and add label as a subview
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, SectionHeaderHeight)];
view.backgroundColor = [UIColor grayColor];
[view autorelease];
[view addSubview:headerLabel];


return view;
}



So, there's nothing to mess with in the interface here. It's just setting up the questions and answers.

So, how do they get set up? Coming in from the other side, we know we have an XMLRead class. It parses the XML and creates some kind of Array of Question object, which probably contains a question and an array of answers.

Here's some of the code:


Here it sets up the tags and the AppState object:

- (id)init
{
// Call the superclass's designated initializer
self = [super init];

// Did the superclass's initialization fail?
if (!self)
return nil;

appState = [[[AppState alloc] init] autorelease];

/* XML Tag Names */
XML_TAG_QUESTION_BLOCK = @"questions";

XML_TAG_QUESTION = @"question";

XML_TAG_QUESTION_ATTRIBUTE_NUMBER = @"number";
XML_TAG_QUESTION_ATTRIBUTE_TEXT = @"text";
XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER = @"correct_answer";

XML_TAG_ANSWERS = @"answers";
XML_TAG_ANSWER = @"answer";
XML_TAG_ANSWER_ATTRIBUTE_TEXT = @"text";

// Return the address of the newly initialized object
return self;
}



The key in the read is this method.

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{

if ([elementName isEqualToString:XML_TAG_QUESTION]){

// answer counter - initialize for new question
cntr = 1;

// allocate the question
question = [[Question alloc] init];

// get the question text
question.questionTxt = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_TEXT]; // XML_TAG_QUESTION_ATTRIBUTE_TEXT

// get get the question number
NSString *text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_NUMBER];

// convert from the text value
question.number = [text intValue];

// Now get the question answer
text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER];

// add the correct answer after converting it to an int
question.correctAnswer = [text intValue];

// add it to the question
[appState addQuestion: question];

[question release];

}

if ([elementName isEqualToString:XML_TAG_ANSWER]){

// convert counter to an object for storage in dictionary
NSNumber *counter = [NSNumber numberWithInt:cntr];

// Example of format string
// NSString *counter = [NSString stringWithFormat: @"%d", cntr];

// Get get the answer from the question
NSString *answer = [attributeDict valueForKey:XML_TAG_ANSWER_ATTRIBUTE_TEXT];

// example of NSLog
//NSLog(@"question, number is %i, text is %@, obj is %@", question.number, question.questionTxt, question);

// add the question to the answer dictionary
[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;

}

}



So, it's creating the question, a number that indicates which is the correct answer, and each question has an answer hash. AppState in turn keeps some kind of a collection of questions.

So, what the app looks like it's doing is accessing the AppState collection of questions each time the question controller displays its view, and displaying it on it's page. It's probably incrementing a counter so that the question it displays each time is different.

If we go to the main app delegate, we'll see how this reading process is initiated:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// Override point for customization after application launch.

NSString *xmlFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"questions.xml"];

NSString *xmlFileContents = [NSString stringWithContentsOfFile: xmlFilePath encoding:NSUTF8StringEncoding error:nil];

NSData *data = [NSData dataWithBytes:[xmlFileContents UTF8String] length:[xmlFileContents lengthOfBytesUsingEncoding: NSUTF8StringEncoding]];


XMLReader *xmlReader = [[XMLReader alloc] init];

[xmlReader parseXMLData: data];

self.appState = xmlReader.appState;

appState.currentQuestionNumber = 1;

[xmlReader release];

currentQuestion = [appState currentQuestion];

//navController.viewControllers = [NSArray arrayWithObject:questonViewController];
navController.viewControllers = [NSArray arrayWithObject:startController];

[window addSubview:navController.view];
[self.window makeKeyAndVisible];


return YES;
}




So, this section in particular does the parsing and setting up:

XMLReader *xmlReader = [[XMLReader alloc] init];

[xmlReader parseXMLData: data];

self.appState = xmlReader.appState;

appState.currentQuestionNumber = 1;

[xmlReader release];


Now, when it gets to the QuestionController, it initiates its own variables with the current question:

- (void) viewWillAppear:(BOOL)animated{


// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}

currentQuestion = appState.currentQuestion;

answers = [currentQuestion answerArray];

// triggers reload, see veiwDidLoad method
[tableView reloadData];
}



Which are later used to initialize the display.

Now, the only remaining question is, what increments the question number? That's probably done in the question controller:


// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}


Ok. So, the next challenge is to switch from the XML input to an SQLLite database input. We'll tackle that in the next post.

Renaming in XCode4 - don't do it.

Ok, yesterday we managed to get as far as getting the question view controller successfully showing after being called from our initial start view controller. Next we're going to do a bit of renaming of xib files, which proved to be a bit problematic. Now that we have that piece working, it will be a good time to give it another try.

Ok, after consulting with SO, it looks like one way to do it is to delete reference to the file from the project, but not the file itself. Then, rename and copy the file back in to its correct location from file finder.

That actually works with moving files as well. I had originally created the StartController in the outside of "classes". moving them in the project doesn't move them on disk, which is odd.

Uh-oh. When I renamed the StartController, it went fine. But, when I renamed the Root Controller, the question controller showed up as a blank. Worse, when I went to the backup, I still get a blank screen. Ouch.

This is a setback. I had the white screen before - and I have the feeling it turned out to be something simple - but I totally forget what it was. The white screen is an awful thing. What could possible be the cause of it? What's really bugging me is the backup doesn't work; neither one which I took this morning, which were both working fine. Which generally means I was just getting lucky with what I was doing before, like a memory leak which didn't show up till later, or something.

This is a sudden "iceburg". These are showstopper errors which basically fall into areas which you have no understanding of, and are not really in your own code. So much of it is "hidden" like an iceburg. Plus the immovability, and size of it.

I'm sure the problem has to do with this renaming of the root. I just don't know why it affects the backups.

Well, what to do? If I go way back to backup 3, I can see the question display again. It might be good to just start from there. I just don't get what went wrong. That's the frustrating thing. What if it happens again?

This is the difficult part. Ok. Just accept the loss. Redoing it will be a good review. I alway like to start with something that works. So, let's just go back to where we can see the display - and hope like hell it doesn't happen again.

1. Copy the last working project to a new folder.

Done. Test. Ok.

I should really figure out how this snapshot works, because obviously my backup strategy isn't so hot. Well, I created one - it's file create snapshot. I should really just do source control. But I'm keen to get this thing back to where I was.

Ok. So, what did I do first? I think I created the StartController. File, new, create UIVeiwController subclass, specify it goes into classes.

Ok, that's done. Now; what was next? I updated the start controller nib with a button.

Ok, that's in there now. Next is to have it called from the QuizAppDelegate. The QuizAppDelegate will need a reference to it.

Ok, substituting the code is the easy part, for the Quiz App Delegate. Now, I'd like to remember how to interface with the nib.

So, I think it needs a to have a connection made to the start view controller's view's. Yes, that's via the IBOutlet.

So, that view needs to exist as an object in the main window's nib file.

Uh-oh. A crash - right at the main. NSUnknownKeyException. It's apparently one of my renames backfiring on me again.

Ok, one more try, start again.

Ok, I've got it to the point where I'm ready to substitute this:

navController.viewControllers = [NSArray arrayWithObject:questionController];

with this:

navController.viewControllers = [NSArray arrayWithObject:startController];


And it works.

Ok, now to have the new StartController call the QuestionController.

And, we're back where we started out this morning. It was a good review. Still, it takes time.

Ok. No more renaming.