このブログのチュートリアルを順に読み進めてくださった方なら、このチュートリアルがUITableVIewを優先的に取り組んできたことをご存知かと思います。
この理由としては、多くのアプリケーションが、このシンプルなコントロールを使って開発されているからです。
今回はUITableViewの最後のチュートリアルとして、今までのチュートリアルで学んできたことをすべて使い、SQLiteの技術を追加し、ToDoリストを作成していきます。
また、多くの機能を持ったテーブルセルを追加し、iPhoneが提供している他のコントロールについても学んでいきます。
過去の記事を既読の方を想定していますので、まだの方は、そちらの記事に先に目を通されることをおすすめします。
このチュートリアルは、いくつかのシリーズから成り立つので、過去の記事に比べ少し長くなります。この最初のチュートリアルで学ぶことは以下です。
今回学ぶこと:
- Navigation-Based Application の新規作成
- データベースの作成
- プロジェクトにデータベースの追加
- SQLite3のフレームワークを追加
- ToDoクラスのオブジェクトの作成
- データベースの初期化
それでは始めましょう。
XCodeを起動し、新規プロジェクトを作成します。Navigation-Based Application を選択し、名前は「todo」とします。
次に、todoデータベースを作成します。あなたのMac上のターミナルを起動してください。アプリケーション > ユーティリティ > ターミナル にあると思います。分からない方は、control + space で Spotlight を起動し、「ターミナル」もしくは、「terminal」と入力すれば、見つかると思います。
以下、ターミナルを使って、SQLiteデータベースを作成する方法について書きますが、ターミナルが苦手な方は、便利なGUIツールを使ってデータベースを作成することも可能ですので、その場合は、こちらの記事を参考にしてください。
XCodeがインストールされていれば、既にsqlite3もインストール済だと思いますので、ターミナル上で、
sqlite3
と入力します。これでsqlite3が起動します。
sqlite3を終了するには
.quit
と入力します。
もし、sqlite3がインストールされていない場合は、インストールしてください。インストール方法については、ネット検索すれば、多くの記事が見つかると思います。
それでは、データベースを作成していきます。
sqlite3 todo.sqlite
ターミナル上からsqliteにアクセスしていない状態で、上記のコマンドを実行します。これで、SQLiteが起動し、「todo.sqlite」というデータベースが読み込まれます。データベースの初期状態では、テーブルは存在せず、空の状態です。
今回作成するアプリケーションは単純なので、データベースは1つのみ作成します。以下のコマンドを実行し、「todo」という名前のテーブルを作成します。
CREATE TABLE todo(pk INTEGER PRIMARY KEY, text VARCHAR(25), priority INTEGER, complete BOOLEAN);
sqlite内で使うコマンドは最後に「;」が必要です。データベース作成コマンドの形式は、以下です。
create table テーブル名(カラム名 カラム定義, カラム名 カラム定義, ・・・);
先ほど実行したコマンドを簡単に説明します。
最初の CREATE TABLE todo は、todoという名前のテーブルを作成するという意味です。
括弧の中の pk INTEGER PRIMARY KEY は、integer型の「pk」という名前のカラムを作り、テーブルのプライマリーキーに設定しています。プライマリーキーに設定することで、このデータベースに行(項目)が追加されるごとに、このpk が1つずつ増加していきます。これにより、各行をユニークなものとして定義することが出来ます。
その他の項目は、プライマリーキー以外の部分は同じ仕様なので、分かるかと思います。
これで、必要なテーブルが作成されましたので、いくつかデータを追加していきましょう。最終的にはアプリケーション側からtodo項目を追加していきますが、今は、ターミナルから、いくつかデフォルトのものを設定していきます。
以下のコマンドをターミナルで実行してください。
INSERT INTO todo(text,priority,complete) VALUES(‘iPhoneを買う’,3,0);
INSERT INTO todo(text,priority,complete) VALUES(‘SQLiteを勉強する’,1,0);
INSERT INTO todo(text,priority,complete) VALUES(‘Objective-Cを勉強する’,1,0);
INSERT INTO todo(text,priority,complete) VALUES(‘ゴミを捨てに行く’,2,0);
これでいくつかのtodo項目が追加されました。priority の値は1~3の数値が配置されていますが、これについては、後ほど触れることにします。(説明が不要な読者も多いかとは思いますが…)
ここまでで、必要なデータベースは作成されましたので、sqlite3を終了します。
それでは、XCodeに戻りましょう。Resources フォルダを右クリック(ctrl + クリック )し、追加 > 既存のファイル から、先ほど作成したtodo.sqlite ファイルを探し追加します。
デフォルトでは、/Users/ユーザー名/ の場所にsqliteファイルが作成されていると思います。
それを見つけて追加をクリックすると以下のような画面が出てきます。
「デスティネーション…」の項目をチェックして「追加」をクリックください。
追加されると以下のようにResourceフォルダに追加されます。
これで、データベースの追加が出来ましたので、これを使えるようにするためにObjective-Cのライブラリを読み込みます。
Frameworks フォルダを右クリックし、追加 > 既存のフレームワークを選択します。「libsqlite3.0.dylib」を探し、追加します。
これで以下の画像のように選択したフレームワークが追加されます。
todoの情報を扱うために、あるオブジェクトを作る必要があります。最終的には、これらの情報の配列を作成しUITableViewで利用します。
XCode上から ファイル > 新規ファイルをクリックし、NSObject のサブクラスを追加します。
名前は、Todo.mとして、”同時にtodo.hも作成する”にチェックし、保存してください。
Todo.hを開き、以下のように編集します。
#import <Foundation/Foundation.h>
#import <sqlite3.h>
@interface Todo : NSObject {
sqlite3 *database;
NSInteger primaryKey;
NSString *text;
}
@property (assign,nonatomic,readonly)NSInteger primaryKey;
@property (nonatomic,retain)NSString *text;
-(id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db;
@end
いくつかの新しい記述がありますね。まず、sqlite3型のデータベース変数です。これはアプリケーションのデータベースを参照し、todoオブジェクトとやりとりを可能にするものです。#import文を忘れないようにしてください。
次に、primary key についてですが、プロパティで宣言されているassignとreadonlyは、コンパイラにこの変数は一度設定されると、変更されないと宣言するものです。これによって、各todo項目はユニークだと定義されます。
This tells the compiler that this variable, once assiged, can not be changed again. This is good since each todo will be uniquely identified by this variable.
また、initWithPrimaryKey を宣言していますが、これはこのオブジェクトのコンストラクタです。このコンストラクタは、整数型をとります。
It takes an integer to assign as the primary key and an sqlite3 object to use as the database reference.
このメソッドを実装していきましょう。
Todo.m を開き、以下のように編集してください。
#import “Todo.h”
static sqlite3_stmt *init_statement = nil;
@implementation Todo
@synthesize primaryKey,text;
-(id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db {
if(self = [super init]){
primaryKey = pk;
database = db;
if (init_statement == nil) {
const char *sql = “SELECT text FROM todo WHERE pk=?”;
if(sqlite3_prepare_v2(database, sql, –1, &init_statement,NULL) != SQLITE_OK){
NSAssert1(0, @”Error:failed to prepare statement with message ‘%s’.”, sqlite3_errmsg(database));
}
}
sqlite3_bind_int(init_statement, 1, primaryKey);
if(sqlite3_step(init_statement)== SQLITE_ROW){
self.text = [NSString stringWithUTF8String🙁char *)sqlite3_column_text(init_statement, 0)];
}else {
self.text = @”Nothing”;
}
sqlite3_reset(init_statement);
}
return self;
}
@end
このコードには新しい記述がいくつかありますので、説明していきます。
static sqlite3_stmt *init_statement = nil
これはデータベースからtodoデータを取得するときに初期化されたステートメントを保持します。このステートメントは任意のインスタンスから独立しており、静的です。
-(id) から始まる行は、todoオブジェクトが初期化される前に、親クラスであるNSObjectクラスを初期化します。
initWithPrimaryKeyメソッドに渡されたパラメータは、ローカルの主キーとデータベースオブジェクトを設定します。
if(init_statement == nil)の行は、アプリケーションの起動ごとに一度だけ発生します。もし、それがnullの場合は、SQLステートメントを含む新しい文字列を作成します。
SQLに詳しい方には、一つの例外を除いて、簡単なSQL文になっていると思います。
なんで”?”があるんでしょうか。
SQL文がコンパイルされた後、そこ(?マーク)に値を置き換えることが出来ます。
So this allows us to have 1 generic SQL statement, but bind different values to it to retrieve different results.
sqlite3_prepare_v2 の行は、init_statementに保持し、準備をしています。
このif行は、正しく処理が完了したかどうかをチェックし、問題があればエラーを表示します。
sqlite3_bind_int の行は、単純に?マークとtodoオブジェクトのプライマリーキーを置き換えています。つまり、以下のような文になります。
SELECT text FROM todo WHERE pk = 1;
SELECT text FROM todo WHERE pk = 2;
SELECT text FROM todo WHERE pk = 3;
SELECT text FROM todo WHERE pk = n;
その後に the sqlite3_step(init_statement) メソッドが呼び出されています。このメソッドはデータベース上でSQL文を実行します。
It is contained inside of an if statement to make sure it executed properly.
これで、ついにtodoデータにアクセスできるようになりました。次の行にいきましょう。
self.text = [NSString stringWithUTF8String:(char*) sqlite3_column_text(init_statement,0)];
とても長い一文ですね。詳しく見ていきましょう。
sqlite3_column_text メソッドは、データベースから文字列のオブジェクトを取り出すことをSQLに伝えます。これは2つのパラメータを持っています。最初のパラメータは、使用されているSQLへの参照で、2つ目は、文字列を取り出したいカラム番号を指定します。
今回のケースでは、1つのカラムしか持たないため、「0」という1つのインデックスしかありません。
次に、(char *)は、文字列へキャストします。(不要かもしれませんが、推奨します。)
最後に、返されたデータによりNSStringオブジェクトをビルドし、それを self.textに割り当てます。
次に、todoAppDelegate.h を開き、以下のように編集してください。
#import <UIKit/UIKit.h>
#import <sqlite3.h>
@interface todoAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
UINavigationController *navigationController;
sqlite3 *database;
NSMutableArray *todos;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;
@property (nonatomic, retain) NSMutableArray *todos;
@end
NSMutableArray のtodoオブジェクトを作成していることに注意してください。これは、前回のフルーツの例のように、todoアイテムを操作する配列です。最終的にはこの配列をつかって、UITableViewを作成します。
また、新しく、sqlite3.h のインポート文と sqlite3 *database の行が追加されています。
それでは、todoAppDelegate.mを開き、いくつかコードを追加していきます。
#import “todoAppDelegate.h”
#import “RootViewController.h”
#import “Todo.h”
@interface todoAppDelegate (Private)
-(void)createEditableCopyOfDatabaseIfNeeded;
-(void)initializeDatabase;
@end
@implementation todoAppDelegate
@synthesize window;
@synthesize navigationController;
@synthesize todos;
ここでは新しく、プライベートなインターフェースを宣言しています。これは、このオブジェクト特有のものなので、.h ファイルで宣言する必要がないので、ここに宣言しています。
実装した二つの機能は、createEditableCopyOfDatabaseIfNeeded と initializeDatabaseです。
これらのコードの大部分は、Appleの SQLBooks チュートリアルに書かれています。
-(void)createEditableCopyOfDatabaseIfNeeded{
BOOL success;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@”todo.sqlite”];
success = [fileManager fileExistsAtPath:writableDBPath];
if(success) return;
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@”todo.sqlite”];
success = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
if (!success) {
NSAssert1(0,@”Failed to create writable database file with message ‘%@’.”,[error localizedDescription]);
}
}
このメソッドが本質的にはしていることは、あなたのプロジェクトフォルダーから、iPhone上のドキュメントフォルダーへ、データベースをコピーしています。
これは、ドキュメントフォルダに既にデータベースが存在するかの最初のチェック時のみ発生します。
関数や変数の名称が、とても理解しやすいように、appleが記述してくれているので、名前を追っていけば、理解しやすいと思います。
次にinitializeDatabaseの機能を実装していきます。
以下のコードを追加してください。
-(void)initializeDatabase{
NSMutableArray *todoArray = [[NSMutableArray alloc] init];
self.todos = todoArray;
[todoArray release];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *path = [documentsDirectory stringByAppendingPathComponent:@”todo.sqlite”];
if(sqlite3_open([path UTF8String], &database) == SQLITE_OK){
const char *sql = “SELECT pk FROM todo”;
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, sql, –1, &statement, NULL) ==SQLITE_OK){
while (sqlite3_step(statement) == SQLITE_ROW) {
int primaryKey = sqlite3_column_int(statement, 0);
Todo *td = [[Todo alloc] initWithPrimaryKey:primaryKey database:database];
[todos addObject:td];
[td release];
}
}
sqlite3_finalize(statement);
}else{
sqlite3_close(database);
NSAssert1(0, @”Failed to open database with message ‘%s’.”,sqlite3_errmsg(database));
}
}
たくさんの記述がありますが、がんばっていきましょう!
最初の行は、NSMutableArrayを作成し、初期化しています。
そして、私たちのオブジェクトのtodos配列にこの配列を設定して、一時的なオブジェクトをリリースします。
その次の3行は、ドキュメントフォルダの中に作成されたデータベースを探します。
sqlite3_openの行は、データにアクセスするためにデータベースに接続します。正常に接続されれば、todoアイテムを取り出します。
const char *sql = “SELECT pk FROM todo”;
この行は、プライマリーキーを取得するためのSQL記述です。
Todo.m で行ったのと同様の記述を準備します。プライマリーキーを取得する条件がないため、今回は”?”マークはありません。単に、すべてのデータベース上のプライマリーキーが欲しいだけなので。
while 文で各結果からプライマリーキーを取り出しています。
int primaryKey = sqlite3_column_int(statement,0);
これは、Todo.m クラスで文字列を取り出すのによく似ています。sqlite3_column_text の代わりに sqlite3_column_int が使われている点をが違うだけです。
プライマリーキーを取得した後、initWithPrimaryKey コンストラクタを呼び出しています。プライマリーキーはデータベースへの参照として渡されています。最終的に、私たちは新たに作成されたTodoオブジェクトをtodosの配列に加えます。
最後の、sqlite3_finalize は、メモリを解放します。
このチュートリアルの最後として、データベースを作成し、初期化するために、これらの機能を呼び出します。applicationDidFinishLaunching の箇所に以下を追加してください。
– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self createEditableCopyOfDatabaseIfNeeded];
[self initializeDatabase];
// Override point for customization after application launch.
// Add the navigation controller’s view to the window and display.
[window addSubview:navigationController.view];
[window makeKeyAndVisible];
return YES;
}
単に、これらの機能を呼び出しました。
今回のチュートリアルは、これで完了ですが、この状態でビルドして実行しても、何も表示されません。
ToDO型ではありませんが、自分も初アプリに挑戦中で、しかもデータ書き出しを必要とするのでNSUserDefaultでいいか、それともSQLiteなのか、と悩んでいたところでした。非っ常に参考になって助かっています。 続きが早く読みたい・・・^^;
@loop_26
コメントありがとうございます!
私自身、iPhoneアプリに関しては、全くの初心者ですので、海外のチュートリアルを参考にしながら、自分で実行し、勉強している状態です。
拙い解説ではありますが、同じような境遇の方の参考になれば嬉しいなと思ってます。
私自身の理解度が低いこともあり、なかなか上手く説明出来てない箇所もあるかと思いますが、、、頑張ります(汗)
loop_26さんも頑張って初アプリ完成させてくださいね☆