Jan 28

Mit dem iPhone auf SQLite Datenbanken zugreifen

Kategorie: Anleitungen / Dokusholeg

Nachdem wir nun unser erstes Erfolgserlebnis mit dem Hello World Programm fürs iPhone hatten, möchte ich hier eine Anleitung veröffentlichen wie man in iPhone UIKit Applikationen auf die beliebte und schnelle Datenbank SQLite zugreifen kann. Unter OS X ab 10.4 (Tiger) kann man mit Core Data auf SQLite zuzugreifen, doch in der iPhone Firmware wurde bisher noch kein Core Data implementiert. Daher muss man entweder direkt die C-API von SQLite oder einen Cocoa Wrapper benutzen. Letzteres ist um einiges komfortabler, lesbarer und wartungsfreundlicher, daher ist diese Methode zu bevorzugen. In dieser Anleitung wird das hier beschriebene SDK benutzt, die Code-Beispiele funktionieren allerdings auch mit der herkömmlichen Makefile Methode (Toolchain ohne Xcode Template).

1. Neues Projekt anlegen

Xcode öffnen und File => New Project wählen. Es erscheint folgendes Fenster:

Neues Projekt Dialog von Xcode

Unter der Gruppe Application wählt man nun den Punkt iPhone UIKit Application aus und betätigt den Next Button. Im Folgenden Dialog gibt man dem Projekt einen Namen, in unserem Fall "SQLiteTest". Nach Klick auf den Next Button öffnet sich das Projektfenster:

Xcode Projektfenster

2. SQLite3 und FMDatabase einbinden

Als Cocoa Wrapper werden wir FMDatabase von August Mueller benutzen, den Quellcode gibt es hier zum downloaden. Die heruntergeladene Datei fmdb.zip entpackt man irgendwo auf seinem Mac. Nun erzeugen wir eine Gruppe (Rechtsklick auf SQLite und dann Add => New Group) mit dem Namen "FMDatabase" und fügen folgende Dateien aus dem entpackten "fmdb" Ordner in die eben erstellte Gruppe:

  • src/FMDatabase.h
  • src/FMDatabase.m
  • src/FMDatabaseAdditions.h
  • src/FMDatabaseAdditions.m
  • src/FMResultSet.h
  • src/FMResultSet.m

Projektfenster mit FMDatabase Gruppe

Nun müssen wir noch die 3 neuen Klassen in das Makefile eintragen. Diese Arbeit nimmt einem Xcode leider nicht ab. Dazu klickt man doppelt auf das Makefile im Xcode Projektfenster und fügt die Klassen bei SOURCES ein:

SOURCES=\
	main.m \
	SQLiteTestApp.m \
	FMDatabase.m \
	FMDatabaseAdditions.m \
	FMResultSet.m

Damit das Ganze ohne Fehler kompiliert, muss man noch die sqlite3 Libraray bei den LDFLAGS im Makefle hinzufügen:

LDFLAGS=-I/usr/local/arm-apple-darwin/include  -lsqlite3 -lobjc -ObjC -framework CoreFoundation -framework Foundation -framework CoreGraphics -framework GraphicsServices -framework UIKit -framework LayerKit

Nun speichert man das Makefile ab. Das Projekt sollte sich nun ohne Fehler kompilieren lassen.

3. View erstellen

Für dieses Beispielprogramm werden wir wie beim Hello World Tutorial eine Navigationsleiste und eine Tabelle verwenden. Wir werden dafür aber eine eigene Klasse erstellen, damit das View vom Controller getrennt wird. In Xcode wählt man dazu File => New File…. Es öffnet sich folgender Dialog:

Neue Cocoa Klasse erstellen

Hier wählt man nun Objective-C class und klickt auf Next. Im folgenden Dialog gibt man der Datei den Namen "MyView.m" und klickt auf Next. Xcode erstellt nun zusätzlich zur Datei "MyView.m" auch die Headerdatei "MyView.h"

In unserer Navigationsleiste wird es einen Hinzufügen-Button geben, welcher bei Betätigung eine Delegate-Methode unseres Hauptcontrollers aufrufen soll. Der Controller soll dann dem Model einen neuen Eintrag hinzufügen. Dazu fügen wir zuerst folgendes in MyView.h nach dem #import <Cocoa/Cocoa.h> ein:

@protocol myViewDelegateProto
	- (void) addNewRow;
	- (BOOL)respondsToSelector:(SEL)aSelector;
@end

Mit diesem Protokoll machen wir die Delegate-Methoden bekannt, welche das View benutzen darf. Da wir anstelle von NSObject von UIView ableiten möchte, ändern wir die Interface Deklaration wie folgt:

@interface MyView : UIView {

Nun deklarieren wir die Klassenvariablen (Properties)

@interface MyView : UIView {
	NSMutableArray* dataArray; // Array, für die Daten der Tabelle
	UITable *table; // Tabelle
	UINavigationBar *nav; // Navigationsleiste
	id<myViewDelegateProto> _delegate;  // Delegate, welchem obiges Protokoll zugewiesen wird
}

Und nun die Klassenmethoden:

// Funktion zur Initialisierung
-(id)initWithFrame:(struct CGRect)rect andArray:(NSMutableArray*) arr;

// accessor und mutator Methjoden fürs Delegate
- (void)setDelegate: (id)delegate;
- (id)delegate;

// Aktualisiert das View mit den aktuellen Daten
- (void)updateView;

@end

Die komplette Datei sieht wie folgt aus:

MyView.h
#import <Cocoa/Cocoa.h>

@protocol myViewDelegateProto
	- (void) addNewRow;
	- (BOOL)respondsToSelector:(SEL)aSelector;
@end

@interface MyView : UIView {
	NSMutableArray* dataArray; // Array, für die Daten der Tabelle
	UITable *table; // Tabelle
	UINavigationBar *nav; // Navigationsleiste
	id<myViewDelegateProto> _delegate;  // Delegate, welchem obiges Protokoll zugewiesen wird
}

// Funktion zur Initialisierung
-(id)initWithFrame:(struct CGRect)rect andArray:(NSMutableArray*) arr;

// accessor und mutator Methjoden fürs Delegate
- (void)setDelegate: (id)delegate;
- (id)delegate;

// Aktualisiert das View mit den aktuellen Daten
- (void)updateView;

@end

Als nächstes müssen wir die Implementation der Klasse erstellen. Dazu öffnen wir die Datei MyView.m und erstellen folgende Methode:

-(id)initWithFrame:(struct CGRect)rect andArray:(NSMutableArray*) arr
{
	[super initWithFrame: rect];
	dataArray = arr;
	[dataArray retain];
	_delegate = nil;

	table = [[UITable alloc] initWithFrame: CGRectMake(0.0f, 48.0f, 320.0f, 480.0f - 16.0f - 32.0f)];

	UITableColumn *col = [[[UITableColumn alloc] initWithTitle: @"title" identifier: @"title" width: 320.0f] autorelease];
	[table addTableColumn: col];

	[table setSeparatorStyle:1];
	[table setRowHeight:50];
	[table setDataSource: self];
	[table setDelegate: self];
	[table reloadData];

	nav = [[UINavigationBar alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 48.0f)];
	[nav showButtonsWithLeftTitle: nil rightTitle: @"Add" leftBack: NO];
	[nav setBarStyle: 0];
	[nav setDelegate:self];

	[self addSubview: nav];
	[self addSubview: table]; 

	return self;
}

Die Accessor und Mutator Methoden für das Delegate sehen wie folgt aus:

- (void)setDelegate: (id)delegate
{
	_delegate = delegate;
}

- (id)delegate
{
	return _delegate;
}

Die updateView-Methode ruft die reloadData-Methode der UITable auf:

- (void)updateView;
{
	[table reloadData];
}

Da wir in der initWithFrame-Methode der UITable das View selbst als Delegate und DataSource zugewiesen haben müssen wir natürlich die erforderlichen Methoden hinzufügen:

- (int) numberOfRowsInTable: (UITable *)table
{
	return [dataArray count];
}

-(UITableCell*)table:(UITable*)table cellForRow:(int)row column:(int)column{
	UIImageAndTextTableCell *cell = [[UIImageAndTextTableCell alloc] init];
	[cell setTitle:[dataArray objectAtIndex:row]];
	return cell;
}

Jetzt fehlt noch die Delegate-Methode der Navigationsleiste:

- (void)navigationBar:(UINavigationBar*)navbar buttonClicked:(int)button
{
	switch (button)
	{
		case 0:
			if ([_delegate respondsToSelector:@selector(addNewRow)]) {
				[_delegate addNewRow];
			}
		break;
	}
}

In dieser Methode überprüfen wir den Button, welcher vom User geklickt wurde. Da wir nur einen Button haben ist dies der Button mit dem Wert 0 (Null). Mit der Methode respondsToSelector überprüfen wir ob es die Delegate-Methode unseres Controllers wirklich gibt. Nur dann wird sie aufgerufen.

Zu guter Letzt müssen wir noch in der dealloc Methode den Speicher wieder freigeben:

-(void)dealloc
{
	[dataArray release];
	[table release];
	[nav release];
	[super dealloc];
}

Der komplette Code sieht wie folgt aus:

MyView.m
#import "MyView.h"

@implementation MyView

-(id)initWithFrame:(struct CGRect)rect andArray:(NSMutableArray*) arr
{
	[super initWithFrame: rect];
	dataArray = arr;
	[dataArray retain];
	_delegate = nil;

	table = [[UITable alloc] initWithFrame: CGRectMake(0.0f, 48.0f, 320.0f, 480.0f - 16.0f - 32.0f)];

	UITableColumn *col = [[[UITableColumn alloc] initWithTitle: @"title" identifier: @"title" width: 320.0f] autorelease];
	[table addTableColumn: col];

	[table setSeparatorStyle:1];
	[table setRowHeight:50];
	[table setDataSource: self];
	[table setDelegate: self];
	[table reloadData];

	nav = [[UINavigationBar alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 48.0f)];
	[nav showButtonsWithLeftTitle: nil rightTitle: @"Add" leftBack: NO];
	[nav setBarStyle: 0];
	[nav setDelegate:self];

	[self addSubview: nav];
	[self addSubview: table]; 

	return self;
}

- (void)setDelegate: (id)delegate
{
	_delegate = delegate;
}

- (id)delegate
{
	return _delegate;
}

- (void)updateView;
{
	[table reloadData];
}

- (int) numberOfRowsInTable: (UITable *)table
{
	return [dataArray count];
}

-(UITableCell*)table:(UITable*)table cellForRow:(int)row column:(int)column{
	UIImageAndTextTableCell *cell = [[UIImageAndTextTableCell alloc] init];
	[cell setTitle:[dataArray objectAtIndex:row]];
	return cell;
}

- (void)navigationBar:(UINavigationBar*)navbar buttonClicked:(int)button
{
	switch (button)
	{
		case 0:
			if ([_delegate respondsToSelector:@selector(addNewRow)]) {
				[_delegate addNewRow];
			}
		break;
	}
}

-(void)dealloc
{
	[dataArray release];
	[table release];
	[nav release];
	[super dealloc];
}

@end

4. Application Controller

Nun müssen wir nur noch unseren Controller anpassen. Zuerst öffnen wir die Header-Datei SQLiteTest.h und passen sie an:

SQLiteTest.h
#import <Cocoa/Cocoa.h>
#import <GraphicsServices/GraphicsServices.h>
#import <LayerKit/LayerKit.h>
#import "FMDatabase.h"
#import "MyView.h"

@interface SQLiteTestApp : UIApplication {
	UIWindow *mainWindow;
	FMDatabase *db;
	NSMutableArray *dataArray;
	MyView *mainView;
}

- (void)updateDataArray;
- (void)addNewRow;

@end

Nun öffnen wir die Datei SQLiteTest.m mit der Implementation. Jedes iPhone UIKit Programm ruft nach dem Start der GUI die Methode applicationDidFinishLaunching auf, in der man weitere Initialisierungen seines Programms vornehmen kann. Wir initialisieren hier unser Daten-Array und erstellen die Datenbankdatei, falls noch nicht vorhanden. Am Ende initialisieren wir das Hauptfenster sowie unse zuvor programmiertes View:

- (void)applicationDidFinishLaunching:(GSEventRef)event;
{

	dataArray = [[NSMutableArray alloc] init];
	db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];
	[db retain];

	if (![db open]) {
		NSLog(@"Could not open db.");
	}

	[db executeUpdate:@"CREATE TABLE test (field text)", nil];

	[self updateDataArray];

	struct CGRect rect = [UIHardware fullScreenApplicationContentRect];
	rect.origin.x = rect.origin.y = 0.0f;

	mainView = [[MyView alloc] initWithFrame: rect andArray: dataArray];
	[mainView setDelegate:self];

	 mainWindow = [[UIWindow alloc] initWithContentRect:[UIHardware fullScreenApplicationContentRect]];
	[mainWindow orderFront:self];
	[mainWindow makeKey:self];
	[mainWindow _setHidden: NO];

	[mainWindow setContentView: mainView];

}

Da wir den Inhalt der Datenbank in einem Daten-Array speichern, müssen wir nach jedem Ändern der Datenbank auch unser Array neu befüllen. Dies erledigen wir in der Methode updateDataArray:

- (void)updateDataArray
{
	FMResultSet *rs = [db executeQuery:@"SELECT * FROM test ORDER BY field", nil];
	[dataArray removeAllObjects];
	while ([rs next]) {
		[dataArray addObject: [rs stringForColumn:@"field"]];
	}

	[rs close];
}

In unserem View rufen wir die Delegate-Methode addNewRow auf, welche wir noch schreiben müssen:

// delegate Methode vom mainView
- (void)addNewRow
{
	[db executeUpdate:@"insert into test (field) values (?)", @"Hallo Welt", nil];
	[self updateDataArray];
	[mainView updateView];
}

Bevor das Programm beendet wird, wird ie Methode applicationWillTerminate aufgerufen. Hier räumt man normalerweise auf. genau das tun wir auch:

- (void)applicationWillTerminate
{
	[mainWindow release];
	[mainView release];
	[db close];
	[db release];
	[dataArray release];
}

Die fertige Datei sieht wie folgt aus:

SQLiteTest.m
#import "SQLiteTestApp.h"
#import "MyView.h"

@implementation SQLiteTestApp

- (void)applicationDidFinishLaunching:(GSEventRef)event;
{

	dataArray = [[NSMutableArray alloc] init];
	db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];
	[db retain];

	if (![db open]) {
		NSLog(@"Could not open db.");
	}

	[db executeUpdate:@"CREATE TABLE test (field text)", nil];

	[self updateDataArray];

	struct CGRect rect = [UIHardware fullScreenApplicationContentRect];
	rect.origin.x = rect.origin.y = 0.0f;

	mainView = [[MyView alloc] initWithFrame: rect andArray: dataArray];
	[mainView setDelegate:self];

 mainWindow = [[UIWindow alloc] initWithContentRect:[UIHardware fullScreenApplicationContentRect]];
 [mainWindow orderFront:self];
 [mainWindow makeKey:self];
	[mainWindow _setHidden: NO];

	[mainWindow setContentView: mainView];

}

- (void)updateDataArray
{
	FMResultSet *rs = [db executeQuery:@"SELECT * FROM test ORDER BY field", nil];
	[dataArray removeAllObjects];
	while ([rs next]) {
		[dataArray addObject: [rs stringForColumn:@"field"]];
	}

	[rs close];
}

// delegate Methode vom mainView
- (void)addNewRow
{
	[db executeUpdate:@"insert into test (field) values (?)", @"Hallo Welt", nil];
	[self updateDataArray];
	[mainView updateView];
}

- (void)applicationWillTerminate
{
	[mainWindow release];
	[mainView release];
	[db close];
	[db release];
	[dataArray release];
}

@end

Nun sollte sich das Beispiel kompilieren lassen. Wenn man das Programm auf dem iPhone startet erscheint folgender Screen:

SQLite Test Programm Startscreen

Durch Klick auf den Add-Button kann man neue Einträge der Tabelle hinzufügen:

SQLite Test Programm Startscreen 2

Das komplette Xcode Projekt kann man hier herunterladen.

3 Kommentare zu “Mit dem iPhone auf SQLite Datenbanken zugreifen”

  1. Sigar sagt:

    Funktioniert ja super. Gibts irgendwo noch eine andere Ressource mit Beispielen, wo man lernt, wie man ein Eingabefeld, Text, Bilder oder anderes auf den Bildschirm zaubert bzw. wie man die Klassen benutzt?

  2. holeg sagt:

    Ich habe vor einige Artikel über das Thema UIKit hier zu veröffentlichen. Wenn Du nicht so lange warten willst kannst Du in diesem Forum einige Code Beispiele finden. Du musst Dich dazu aber anmelden. Eine andere Alternative ist Google Code. hier einfach mal nach iphone suchen. In der Trefferliste sind viele Programme dabei, welche man auch von Installer.app her kennt. Den Quellcode der Apps bekommt man dann per SVN. Bei dem entsprechenden Google-Code Projekt unter dem Tab "Source" findet man den SVN Befehl um sich den Quellcode zu ziehen .

  3. stepra sagt:

    Hey gutes tutorial,
    funktionieren die sqlite klassen auch mit osx?

Dein Kommentar