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ファイルを参照する方法

をお伝えしておきます。

生成された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.sqliteLitaで開いてみると、無事データが入っていることを確認!



以上、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までお願いします。(アットマークを@にしてください)お待ちしております!