iPhoneデータベース入門(SQLite)終わりの始まり FMDBを使った「検索日記」のソースコード解説
前回:iPhoneデータベース入門(SQLite)Part.3 「検索日記」を実装するにあたっての考え方
ソースコードの解説をします。
「検索日記」のアプリダウンロードはこちらから
http://itunes.apple.com/jp/app/jian-suo-ri-ji/id517392095?mt=8
「検索日記」のソースコードはGitHubからどうぞ
https://github.com/ics-hiro/Kensaku_Nikki_ARC
Viewの背景をグループタイプにする
まずはViewの背景を変えます。
ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; //←追加 }
これでグループタイプの背景になります。
検索窓のTextFieldを設置する
TextFieldに自分が検索した語句をいれて、Searchします。
TextFieldを設置するコードを書いてみましょう。
ViewController.m
@implementation ViewController { @private UITextField *tf; }
- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; //先ほど追加しましたね tf = [[UITextField alloc] initWithFrame:CGRectMake(35, 65, 250, 30)] ; //テキストフィールドの配置、サイズです tf.borderStyle = UITextBorderStyleRoundedRect; //テキストフィールドのスタイルを決定します tf.returnKeyType = UIReturnKeySearch; //「検索」で最後決定するため tf.placeholder = @"検索"; //はじめからテキストフィールドの中に薄い文字で「検索」と書かれてあるようにする tf.clearButtonMode = UITextFieldViewModeAlways; // 編集が終了した後、フォーカスが外れた時にhogeメソッドを呼びます [tf addTarget:self action:@selector(hoge:) forControlEvents:UIControlEventEditingDidEndOnExit]; [self.view addSubview:tf]; //Viewにテキストフィールドをのせる }
さて、ここまでは普通にテキストフィールドを置いてみただけです。
わからない処理があれば検索してくださいね。
とりあえずどんどん先に進んでみます。
touchesBeganメソッドを追加します。viewDidLoadの下にでも書いておいてください。
画面がタップされたときに、入力を解除すると言えばいいのかな。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [tf resignFirstResponder]; }
「検索」が押されたら、hogeメソッドが呼び出される。
hogeメソッドの中身を一応すべて記述すると、
-(void)hoge:(UITextField*)textfield{ NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; NSString *strWords = [[NSString alloc] initWithFormat:@"%@",tf.text]; NSString* sqll = @"CREATE TABLE IF NOT EXISTS words (id INTEGER PRIMARY KEY AUTOINCREMENT,words TEXT,date TEXT)"; NSDate *today = [NSDate date]; NSLocale *locale_ja = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"] ; NSDateFormatter *formatter = [[NSDateFormatter alloc]init]; [formatter setDateStyle:NSDateFormatterMediumStyle]; [formatter setLocale:locale_ja]; NSString *strTime = [[NSString alloc] initWithFormat:@"%@",[formatter stringFromDate:today]]; [db open]; [db executeUpdate:sqll]; [db beginTransaction]; [db executeUpdate:@"INSERT INTO words (words,date) VALUES (?,?)",strWords,strTime]; [db commit]; [db close]; WebViewController *dialog = [[WebViewController alloc] init]; dialog.modalTransitionStyle = UIModalTransitionStyleCoverVertical; dialog.eleData = [NSString stringWithString:[Util urlencode:tf.text]]; [self presentModalViewController:dialog animated:YES]; }
1部分ずつ解説していきます。
SQLiteファイルを生成する(参照する)
NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]];
やっとFMDBが出てきましたね(笑)
databaseWithPath メソッドにファイルのパスを指定します。
ファイルがすでに存在しているなら参照(検索を一回すれば、searchLog.sqliteがつくられます。それ以後はこのSQLiteファイルでデータの出し入れがなされます)、なければ新規作成(最初にアプリを立ち上げたとき、SQLiteファイルはありません。hogeメソッドが初めて呼ばれたときに生成されます)し、そこに接続されたFMDatabase のインスタンスを返します。
TextFieldに入力された文字列をNSStringのstrWordsでもつ
NSString *strWords = [[NSString alloc] initWithFormat:@"%@",tf.text];
tf.textで扱ってもいいんですが、わかりやすい名前でもっとおこうと思ったしだいです。
日本時間を取得しよう
NSDate *today = [NSDate date]; NSLocale *locale_ja = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"] ; NSDateFormatter *formatter = [[NSDateFormatter alloc]init]; [formatter setDateStyle:NSDateFormatterMediumStyle]; [formatter setLocale:locale_ja]; NSString *strTime = [[NSString alloc] initWithFormat:@"%@",[formatter stringFromDate:today]];
日時関連はNSDateですね。
最後はstrTimeという文字列でもってます。データベースにこれを差し込みますよー。
テーブルをつくるSQL文を考えよう
「 @"CREATE TABLE IF NOT EXISTS words (id INTEGER PRIMARY KEY AUTOINCREMENT,words TEXT,date TEXT)"」という部分です。
SQLiteで「IF NOT EXISTS」が使えるので使う。
もし「words」というテーブルがない場合には作りますよ、というSQL文。
テーブル名とフィールド名がかぶってるのが少しまぎらわしいかもしれませんが、あえて作ったまんまにしてます。
構造としては
- id(一意の整数)
- words(検索ワードを格納)
- date(検索した日時を格納)
データベースを生成する場合、構造を考えておかなければなりません。
より良いデータベース構造を考えるほうが、プログラムを組むより難しい気はします。。。
今回の「検索日記」は至って単純な構造です。
データベースを開いて、SQL文を実行しよう
[db open]; //データベースを開きます [db executeUpdate:sqll]; // すでに上で解説済みです。 [db beginTransaction]; //トランザクション処理(はっきり言っていらないですが [db executeUpdate:@"INSERT INTO words (words,date) VALUES (?,?)",strWords,strTime]; //「?」というパラメータにstrWords,strTimeを渡します。 [db commit]; //トランザクション処理終了(はっきり言っていらないですが [db close];
コメントに書いた通りですね。
トランザクション処理はいりません、自分で書いときながら笑
FMDBの実行で少し注意しておいてほしいのは、「executeUpdate」と「executeQuery」は違うというところです。Queryはこの解説でまだでてきていません。
画面遷移、異なるView間で値(文字列)の受け渡し
WebViewController *dialog = [[WebViewController alloc] init]; dialog.modalTransitionStyle = UIModalTransitionStyleCoverVertical; dialog.encoded_word = [NSString stringWithString:[Util urlencode:strWords]]; //←ここが重要 [self presentModalViewController:dialog animated:YES];
WebViewController.hでアクセサを通してます。
#import <UIKit/UIKit.h> @interface WebViewController : UIViewController @property (strong, nonatomic) IBOutlet UIWebView *wv; @property (nonatomic,retain) NSString *encoded_word; //←これのこと。これで検索ワードの受け渡しをしている - (IBAction)toHome:(id)sender; @end
エンコード大事です。
[Util urlencode:strWords] //strWordsをエンコードして、encoded_word
これでhogeメソッドについての解説は終わりとします。
UIViewControllerの上にUITableViewをのせる
UIViewControllerの上にUITableViewをのっけています。
大事なのは、
ViewController.hで、
mファイルのviewDidLoadメソッドの中でテーブルビューに関連する部分は、
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 100, 320, 365) style:UITableViewStyleGrouped]; _tableView.dataSource = self; _tableView.delegate = self; [self.view addSubview:_tableView];
データベースから日づけの部分を配列で取得しておく
mファイルのviewDidLoadメソッドの中に、こういう部分がありますね。
dbのopenと、dbのcloseを忘れずに。
dateSection = [[NSMutableArray alloc] init ]; [db open]; FMResultSet* fResult = [db executeQuery:@"select distinct date from words order by id desc"]; while ([fResult next]) { iData = [fResult stringForColumn:@"date"]; [dateSection addObject:iData]; } [db close];
セクションタイトルのところでdateSectionという可変配列を使用します。
FMResultSet* fResult = [db executeQuery:@"select distinct date from words order by id desc"];
「executeQuery」です。SQL文の結果を取得。
while ([fResult next]) { iData = [fResult stringForColumn:@"date"]; [dateSection addObject:iData]; }
fResultをwhile構文で展開、nextは要素がなくなった場合にnilを返し、ループを抜ける。
iDataはdateSectionに要素をいれるためのもの。dateSectionは空の可変配列(NSMutableArray)
UITableViewに必要なメソッドを記述する
TableViewに関わる部分を全部抜き出すと、
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return dateSection.count; } //--------------セクションタイトル---------------- -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{ for (int i = 0; i < [dateSection count]; i++) { if (section == i) { NSString *str = [[NSString alloc] initWithFormat:@"%@ 検索履歴",[dateSection objectAtIndex:i]]; return str; } } return 0; } //-------------------------------------------- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; wArray = [[NSMutableArray alloc] init ]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (section == i) { FMResultSet* fResult = [db executeQuery:@"select * from words where date = ?",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } return [wArray count]; } } return 0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] ; } wArray = [[NSMutableArray alloc] init ]; NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (indexPath.section == i) { FMResultSet *fResult = [db executeQuery:@"select * from words where date = ? order by id desc",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } cell.textLabel.text = [wArray objectAtIndex:indexPath.row]; } } [db close]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { wArray = [[NSMutableArray alloc] init ]; NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (indexPath.section == i) { FMResultSet *fResult = [db executeQuery:@"select * from words where date = ? order by id desc",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } for (int m = 0; m < [wArray count]; m++) { if (indexPath.row == m) { WebViewController *dialog = [[WebViewController alloc] init]; dialog.modalTransitionStyle = UIModalTransitionStyleCoverVertical; dialog.encoded_word = [NSString stringWithString:[Util urlencode:[wArray objectAtIndex:m]]]; [self presentModalViewController:dialog animated:YES]; } } } } [db close]; }
少し長いように見えますが、ひとつひとつ見ていきましょうね。
TableViewのセクションの数を定める
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return dateSection.count; }
可変配列dateSectionの中身の数をカウントして、その数を返しています。
同じ日づけの場合、重複しちゃんじゃないの?という疑問が生まれるかもしれません。
しかしこれはすでにviewDidLoadのなかで、
「select distinct date from words order by id desc」で重複をのぞくように命令しています。
セクションのタイトルを定める
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{ for (int i = 0; i < [dateSection count]; i++) { if (section == i) { NSString *str = [[NSString alloc] initWithFormat:@"%@ 検索履歴",[dateSection objectAtIndex:i]]; return str; } } return 0; }
日づけごとにセクションがあります。そして、日付の名前がセクションのタイトルに入ってます。
dateSectionの要素の数を数え、その数だけforループさせる。
TableViewのセルの数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; wArray = [[NSMutableArray alloc] init ]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (section == i) { FMResultSet* fResult = [db executeQuery:@"select * from words where date = ?",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } return [wArray count]; } } [db close]; return 0; }
まずはデータベースを開く。
セクション(日づけ)によって検索したワードの数(きのうは10個検索して、今日は5個かもしれない)がちがいますよね。
まずはセクションの数をカウントする。これがdateSection countですね。
これをforループで回して、sectionがもし0のとき、1のとき、2のときという風にしていく。
日づけによってセクションをわけているので、ある日づけのときの検索ワードを取得してくるんです。
ちょっと説明がわかりにくいですかね。。。
検索ワードがwArrayという空の可変配列に格納され、検索ワードの数(wArrayの要素数)だけ返します
TableViewのセルの表示
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] ; } wArray = [[NSMutableArray alloc] init ]; NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (indexPath.section == i) { FMResultSet *fResult = [db executeQuery:@"select * from words where date = ? order by id desc",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } cell.textLabel.text = [wArray objectAtIndex:indexPath.row]; } } [db close]; return cell; }
データベースを開く、閉じる。
セルの数の時と同様に、forループでdateSectionの数だけ回し、取得してきた検索ワードをwArrayに格納して、indexPath.rowで順番にセクションごとに表示させます。
TableViewのセルをタップしたときの処理
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { wArray = [[NSMutableArray alloc] init ]; NSArray* paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES ); NSString* dir = [paths objectAtIndex:0]; FMDatabase* db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"searchLog.sqlite"]]; [db open]; for (int i = 0; i < [dateSection count]; i++) { if (indexPath.section == i) { FMResultSet *fResult = [db executeQuery:@"select * from words where date = ? order by id desc",[dateSection objectAtIndex:i]]; while ([fResult next]) { iData = [fResult stringForColumn:@"words"]; [wArray addObject:iData]; } for (int m = 0; m < [wArray count]; m++) { if (indexPath.row == m) { WebViewController *dialog = [[WebViewController alloc] init]; dialog.modalTransitionStyle = UIModalTransitionStyleCoverVertical; dialog.encoded_word = [NSString stringWithString:[Util urlencode:[wArray objectAtIndex:m]]]; [self presentModalViewController:dialog animated:YES]; } } } } [db close]; }
データベースを開く、閉じる。
アプリの動きとしては、検索ワードを押したとき、Webビューが開いて、なおかつその検索ワードがGoogle検索された状態になっているということ。
まずはセクション(日づけ)を大きな枠とする。つまりif(indexPath.section = i)という枠をまずは作り、そこで検索ワードを取得してくる。その検索ワードをエンコードして、Webビューに渡す、という処理をしている。
さて、次はWebViewControllerについて。
Webビューの実装は
絶対に挫折しないiPhoneアプリ開発入門Part.14 ~UIWebView、webページを表示する~
という記事を昔書いたので参照願います。
アクセサを通して、検索ワードを受け渡しするところ、大事です
Google検索のクエリに文字列を埋め込む
viewDidLoadの中は、こう書いてる。
NSString *urlStr = [[NSString alloc] initWithFormat:@"http://www.google.co.jp/search?q=%@",encoded_word]; NSURL *url = [NSURL URLWithString:urlStr]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [self.wv loadRequest:request];
Google検索は、
「http://www.google.co.jp/search?q=%@」
の部分です。
リンクになってますが気にしないでください(笑)
%@にエンコーディングされた日本語文字列が入ります。
search?q=の後に、埋め込むというわけです。
「検索日記」の解説は、以上とさせて頂きます。
あとは、
をお伝えしておきます。
生成されたSQLiteのデータベースがちゃんとできてるのか確認したいですよね。
見てみましょう。
シミュレータの中で生成されたSQLiteファイルを参照する
/Users/ユーザー名/Library/Application Support/iPhone Simulator/5.1/Applications/アプリID/Documents
5.1の部分は、どのシミュレータを使っているかで変わります。
ぼくの場合は、5.0と5.1が併存しています。
実機の中で生成されたSQLiteファイルを参照する
検索が終わったら、「ホーム」ボタンを押して、検索した言葉が保存されていることを確認。
ここからです。
実機をつないだままで、Organaizerを開いてください。
以下の手順でデータをダウンロードしてください。
ダウンロードを押したら、デスクトップに保存してみましょう。
これを右クリックして、「パッケージの内容を表示」を選択します。
/AppData/Documents/searchLog.sqliteを発見するはず!
searchLog.sqliteをLitaで開いてみると、無事データが入っていることを確認!
以上、iPhoneデータベース入門、「終わりの始まり」です。
iPhoneデータベース入門記事一覧
iPhoneデータベース入門(SQLite)Part.1 FMDBでSQLiteを扱う準備をしよう
iPhoneデータベース入門(SQLite)Part.2 Xcode4.3.2でFMDBを使ったアプリをつくる
iPhoneデータベース入門(SQLite)Part.3 「検索日記」を実装するにあたっての考え方
iPhoneデータベース入門(SQLite)終わりの始まり FMDBを使った「検索日記」のソースコード解説
======================
当ブログ管理人のツイッターはこちら
◎フォローしてくださると泣いて喜びます!ツイッター上で当ブログの質問などにもできるだけお答えしますし、役に立つiPhoneアプリ開発情報もつぶやきます。個人的なご依頼(たとえば、プログラミングの家庭教師、Skypeレッスンをしてくれないか、iPhoneアプリ開発の勉強会ってやってるの、とかですね)でもかまいません。
■スカイプレッスンについて
開発をしているとその都度ぶつかる問題があると思いますが、検索で調べてもなかなかわからない、あるいは調べても非常に時間がかかる場合があると思います。開発者のみなさんもお忙しいと思いますので、私のレッスンを受けながら開発を進めていただけると、大幅にお時間を短縮できます。加えて、iPhoneアプリ開発の基礎、応用が身につき、今後の開発もスムーズに進められます。
お問い合わせはツイッター、またはhiyoshimarukoアットマークgmail.comまでお願いします。(アットマークを@にしてください)お待ちしております!