アーカイブ

Archive for 2009年7月

UITextFieldの入力文字数を制限する

ユーザに文字入力して貰う時に、最大文字数で制限したい時があります。
残念ながらUITextFieldには、maxTextLengthみたいな名前のプロパティはありません。

このような場合、UITextFieldDelegateのtextField:shouldChangeCharactersInRange:replacementString:を使う事ができます。
文字入力やバックスペースが入った時、カットやペーストを行った時に、rangeに置き換え範囲、stringに置き換え文字列が渡ってきます。
現在のテキストに対して置換を実行してやれば入力確定後の文字列が得られるので、文字数をチェックして受け入れるか拒否します。

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
    NSMutableString *text = [[textField.text mutableCopy] autorelease];
    [text replaceCharactersInRange:range withString:string];
    return [text length] < <最大文字数>
}

文字数のチェックにlengthを使っていますが、これはサロゲートペアを処理しないので、見た目の文字数でチェックしたい場合はグリフを数えてください。

問題点

実は日本語環境では問題があります。予測変換で単語を選択入力すると、textField:shouldChangeCharactersInRange:replacementString:が発生しません。キーボードから文字を入れた時のみ発生するようです。
そのため、パスワード入力とか日本語キーボードを禁止している時しか当てになりません。
iPhone OSのバグのような気もしますね。
現状、文字の切り落としを併用して対処しています。

広告
カテゴリー:開発 タグ: ,

SQLiteのラッパーPlausible Database

iPhoneでは、素のファイルよりもSQLiteを使ってデータを保存する事が推奨されています。
ですが、Objective-CのAPIは用意されておらず、素のSQLiteのAPIを使う事になります。
SQLiteのAPIは難解ではありませんが、普段のObjective-Cのプログラミングと比較すれば、手間な事には間違い有りません。
少なくても、

  • データ型の変換
  • 文字エンコーディングの変換
  • 例外・エラーの変換
  • C言語水準のリソース管理

につきあう必要があります。
MemoPlusの開発は素のSQLiteを使いましたが、冗長なコードを沢山書いた気がします。

そこでSQLiteのラッパーライブラリを探したところ、Plausible Databaseというライブラリを見つけました。
現在までに二つの製品の開発で使用していますが、特に不具合も無くコードもすっきりしました。
次のような理由で、SQLiteのライブラリを探している方に、Plausible Databaseをオススメします。

  • インターフェースが突飛では無い
    JavaのJDBCあたりの経験があれば、直感的に使用方法が解ります。
  • オブジェクトマッパーでは無く、SQLラッパー
    余計な事をしません。
  • エラー処理の考え方がCocoa的に適切
    バグはNSExceptionで、実行時に起こりうるエラーは戻り値とNSErrorで検出できます。
  • メンテされている
    放置されているコードは使いたく無いものです。

ビルド方法

パッケージをダウンロードしたら、Classesに含まれるライブラリのソースコードを自分のプロジェクトに追加します。ただし〜Tests.mという名前のファイルは、ライブラリ自体のテストコードなので不要です。
次に、Frameworksにlibsqlite3.dylibを追加して、SQLiteの本体をリンクします。
最後にビルド設定に下記を追加します。

OTHER_CFLAGS -DPL_DB_PRIVATE=1

使用方法

データベースのオープンとクローズは、こんな感じです。

PLSqliteDatabase *database = [[PLSqliteDatabase alloc] initWithPath:<ファイルパス>];
NSError *error = nil;
if (![database openAndReturnError:&error]) {
    <エラー表示>
}

…

[database close];
[database release];

問い合わせはこんな感じです。

NSError *error = nil;
NSObject<PLResultSet> *resultSet = [database executeQueryAndReturnError:&error statement:@"select id, name, date from Datas"];
if (!resultSet) {
    <エラー表示>
}
while ([resultSet next]) {
    NSInteger id_ = [resultSet intForColumn:@"id"];
    NSString *name = [resultSet stringForColumn:@"name"];
    NSDate *date = [resultSet dateForColumn:@"date"];
    <データを使う>
}
[resultSet close];

更新はこんな感じです。

NSInteger id_ = <データを用意>
NSString *name = <データを用意>
NSDate *date = <データを用意>
NSError *error = nil;
if (![database executeUpdateAndReturnError:&error statement:@"insert into Datas(id, name, date) values(?, ?, ?)", id_, name, date]) {
    <エラー表示>
}

iPhoneではあまり使う機会は無いと思いますが、プリペアドステートメントやトランザクションもサポートされています。

カテゴリー:開発 タグ:

UITableViewに戻った時に確実に行のハイライト解除アニメーションを表示する

2009年7月26日 2件のコメント

UITableViewの行はタッチした時にハイライトしますが、ハイライトの解除はそこからビューが遷移した先から戻った時に、遅れて行います。
これはviewWillAppear:でindexPathForSelectedRowから取得したハイライトされている行を、deselectRowAtIndexPath:animated:で解除するだけの事なんですが、上手くいく場合と上手くいかない場合があります。
上手くいかない場合は、テーブルをリロードしてしまっている時です。
この場合選択状態が失われてしまうので、最初からハイライトが消えていて解除アニメーションがでません。
詳細表示側で行が増減してしまった場合や、内容を更新してしまった場合がこの状況に該当します。

そこで対処ですが、次のように処理する事で可能です。

  1. テーブルをリロードする。
  2. テーブルのデータソースを検索して、直前に詳細ビューで表示していた行を特定する。
  3. 特定した行を、逆にselectRowAtIndexPath:animated:scrollPosition:でハイライトする。

この処理は直感に反しますが、上手く動きます。

テーブル全体のリロードを避けて、行の増減を実行したり、更新された行だけを更新する事でも回避が可能かもしれませんが、手間が相当になると思います。
また、メモリワーニングでビューがリロードされている場合は、テーブル全体のリロードが回避不能です。

コードは次のようになると思います。

- (void)viewWillAppear:(BOOL)animated {
    [self.tableView reloadData];

    if (detailViewController.data) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[datas indexOfObject:detailViewController.data] inSection:0];
        [self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
    }
    [super viewWillAppear:animated];
}
カテゴリー:開発 タグ: ,

UISearchDisplayControllerのサーチバーを最初は隠しておきたい

UISearchDisplayControllerを使うと、Appleの標準アプリのような検索画面が簡単に作れていい感じなんですが、テーブルビューを表示した瞬間に、最初からサーチバーが見えている状態になります。
一方Appleの標準アプリの場合は、起動直後はサーチバーが見あたらず、テーブルビューを下にスクロールさせると、ナビゲーションバーの下から出てきます。
これは単純に、オフセットを弄れば対処できるようです。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.contentOffset = CGPointMake(0.0, self.searchDisplayController.searchBar.bounds.size.height);
}

オフセットを、サーチバーの高さ分動かします。

カテゴリー:開発 タグ: ,

UIPickerViewのカスタム行をUITableViewCellで実装する

2009年7月24日 3件のコメント

UIPickerViewに右端にアイコンの付いた行を表示したり、左端にチェックマークの付いた行を表示したい場合、1行分のUIViewを組み立てて、pickerView:viewForRow:forComponent:reusingView:から返す必要があります。
さほど面倒ではありませんが、見栄えをキチンと揃えるにはそれなりの手間がかかります。
しかし良く考えると、この要求仕様はUITableViewCellに非常に近い事が判ります。
試しに実装してみましたが、非常に良好です。

- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
    UITableViewCell *cell = (UITableViewCell *)view;
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
        CGSize size = [pickerView rowSizeForComponent:component];
        cell.bounds = CGRectMake(0.0, 0.0, size.width, size.height);
        cell.backgroundColor = [UIColor clearColor];
    }
    else {
        [cell prepareForReuse];
    }
    cell.imageView.image = <アイコンとか>
    cell.textLabel.text = <文字列とか>
    cell.accessoryType = <マークとか>
    return cell;
}

ポイントは、prepareForReuseの呼び出しです。
UITableViewはこれを勝手に呼び出しますが、UIPickerViewの場合は自分で呼び出さないと、セルが再利用された時に各サブビューの位置取りが狂います。
同様に、セルサイズも明示的に設定する必要があります。
また、セルの背景を透明にしないとUIPickerViewの背景の球面感に溶け込みません。

カテゴリー:開発 タグ: ,

UITableViewControllerは何をやっているか

諸々の事情で、UITableViewを制御する時にUITableViewControllerを使えない場合があります。たとえば、トップビューがUITableViewでは無くUIViewで、サブビューにUITableViewが配置されている場合などです。iPhone OS 2.x時代であれば、UITableViewの下にUIToolbarを配置するケースが該当します。

このような場合、UIViewControllerのサブクラスにUITableViewController相当の機能を実装する必要があります。概ね、下記を実装すれば遜色ない動作をするようです。

  • viewWillAppear:をオーバーライドして、UITableViewのreloadDataを呼び出し、データをリロードする。ただし初回のみ。メモリワーニングでビューがアンロードされた場合は再度リロードが必要になるので、viewDidLoadでフラグを立てると良い。
  • viewDidAppear:をオーバーライドして、UITableViewのflashScrollIndicatorsを呼び出し、スクロールインジケータをフラッシュさせる。
  • setEditing:animated:をオーバーライドして、UITableViewのsetEditing:animated:を呼び出し、編集状態を更新する。

コードにするとこんな感じ。

- (void)viewDidLoad {
    [super viewDidLoad];
    reloaded = NO;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    if (!reloaded) {
        [tableView reloadData];
        reloaded = YES;
    }
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [tableView flashScrollIndicators];
}

- (void)setEditing:(BOOL)aEditing animated:(BOOL)animated {
    [super setEditing:aEditing animated:animated];
    [tableView setEditing:aEditing animated:animated];
}
カテゴリー:開発 タグ: ,