Wednesday, August 31, 2011

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.

No comments:

Post a Comment