暗号化zipを利用したストレージ管理

Androidアプリはapkをapktoolなどで展開できるのでasset, res内のデータは比較的簡単に覗き見ることができます。今回は配布データの隠蔽化の目的でパスワード付きzipを試してみました。
Android用のパスワード付きzipの解凍モジュールは下記のサイトのモジュールを使わさせてもらいました。

Androidでパスワード付きzipを生成する
Android用Zipユーティリティクラス


Assetからファイル読込はInputStreamでしか受け取れないので、一旦assetsから内部ストレージに暗号化zipをそのままコピーして、必要な時に都度Entryパス指定で解凍してメモリ展開するようにします。

Assetからzipファイルを読込み内部ストレージにコピー

File outDir = context.getDir("data", Context.MODE_PRIVATE);
File outFile = new File(dir, "data.zip");

AssetManager assetManager = context.getResources().getAssets();
BufferedInputStream bis = new BufferedInputStream(assetManager.open("data.zip"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile));

byte[] buffer = new byte[1024];
int len = 0;
while ( (len = is.read(buffer, 0, buffer.length)) > 0) {
     bos.write(buffer, 0, len);
}
bos.flush();
bos.close();
bis.close();


Context#getDir("data",Context.MODE_PRIVATE) で /data/data/{package_name}/app_data が作成されます。
AssetManager#open(ファイル名)でInputStreamが取得できるので、FileOutputStreamでbyte書込を行いapp_data/data.zipにバイナリコピーします。

指定のEntryパスで読込

Bitmap bitmap = null;

ZipFile zipFile = new ZipFile(dataFile, "UTF-8");
zipFile.setPassword(password.getBytes("UTF-8"));
zipFile.setCheckCrc(true);
ZipEntry entry = zipFile.getEntry("data/binary/photos/frog.jpg");

BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
bitmap = BitmapFactory.decodeStream(bis);
bis.close();
zipFile.close();

内部ストレージのdata.zipをZipFileでインスタンスを作成しパスワードとCRCチェックの有無を設定します。
ZipFile#getEntry() で 対象のZipEntryを取得し、ZipFile#getInputStream(entry) で InputStreamを取得します。

尚、ZipFile#setCheckCrc(true) は ファイルが破損している場合はZipFileのInputStreamのread処理でZipCrcExceptionが発生します。


サンプルソース

サンプルソースgithubにアップしました。

https://github.com/hmori/CryptZipTest

  1. 起動時にasset/data.zipを /data/data/jp.hmori.cryptziptest/app_data/data.zip にコピー
  2. data.zip内に含まれるEntryPathをListViewに表示
  3. ListViewのEntryPathの行クリックで対象のEntryをメモリ展開し表示

所感

軽微なデータ量であれば問題ないのですが、大きめの画像ファイル等になると展開にパフォーマンスが劣化します。サンプルソースでは1Entryの展開にかかる時間を計測していますので、この方法を利用する場合はこちらで確認してから利用した方がいいと思います。
大きめのデータを扱う場合は、素直にSQLCipherを利用するか自前でAES暗号化を行う方がよいかもしれません。

UITableViewをStoryboard上でStatic Cellsみたいに使ってみる

Static Cellsはコード量が極端に減り非常に有効ですが、UITableViewController上でしか使えない制限があるためカスタマイズしにくい欠点があります。UIViewControllerにUITableViewを載せてできるだけStoryboard上で定義する方法を紹介しました。


UIViewController継承のControllerにUITableViewのdelagate, dataSourceを定義する


TableViewのContentプロパティは通常通りDynamic Propertiesにする
Prototype cellsはStoryboard上で扱うCell数


TableViewCellのidentifierを"cell_0_0", "cell_0_1"のように"cell_{sectionNo}_{rowNo}"にする


UITableViewを載せたControllerのUITableViewDataSourceをオーバーライドする

#pragma mark UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    //prototype cellsで設定した数
    return 4;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView dequeueReusableCellWithIdentifier:
            [NSString stringWithFormat:@"cell_%d_%d", indexPath.section, indexPath.row]
            ];
}


あくまで通常のUITableViewの機構に頼っただけのものなので、高さの変更など必要があればそれぞれのDelegateメソッドをオーバーライドする必要があります。

#pragma mark UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
     [NSString stringWithFormat:@"cell_%d_%d", indexPath.section, indexPath.row]
     ];
    return cell.frame.size.height;
}

Dynamic propertiesは通常のUITableView通りに、Section, Rowをコードで制御する仕組みであるため、Storyboardのパレット上にTableSectionを扱うパーツが存在しません。残念ながらヘッダー、フッターの制御はコードで定義するか、Cellのパーツを拡張してUser Defined Runtime Attributesを利用するなどの工夫が必要になります。そこまでStoryboard上の編集にこだわるメリットは薄いとは思いますが。今後、Static Cellsが進化して使いやすくなることを願います。

サンプルプロジェクト

https://github.com/hmori/StoryboardTest


参考サイト

UITableViewのStatic Cellsが使えなかったけどstoryboardでできる限りそれっぽくやってみた



Storyboardは個人的に今後の進化に非常に期待がもてる内容だと思います。iOS開発はデザイナ・プログラマなどと複数人での分業が難しいと感じていましたが可能性があるように見えますね。色々と工夫すればデータ部、コントロール部、デザイン部が効果的に分離できるか、これからも調べていきたいと思います。

Storyboardについて発表してきました

SWWDC 仙台iOS開発者向け勉強会で Storyboard について発表してきました。
今までInterfaceBuilderすら毛嫌いして使ってこなかったのですが、他の開発者の方から便利になったと聞いて色々と試してみました。現段階ではデメリットはあるものの、メリットの部分が大きいので利用する価値は高いです。まだまだ発展途上のツールに感じますが、今後こうなるだろうなという方向性も見えてくるように思います。

  • Storyboardの特徴 (IBとの違い)
    • トランジションをUIで定義できる
    • UIで設定できるパーツのプロパティが増えた (User Defined Runtime Attributes)
    • TableView Static Cellsの機能が増えた
  • Storyboardを使うメリット
    • コード量が減る
    • 第3者がアプリ全体を把握しやすくなる
    • 画面間の設計が疎結合になる
    • 簡単なTableViewであればUI操作のみで作成可能
    • デザイナとプログラマの作業領域が分離しやすくなる
  • Storyboardの痒いところ
    • モーダルコントローラのdismiss処理はコーディングが必要
    • Static Cellsが利用できるのはUITableViewControllerの継承が必須
    • User Defined Runtime Attributesで設定できる型が少ない

一から作成することを目的としたので、ほとんどライブコーディングで行いました。
EmptyApplicationから作成して、簡単なトランジション、遷移間パラメータの受け渡し、CustomSegue、NavigationController、TabController、User Defined Runtime Attributesなどを使って試しています。

資料と作成したサンプルプロジェクト

サンプルプロジェクト

https://github.com/hmori/StoryboardTest



今回、開場はBeehive+さんのお店だったのですが設備も充実してて勉強会にも十分使えそうな雰囲気で良かったと思います。

緊急時に10秒でマルチ送信できる無料アプリ「生存連絡」をリリースしました

東日本大震災から1年が経ちました。あらためてお亡くなりになられた方々のご冥福をお祈り申し上げます。

当日、私は仙台市で務めていた会社のビル8階で地震に見舞われたのですが、今までに体験したことのないような状況でおそらく一生忘れることはないと思います。2分近い揺れの中で机の下に潜り死を意識した瞬間もあるくらいでしたので、1階に避難する際にもかなり動揺していたことを覚えています。自分の安全を確保してまず行うことは家族の安否連絡なのですが当然のように電話は通じませんでした。ネット回線はまだ可能な状態でしたのでとりあえずはTwitterFacebookに自分が無事であるメッセージを残したのですが、動揺と地面の揺れで上手く操作が行えなかったことを記憶しています。

最近また地震がちょくちょくあり全体的に防災意識が高まっていると思いますので、今回「緊急異常事態時にすぐさまにメッセージを送信できるように」というiPhoneアプリを無料でリリースしました。事前に送信先・連絡先を設定しておき、緊急時にメッセージを送信するというアプリなのですが、慌てていても10秒以内にメール、SMSメッセージ、TwitterFacebookに同時送信することができます。


生存連絡


インストール (無料)





大災害の時の安否連絡は電話よりもインターネットを利用した伝達手段が確実ですが、落ち着いてメッセージを入力したりするのは困難です。東日本大震災の発生直後では「連絡先が直ぐに出てこない」「気が動転して上手く操作できない」などの声が聞かれました。このアプリでは送信先を事前に登録してメッセージを選択するだけの簡単操作でわずか10秒で生存連絡などが送信できます。
災害を事前に備えましょう。

=主な機能=
・文章選択によるメッセージ作成機能
・メッセージのマルチポスト送信機能
・現在地発信機能

=送信先媒体=
・Email
・ショートメール
Twitter
Facebook

インストール後は最初に送信先の設定を行なってください。

スクリーンショット





実際に使われるような状況は起きてほしくないと心から願ってるのですが、このアプリをインストールして設定することで安心が得られるのなら幸いです。

Blocksの活用法について

iphone_dev_thk 仙台iOS開発者勉強会! 2012/3/3 にて「Blocksの活用法」について発表してきました。内容はBlocksの基本的な部分の説明と下記の3つのソース解説です。

勉強会ではBlocksの仕様、挙動、ハマりどころを踏まえ、拡張のテクニック等の突っ込んだコアな内容までやったので時間的にも多少厳しかったかもしれません。まあ便利になりそうな雰囲気だけでも掴んでもらえればと思います。

今回の勉強会で使用した資料とサンプルコードはアップしてありますので参照ください。

スライド

解説に使用したサンプルコード

https://github.com/hmori/MyBlocks


今回サンプルコード解説では大きく分けて、BlocksKit自体の使い方、UITableView+BlocksKitExtendsの実装と使い方、UIViewControllerの基底拡張クラスの実装と使い方を中心に行いました。

MyBlocksでは下記のそれぞれ下記のGroupに実装しています。

  • BlocksKitSample (BlocksKitの使い方)
  • TableViewSample (UITableView+BlocksKitExtendsの使い方)
  • ViewLifeCycleSample (UIViewControllerの基底拡張クラスの実装と使い方)


まず最初にソースを見ないで色々と動作させてみて実装方法をイメージしてみてから、実際にソースを見てみるとかなりギャップがあると思います。色々と無茶してはいますがスマートに纏まっています。Blocksによる拡張はアイディア次第で疎結合な実装が簡潔に行え、ビジネスロジック部を切り出すことが可能です。
本当にiOSコーディングスタイルを変えるほどのインパクトがありますので、これを機にBlocksの拡張実装を検討してみては如何でしょうか。

ATND暦のソースコードを公開しました

ATND暦のアップデート版がApple審査が無事通過し公開されました。だいぶバグも落ち着き安定したので晴れてver1.0としました。今回のアップデートはWeb版のATNDカレンダーと同じく7日数以上続くイベントを表示しないようにしました。日数はオプションで変更が可能です。

ダウンロードはこちらから
ATND暦 ver1.0
http://itunes.apple.com/jp/app/id469446797?mt=8


ATND暦のソースコード公開

ver1.0の公開に合わせてATND暦のソースコードGithubに公開しました。
扱いやすいようにBSDライセンスとしています。コピーレフト条項はありません。ご自由に利用ください。起動アイコン(icon.png)についてはCCライセンスver3.0の表示のみとしています。

https://github.com/hmori/atndcal


何度かリファクタを続けてるので、そんなに酷いコードではないと思います。色々なWebサービスにアクセスしたり様々な機能を盛り込んでいるので参考になるかと思います。もし、何かあればフィードバックいただければ幸いです。

実装機能
サードパーティ
    • TapkuLibrary
    • json-framework
    • RegexLiteKit
    • FacebookLibrary
    • EvernoteLibrary
注意点
  • iOS4.0以上をターゲットとしています。
  • Xcode4以上で作成しましたので、Xcode3では動かないかもしれません。
  • 外部サービスのWeb APIのキーはダミーに変更しています (atndcal-exclusive-Prefix.pch)


そのうち、今後も技術的なネタをこの中のソースコードから紹介していきます。
余裕があれば。

Blockを利用し使いやすくUITableViewをカテゴリで拡張する

UITableViewについて実装の経験がある方は使いづらいと感じたことがあると思います。UITableViewDelegate、UITableViewDataSourceのデリゲートパターンのサイクルは覚えてしまえばすむ話ですが、実装後のメンテに苦労する人も多いのではないでしょうか?
下位実装においては処理単位で実装するように設計しますが、上位実装においては項目単位で属性やイベントの設計を行います。UITableViewのデリゲートパターンの使いにくい点は、Cellの数設定、CellのText設定、Cellの選択時の処理などの処理がそれぞれにまとまり、1項目に対した処理が様々なメソッドに記述しなければならないためです。UIKitのUITableViewでは、NSIndexPathで同じような分岐をそれぞれのDelegateメソッドで記述するので冗長になりやすく、また項目自体が条件により増減する場合は複雑になりがちです。


今回、以前ブログで紹介したBlocksKitのA2DynamicDelegateを利用して、UITableViewでBlockを利用できるようにカテゴリで拡張してみました。

例えば次のような使い方が可能になります。

行設定時の例
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];
	} 
	cell.textLabel.text = [_rows objectAtIndex:indexPath.row];

	[tableView setHandler:^(UITableView *tv, NSIndexPath *ip){
		//Cell選択時の処理
		[tv deselectRowAtIndexPath:ip animated:YES];
	} forDidSelectRowAtIndexPath:indexPath];

	[tableView setHandler:^(UITableView *tv, NSIndexPath *ip){
		//アクセサリーボタン押下時の処理
	} forAccessoryButtonTappedForRowWithIndexPath:indexPath];
	return cell;
}
UITableViewのセクション数、セクション毎の行数、セクションヘッダ、フッターの設定
    self.tableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-20-44)  
                                                   style:UITableViewStyleGrouped 
                                                delegate:self 
                                              dataSource:self] autorelease];
    [self.view addSubview:_tableView];

    // Number of Section in TableView.
    [_tableView setHandlerForNumberOfSectionsInTableView:^NSInteger(UITableView *tv){
        return 2;
    }];
    
    // Part of Section 0
    NSInteger section = 0;
    [_tableView setHandler:^id(UITableView *tv, NSInteger s){
        return @"Copy & Paste";
    } forTitleForHeaderInSection:section];
    [_tableView setHandler:^id(UITableView *tv, NSInteger s){
        return @"Menu is shown by LongPress";
    } forTitleForFooterInSection:section];
    [_tableView setHandler:^NSInteger(UITableView *tv, NSInteger s){
        return 2;
    } forNumberOfRowsInSection:section];
    
    // Part of Section 1
    section++;
    [_tableView setHandler:^id(UITableView *tv, NSInteger s){
        return @"Editing";
    } forTitleForHeaderInSection:section];
    [_tableView setHandler:^id(UITableView *tv, NSInteger s){
        return @"EDIT button press.";
    } forTitleForFooterInSection:section];
    [_tableView setHandler:^NSInteger(UITableView *tv, NSInteger s){
        return weakSelf.rows.count;
    } forNumberOfRowsInSection:section];

iOS5で追加されたポップアップメニューの例

        //copy function menu is shown by LongPress.
        [tv setHandler:^BOOL(UITableView *t, NSIndexPath *i){
            return YES;
        } forShouldShowMenuForRowAtIndexPath:ip];
        [tv setHandler:^BOOL(UITableView *t, SEL action, NSIndexPath *i, id sender){
            return action == @selector(copy:);
        } forCanPerformActionForRowAtIndexPath:ip];
        __block UITableViewCell *weakCell = cell;
        [tv setHandler:^(UITableView *t, SEL action, NSIndexPath *i, id sender){
            [UIPasteboard generalPasteboard].string = weakCell.detailTextLabel.text;
        } forPerformActionForRowAtIndexPath:ip];

使い方

ソースはこちらになります。

扱いやすいようにBlocksKitと同じMITライセンスにしますので改変含めご自由にご利用ください。


また、BlocksKitを利用しているのでBlocksKitおよびA2DynamicDelegateをプロジェクトに導入してください。導入方法はiOSコーディングスタイルを変えてしまうBlocksKitの紹介を参照してください。

UITableView+BlocksKitExtendsのインポート

利用するControllerでインポートしてください。

#import "UITableView+BlocksKitExtends.h"
UITableViewの初期化

A2DynamicDelegateを利用するため、初期化はUITableView+BlocksKitExtendsの initWithFrame:style:delegate:dataSource: を使ってください。

- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style delegate:(id)delegate dataSource:(id)dataSource;
    self.tableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 480-20-44)  
                                                   style:UITableViewStyleGrouped 
                                                delegate:self 
                                              dataSource:self] autorelease];
handlerの設定

setHandler:forXXXX:の XXXXの部分のメソッド名がデリゲートプロトコルに定義されたメソッド名になります。height関連メソッド以外は全て定義しています。handlerにnilを設定するとBlockはクリアされます。


注意点

標準のデリゲートメソッドの扱い

通常通りにViewControllerにUITableViewDelegate、UITableViewDataSource のデリゲートメソッドを記述しても動作します。UITableView+BlocksKitExtendsでは、通常のデリゲートメソッドを実行、更にsetHandlerで設定されたBlockを実行するようになっています。従って返却値が存在するメソッドについてはBlockで指定したほうが優先されます。また、デリゲートメソッド、Blockが存在しない場合はそれぞれ無視されます。

tableView:canEditRowAtIndexPath: の拡張デリゲートメソッド

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    BOOL ret = NO;
    id realDelegate = self.realDelegate;
    if (realDelegate && [realDelegate respondsToSelector:@selector(tableView:canEditRowAtIndexPath:)]) {
        //通常のデリゲートメソッド
        ret = [realDelegate tableView:tableView canEditRowAtIndexPath:indexPath];
    }
    NSString *key = [NSString stringWithFormat:kFormat, kHandlerCanEditRow, indexPath.section, indexPath.row];
    BKTableViewReturnBoolBlock block = [self.handlers objectForKey:key];
    if (block) {
        //setHandlerで設定されたBlock
        ret = ((BKTableViewReturnBoolBlock)block)(tableView, indexPath);
    }
    return ret;
}

大体の拡張デリゲートメソッドはこのようなロジックになっています。


高さ設定用メソッドについて

行、セクションヘッダ、セクションフッタの高さを設定する下記メソッドは、iOSのUITableViewDelegateのサイクル上、表示関連を扱うメソッド以前にコールされるためBlockを設定する機会がないので除外しました。

  • UITableViewDelegate#tableView:heightForRowAtIndexPath:
  • UITableViewDelegate#tableView:heightForHeaderInSection:
  • UITableViewDelegate#tableView:heightForFooterInSection:

デフォルトの高さを変更する際には通常のDelegateパターンによるメソッドを定義してください。


循環参照メモリリークについて

iOSコーディングスタイルを変えてしまうBlocksKitの紹介ででも書きましたが、ブロック外の変数にアクセスする場合は、循環参照を防ぐため__block指定子、__weak修飾子で再定義して参照するようにしてください。(※__weakはARCの場合のみ)
循環参照はInstrumentsで発見できないメモリリークなので注意が必要です。deallocにサウンドBreakPointの設定をお勧めします。


サンプルソース

サンプルソースGithubにアップしました。
MBBlocksTableViewControllerがUITableView+BlocksKitExtendsを利用したサンプルになります。

https://github.com/hmori/MyBlocks


MBBlocksTableViewControllerで実装しているTableViewの機能は下記になります。

  • ヘッダタイトル設定
  • フッタの設定
  • 行削除
  • 行移動
  • アクセサリーボタン
  • セル選択
  • セル内のポップアップメニュー&コピペ(セル長押し)

また無理やりにBlockを使った感満載ですが、loadViewとtableView:cellForRowAtIndexPath: のみでUITableViewに関するすべての処理を記述しました。


尚、このカテゴリはiOS5でしか動作確認をしていません。何か問題がある場合はフィードバックいただけると幸いです。