[8]:SQLiteを使ってToDoリストアプリを作成(3)

この記事はタイトルの通り「SQLiteを使ってToDoリストアプリを作成」の第三部になるので、前回までの記事を既に読んでくださっている方を対象にしています。

このチュートリアルのメインは、選択されたtodoアイテムを表示することです。todoステータスを更新する方法も扱います。

このチュートリアルでは、todoデータベースの最後の変数complete のstatus(実行中 or 完了)を取得する必要があります。既に設定した通り、completeフィールドの値はboolean(1= 正、2=偽)です。この値をデータベースから取り出し、todoオブジェクトに関連づける必要があります。

まず、最初に、この値を取得するためのプロパティを作成していきましょう。

todo.h を開き、以下のコードを追加してください。

#import <Foundation/Foundation.h>

#import <sqlite3.h>

@interface Todo : NSObject {

sqlite3 *database;

NSString *text;

NSInteger primaryKey;

NSInteger priority;

NSInteger status;

BOOL dirty;

}

@property (assign, nonatomic, readonly) NSInteger primaryKey;

@property (nonatomic, retain) NSString *text;

@property (nonatomic) NSInteger priority;

@property (nonatomic) NSInteger status;

– (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db;

– (void)updateStatus:(NSInteger) newStatus;

– (void)updatePriority:(NSInteger) newPriority;

– (void)dehydrate;

@end

ここではいくつかの変更があります。まず、NSintegerステータスを追加しました。これは、todoが完了したかどうかのステータスに関連づけられるプロパティです。さらに、それからpropertyを作成しています。

次に、dirtyと呼ばれるBOOL値があります。

このオブジェクトをtodoが変更されている場合を示すために使います。

これがどのような役割を果たすのかは、dehydrate メソッドを実装するときに分かります。

加えて、以下の3つのメソッドを宣言しています。

updateStatus – ステータスの値を更新したい時に呼び出すメソッドです。

updatePriority – 同じように、priorityを更新したい時に呼び出すメソッドです。

dehydrate – 基本的に、todoの状態をデータベースに保存する時に使います。プログラムの終了時に各Todo項目に対して、このメソッドを呼び出すことになります。

status変数をsynthesize行に追加するのを忘れないようにしてください。

以前行ったのと同様に、コンパイルされたdehydrationステートメントを保持するために、静的なsqlite3_stmt を作成する必要があります。

Todo.m を開いて、以下を追加してください。

static sqlite3_stmt *init_statement = nil;

static sqlite3_stmt *dehydrate_statment = nil;

@implementation Todo

@synthesize primaryKey,text,priority,status;

メソッドを実装していきましょう。

– (void)updateStatus:(NSInteger)newStatus {

self.status = newStatus;

dirty = YES;

}

– (void)updatePriority:(NSInteger)newPriority {

self.priority = newPriority;

dirty = YES;

}

– (void) dehydrate {

if(dirty) {

if (dehydrate_statment == nil) {

const char *sql = “UPDATE todo SET priority = ?,complete = ? WHERE pk=?”;

if (sqlite3_prepare_v2(database, sql, –1, &dehydrate_statment, NULL) != SQLITE_OK) {

NSAssert1(0, @”Error: failed to prepare statement with message ‘%s’.”, sqlite3_errmsg(database));

}

}

sqlite3_bind_int(dehydrate_statment, 3, self.primaryKey);

sqlite3_bind_int(dehydrate_statment, 2, self.status);

sqlite3_bind_int(dehydrate_statment, 1, self.priority);

int success = sqlite3_step(dehydrate_statment);

if (success != SQLITE_DONE) {

NSAssert1(0, @”Error: failed to save priority with message ‘%s’.”, sqlite3_errmsg(database));

}

sqlite3_reset(dehydrate_statment);

dirty = NO;

}

}

@end

最初の二つのメソッド(udpateStatus と updatePriority)は、分かりやすいです。これらは、todoのpriorityとstatusを更新し、dirtyプロパティを 1 に設定します。

これは、todoが変更され、データベースに保存する必要があるという印です。

最後に、dehydrate メソッドです。プログラムの終了時に各todoにこのメソッドを呼び出します。dirty プロパティがYESなら、新しいデータをデータベースに登録する必要があります。

データベースのコードは、前のチュートリアルのコードに似ています。まず、dehydrate_statement が nil かどうか調べます。

これは、このメソッドが呼び出された最初の時だけに実行されます。

次に、update文を作成し、bind によって各”?”に変数をあてはめます。注意すべきは、数字が、左から右へ”?”の番号を表していることです。

最後に、sqlite3_stepを呼び出して、SQLiteステートメントを実行し、そして、ステートメントをリセットします。

Todo.m で最後にするのは、completeフィールドを取得するために、initWithPrimaryKey メソッド内のSELECT文を変更することです。

以下のようにコードを変更してください。

@synthesize primaryKey,text,priority,status;

– (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db {

if (self = [super init]) {

primaryKey = pk;

database = db;

// Compile the query for retrieving book data. See insertNewBookIntoDatabase: for more detail.

if (init_statement == nil) {

// Note the ‘?’ at the end of the query. This is a parameter which can be replaced by a bound variable.

// This is a great way to optimize because frequently used queries can be compiled once, then with each

// use new variable values can be bound to placeholders.

const char *sql = “SELECT text,priority,complete 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));

}

}

// For this query, we bind the primary key to the first (and only) placeholder in the statement.

// Note that the parameters are numbered from 1, not from 0.

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)];

self.priority = sqlite3_column_int(init_statement,1);

self.status = sqlite3_column_int(init_statement,2);

} else {

self.text = @”Nothing”;

}

// Reset the statement for future reuse.

sqlite3_reset(init_statement);

}

return self;

}

ここはあまり変更点はありません。第一の変更は、synthesize行にstatusを追加したことです。次は、SQL文が、以下のように、データベースからcompleteフィールドを取得するように変更された点です。

SELECT text,priority,complete FROM todo WHERE pk=?

最後に、“self.status = sqlite3_column_in(init_statement,2);” という行があります。

これは、sqlデータ配列内の2番目のインデックスにstatusプロパティを割り当てています。このフィールドが使えるようになります。

そして、ナビゲーションが正しく機能するように、メインビューにタイトルを追加する必要があります。

rootViewController.m を開き、viewDidLoad メソッドに以下のように編集してください。

– (void)viewDidLoad {

// Add the following line if you want the list to be editable

// self.navigationItem.leftBarButtonItem = self.editButtonItem;

self.title = @”Todo Items”;

}

Todo詳細情報viewの作成

UITableViewからtodoが選択された時に表示される詳細情報のviewを作成します。XCode上から、いずれかのxib(nib)ファイルをダブルクリックし、Interface Builder を起動します。ファイル > 新規作成 をクリックし、viewを選択します。

792-1

そして、作成されたviewに以下を追加します。

  • UITextView
  • UISegmentedControl – これには、セグメントの数を「3」に設定する必要があります。また、このオプションの下にドロップダウンメニューが表示されています。各セグメントを選択し、タイトルにpriorityを記入してください。ここではスクリーンショットです。それぞれのpriority(低、中、高)にタイトルを設定してください。
  • UILabel – これは status を表示するのに使います。
  • UIButton – ユーザがstatusを更新するのにクリックするボタンです。(完了、未完了など)

792-2

上記のようなインターフェースが出来上がったでしょうか?色やサイズなどは、お好みで設置してください。command + S で保存してください。もしくは、ファイルからsaveを選んで保存してください。名前はTodoViewController とします。保存先のプロジェクトを間違えないようにしてください。この場合は、Todoというプロジェクトに保存しています。

792-3

以下のようなダイアログが出たら、プロジェクト名にチェックを入れて実行してください。

792-4

以上で、Interfaec Bilder を閉じます。次に、viewController クラスを追加し、変数をこのviewのインターフェースに設定します。

TodoViewControllerクラスを作成

XCode上から ファイル > 新規ファイル をクリックし、UIViewController Subclass を選択します。

792-5

ファイル名は「TodoViewController」とし、”TodoViewController.h”ファイルも同時に作成にチェックします。

792-6TodoViewController.h を開き、以下のように編集します。

#import <UIKit/UIKit.h>

#import “Todo.h”

@interface TodoViewController : UIViewController {

IBOutlet UITextView *todoText;

IBOutlet UISegmentedControl *todoPriority;

IBOutlet UILabel *todoStatus;

IBOutlet UIButton *todoButton;

Todo *todo;

}

@property(nonatomic,retain) IBOutlet UITextView         *todoText;

@property(nonatomic,retain) IBOutlet UISegmentedControl *todoPriority;

@property(nonatomic,retain) IBOutlet UILabel            *todoStatus;

@property(nonatomic,retain) IBOutlet UIButton           *todoButton;

@property(nonatomic,retain) Todo *todo;

– (IBAction) updateStatus:(id) sender;

– (IBAction) updatePriority:(id) sender;

@end

基本的に、Interface Builder アウトレットの各部品をXCodeと接続しています。

UIButtonがIBOutletを持っていることに注意してください。これは、todoが完了されているかどうかによって、ボタンの文字列を更新する必要があるからです。

また、updateStatus という名前のIBAction を設定しています。

これは後でボタンと接続されます。これはtodoアイテムのステータスの切り替えスイッチです(完了 or  未完了)。

最後に、updatePriority メソッドです。このメソッドは、UISegmentControl 上のpriority のセグメントがユーザによって選択された時に呼び出されます。

次に、TodoViewController.m を開いて、以下のコードを追加してください。

@synthesize todoText,todoPriority,todoStatus,todoButton,todo;

これは、各変数のgetterとsetterです。

これらのコードをインターフェースに接続する前に、ユーザがtodoが完了したと記録するためにボタンを押した時と、UISegmentedControl のセグメントが押された時に、呼ばれるメソッドを実装する必要があります。

先ほど追加したsynthesize行の下に以下のメソッドを追加してください。

– (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {

if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {

// Initialization code

}

return self;

}

– (IBAction) updateStatus:(id) sender {

if(self.todo.status == 0) {

[self.todoButton setTitle:@”Mark As In Progress” forState:UIControlStateNormal];

[self.todoButton setTitle:@”Mark As In Progress” forState:UIControlStateHighlighted];

[self.todoStatus setText:@”Complete”];

[self.todo updateStatus:1];

} else {

[self.todoButton setTitle:@”Mark As Complete” forState:UIControlStateNormal];

[self.todoButton setTitle:@”Mark As Complete” forState:UIControlStateHighlighted];

[self.todoStatus setText:@”In Progress”];

[self.todo updateStatus:0];

}

}

– (IBAction) updatePriority:(id)sender {

int priority = [self.todoPriority selectedSegmentIndex];

[self.todo updatePriority🙁2-priority+1)];

}

詳しく見て行きましょう。まず、updateStatus メソッドがあります。これはユーザがstatusを変更するボタンを押した時に呼ばれます。

基本的にtodoの現在のstatusをチェックし、それに従って、UIButtonのテキストが変更します。

todoが未完了な状態で、完了ボタンが押されると、ボタンに表示されている文字列が”完了に設定”から”未完了に設定”にかわります。

最後に、todo の updateStatus を呼び出し、新しい値(1 or 0)を渡します。

次に、updatePriority メソッドを見て行きましょう。

これは単純に、selectedSegmentIndex を呼び出すことによって、UISegmentedControlの値を読んでいます。

次の箇所は少し複雑ですね。

UISegmentedControlの値をこのメソッドに直接渡すことが出来ないからです。

UISegmentedControl は「1,2,3」のように昇順で並んでいますが、priority は(3 = 低, 2 = 中, 1 = 高)と降順にならんでいます。

これは、“2 – priority”の箇所です。

次は、UISegmented のコントロールは、「0」からはじまります。priority は「1」から始まるので、“+1″を追加する必要があります。

それでは、Interface Builder  上の UI Components をこのコードに接続しましょう。

TodoViewController.xib をダブルクリックし、Interface Builder を開きましょう。

コードとUI部品を接続する

まず、先ほど作成したクラスとviewを関連づけます。Interface Builder 上のFile’s Owner オブジェクトをクリックします。次に、Tools > Identity Inspector を開きます。classという項目のところにドロップダウンがあります。このリストから「TodoViewController」を選択します。

792-7

これで、このクラスが関連づけられました。部品を接続していきましょう。viewウインドウをクリックします。どのUI部品も選択せずに、view自身が選択されているのを確認してください。Tools > Connection Inspector を開きます。“New Referencing Outlet”の横の○(円マーク)をドラッグし、File’s Owner の上で、放します。

“view”という文字がポップアップされるので、それをクリックします。

以下の画像を参考にしてください。

792-8以下のようにviewとFile’s Ownerが接続されていればOKです。

この手順を繰り返し、各部品(UITextView, UISegmentedControl, UILabel, UIButton)をFile’s Owner に接続します。先ほどポップアップした”view”ではなく、それぞれ、相当する名称がポップアップされますので、そちらを選択してください。

例えば、UITextView では、File’s owner にドラッグすると、“todoText”という文字列がポップアップされて現れると思います。

updateStatus メソッドをUIButton に接続するために、UIButton を選択した状態で、“Touch up inside” の○をFile’s Owner にドラッグします。すると“updateStatus”というポップアップが出てくると思うので、それを選択します。

792-10

すると以下の画像のようになります。

792-11Interface Builder で行う最後の作業は、UISegmentedControl を接続することです。viewの中からUISegmentedControlを選択し、Connections Inspectorを表示させます。

“Value Changed” メソッドの横の○をドラッグし、File’s Owner の上で放します。すると”updatePriority”というメソッドがポップアップしてきますので、それをクリックします。以下の画像のようになればOKです。

792-12それでは、列が選択された時、このviewを表示させましょう。Interface Builder を終了し、RootViewController.h を開き、以下のコードを追加します。

#import <UIKit/UIKit.h>

#import “TodoViewController.h”

@interface RootViewController : UITableViewController {

TodoViewController *todoView;

}

@property(nonatomic,retain) TodoViewController *todoView;

@end

viewを変えるために、TodoViewController に変数を関連づけます。

RootViewController.m を開き、synthesizeを追加します。

@synthesize todoView;

最新の状態をUITableViewに反映させる

todo アイテム(status または、priority)が変更されるたびに、新しい変更が UITableView に反映される必要があります。

viewWillAppear を以下に編集します。

– (void)viewWillAppear:(BOOL)animated {

[self.tableView reloadData];

[super viewWillAppear:animated];

}

[self.tableView reloadData] の行は、view が表示(非表示)されるたびに、テーブルデータを読み出します。これによって、常にテーブルが最新の情報を反映するようにします。

didSelectRowAtIndex メソッドを以下のように編集します。

– (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

todoAppDelegate *appDelegate = (todoAppDelegate *)[[UIApplication sharedApplication] delegate];

Todo *todo = (Todo *)[appDelegate.todos objectAtIndex:indexPath.row];

if(self.todoView == nil) {

TodoViewController *viewController = [[TodoViewController alloc]

initWithNibName:@”TodoViewController” bundle:[NSBundle mainBundle]];

self.todoView = viewController;

[viewController release];

}

[self.navigationController pushViewController:self.todoView animated:YES];

self.todoView.todo = todo;

self.todoView.title = todo.text;

[self.todoView.todoText setText:todo.text];

NSInteger priority = todo.priority1;

if(priority > 2 || priority < 0) {

priority = 1;

}

priority = 2 – priority;

[self.todoView.todoPriority setSelectedSegmentIndex:priority];

if(todo.status == 1) {

[self.todoView.todoButton setTitle:@”Mark As In Progress” forState:UIControlStateNormal];

[self.todoView.todoButton setTitle:@”Mark As In Progress” forState:UIControlStateHighlighted];

[self.todoView.todoStatus setText:@”Complete”];

} else {

[self.todoView.todoButton setTitle:@”Mark As Complete” forState:UIControlStateNormal];

[self.todoView.todoButton setTitle:@”Mark As Complete” forState:UIControlStateHighlighted];

[self.todoView.todoStatus setText:@”In Progress”];

}

}

おなじみのコードがたくさん並んだ、とても長いメソッドですね。

まず、appDelegate へのハンドルと選択されたtodoオブジェクトを取得します。

次に、画面を推移させるために、Interface Builder で作成した todoView を viewController に配置します。

その後、view のいくつかのプロパティを設定します。

title は、todoのタイトル文字列を設定します。あまりに文字列が長い場合は、削られます。さらに、UITextView にも文字列がセットされます。

次に、UISegmentedView のインデックスに変換します。既に、この必要性については、説明しました。

setSelectedSegmentIndex メソッドを使って、UISegmentedControl のインデックスがセットされます。

最後に、ボタンの文字列とtodoのstatusがセットされます。

このチュートリアルの最後に、アプリケーションが閉じられる時、変更を保存するように設定します。

todoAppDelegate.m を開き、applicationWillTerminate メソッドを、以下のように追加してください。

– (void)applicationWillTerminate:(UIApplication *)application {

// Save data if appropriate

[todos makeObjectsPerformSelector:@selector(dehydrate)];

}

makeObjectsPerformSelector メソッドは、”NSArrayのメソッドに組み込まれています。

これは、配列内のすべてのオブジェクトに対し、渡したメソッドを繰り返します。

全てのtodoに対して、todo[x].dehydrate メソッドを呼び出すようにループします。

もし、todoが”dirty”なら変更されたということなので、データベースに保存します。そうでなければ、dehydrateメソッドは何もしません。

注意すべきは、アプリケーションを起動中にシミュレータを終了すると、このapplicationWillTerminateメソッドは呼ばれないということです。

これが正常に呼ばれていること(todoデータの変更が保存されたこと)を確認するためには、todoの内容を変更してから、シミュレータ上のホームボタンを押します。

これで今回のチュートリアルは終了ですので、ビルド&実行してみてください。

以下のような画面が表示されるはずです。

792-13

792-14

投稿日:
カテゴリー: 過去Blog

1件のコメント

コメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です