Dropboxを利用したOTA配布方法
開発したiOSアプリを自分以外の人にテストしてもらいたいことがあると思いますが、その際、AppStore経由インストールでなくとも起動が行えるAdHoc配布という方法を行います。AdHoc配布の前提として、インストールする実機のUDIDを事前にiOS Provisioning Portalに登録しておき、そのプロビジョニングファイルでビルドする必要があります。
※1ライセンスにつきデバイス新規登録数は100台まで
※1年後の開発ライセンスの更新までリセットされない
登録されているプロファイルでiOSアプリのAdHoc配布用ビルドモジュールを作成したら配布する方法として2種類あります。
iTunesライブラリ経由のインストールはテスターにとって非常に手間がかかります。現在ほとんどiOS4.0だと思いますので特別な理由がない限りOTA配布をお勧めします。
今回は簡易的に運用しやすいDropboxのウェブ公開機能を使ったOTA配布の構築を行います。
Dropboxの公開ページの利用
Dropboxは、複数PC上でファイルを同期するクラウドサービスですが、Webインターフェースも用意されており、htmlファイルはそのままウェブページとして表示されます。またPublicフォルダは常に固定URLで公開されているので簡易的な静的ホスティングサーバとしても使うことができます。
PC上のファイルと同期しているのでFTPなどのアップロードの手間が省けるのでOTA環境の運用にも最適だと思います。
Dropboxの公開準備
Dropboxをインストールすると「Dropbox>Public」というフォルダが作成されます。
Publicフォルダに「ota」というフォルダを作成し「index.html」を配置します。
中身は「Hello Dropbox」とでもしておきます。
Finderでindex.htmlを右クリックしパブリックリンクのコピーをします。
クリップボードにコピーされたURLにアクセスしindex.htmlの内容が表示されれば大丈夫です。
私の場合は下記のようなURLになりました。
http://dl.dropbox.com/u/8511076/ota/index.html
「http://dl.dropbox.com/u/8511076/」のDocumentRootがPublicフォルダとなります。
AdHocモジュールとアプリ配布定義ファイルの生成方法
1. XcodeでAdHoc配布用プロビジョニングファイルをBuildSettingsに設定し、ビルドターゲットをDeviceにして「Archive」する。
2. 「Organizer-Archives」でビルド済みモジュールを選択し、「Share」を選択する。
3. Contentsを「iOS App Store Package」を選択し「Next」を選択する。
4. 保存先をDropboxの「ota」を選択し、ipaファイル名を指定し、「Save for Enterprise Distribution」にチェックを入れ各項目を設定する。
- Application URL: Dropboxの公開Webページの絶対パス+ipaファイル名
- Title: インストール時のアプリ名
- Subtitle: 何でもよい
- Large Image URL: インストール中に表示されるRetinaディスプレイ用iconイメージ
- Small Image URL: インストール中に表示される57pxのiconイメージ
- Add Shine Effect to Images: インストール中に表示されるイメージの光沢効果
※Subtitle、Large Image URL、Small Image URLは設定しなくても問題ありません。Image URLを設定した場合は「ota」フォルダにアイコンの画像ファイルを配置します。
5. AdHoc配布用プロビジョニングファイルを「ota」配置する
6. index.htmlを編集する。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width"> <title>Dropbox OTA</title> <style type="text/css">img {width: 57px; height: 57px; -webkit-border-radius: 11px; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .4); margin:6px; vertical-align:middle;}</style> </head> <body> <p><img src="atndcal.png"> ATND暦 ver1.0 <a href="itms-services://?action=download-manifest&url=http://dl.dropbox.com/u/8511076/ota/atndcal.plist">Install</a></p> <p>プロファイル atndcal_adhoc <a href="atndcal_adhoc.mobileprovision">Install</a></p> </body> </html>
※モジュールインストールのurlはplistの絶対パスを記述します。画像, プロビジョニングのURLは相対パスで大丈夫です。
これで、iOS Provisioning Portalに登録されたデバイスであればSafariでindex.htmlにアクセスし直接インストールすることが可能になります。
注)ここで紹介しているDropboxサイトで公開しているモジュールはテスター用AdHocなので当然ながらインストール出来ません。ATND暦はAppStoreにて公開しています。
Dropbox共有フォルダの利用
前述の方法ではサイトへのアクセス制限がありません。プロファイルの仕組みにより端末の事前登録が必須なのでインストールできる端末の制限は行えますが、フルオープンなOTA環境を避けたい場合もあります。この場合はDropboxの共有フォルダを利用します。名前の通り当然、テストユーザ全員がDropboxアカウントを持っていることが前提となります。
1. Dropbox以下の適した位置で共有用のフォルダを作成する (Publicフォルダ以外の場所)
2. フォルダを右クリックし「このフォルダの共有...」を選択する
3. Dropboxサイトで招待するテストユーザーのメールアドレスを追加する
index.htmlのURLは次のようになります。
https://dl-web.dropbox.com/get/ota/index.html?w=xxxxxxxx
この場合のサイトへのアクセスURLはユーザーによってxxxxxxxxの部分が異なるので、テストユーザーはDropboxの共有されたフォルダのindex.htmlを選択する必要があります。
※一度アクセスしたパスは共有を消さない限り変わらないのでブックマークに登録できます。
OTA配布はiOSに不慣れなテスタでも簡単にインストールできます。Dropboxを使えば管理も楽に行えますのでお試しください。
iOSコーディングスタイルを変えてしまうBlocksKitの紹介
iOS4からBlockが導入されました。利用目的としてはDelegateパターンと大差ないと思っていますが、処理記述が呼び出し元で記述できることで可読性が高くなります。
また、コールバック時に利用されるperformedSelectorが非常に使いづらく複数の引数となった場合などを考慮してNSDictionaryで定義したりInvocationを使う必要がったりと面倒な点が多々ありました。
今回は、CoreFoundationにカテゴリで組み込まれたBlockを活用したBlocksKitフレームワークを紹介したいと思います。かなり便利なため、今後のコーディングスタイルを変えてしまうほどのインパクトがあると個人的には思っています。
- BlocksKit
https://github.com/zwaldowski/BlocksKit
BlocksKitはBSD, MITライセンスの元で利用可能です。Blockを使うので当然ながら、iOS4.0以上、MacOSX 10.6以上となります。BlocksKitの内部で別プロジェクトA2DynamicDelegateというモジュールが必要となります。
- A2DynamicDelegate
https://github.com/pandamonia/A2DynamicDelegate
BlocksKit導入手順
BlocksKitはFrameworkとしても導入できますが、ここでは静的ライブラリとして導入してみます。
- 1. ソースのダウンロード
- 上部のBlocksKit、A2DynamicDelegateからプロジェクトをダウンロードします。
- 2. BlocksKitにA2BlockDelegateを追加
- BlocksKitにA2DynamicDelegateとA2BlockDelegateのモジュールがないので「BlocksKit/A2DynamicDelegate/」にA2BlockDelegateとA2DynamicDelegateをコピーします。プロジェクト上でリンクが切れている場合は、リンクを消しXcodeからファイルを追加します。
- 3. 自ブロジェクトフォルダにBlocksKitフォルダを追加
- ここでは「自プロジェクトフォルダ/External/BlocksKit/」に配置しました。
- 4. 自プロジェクトにBlocksKitプロジェクトを追加
- 自分のプロジェクトにを開き、BlocksKit.xcodeprojをプロジェクトナビゲータにドロップして、BlocksKitプロジェクト毎追加します。
- 5. リンクライブラリにlibBlocksKit.a、MessageUI.frameworkを追加
- 自分のプロジェクトのターゲットのLink Binary With Librariesに libBlocksKit.a とMessageUI.framework を追加します。
- 6. その他リンカフラグに「-ObjC -all_load」を追加
- 自分のプロジェクトターゲットのBuild Settingsの Other Linker Flags に ObjC -all_load を追加します。
- 7. ヘッダ検索パスにBlocksKitを設定
- 自分のプロジェクトターゲットのBuild Settingsの Header Search Paths に"$(SRCROOT)/External/BlocksKit" を追加し、Recursiveにチェックをつけます。
- 8. pchにBlocksKitをimport
- 全ソースから呼び出せるように自分のプロジェクトのPrefix.pchに #import
を追加する
MyBlocks-Prefix.pch
#import <Availability.h> #ifndef __IPHONE_4_0 #warning "This project uses features only available in iOS SDK 4.0 and later." #endif #ifdef __OBJC__ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import <BlocksKit/BlocksKit.h> #endif
BlocksKitの利便性と便利な使い方
UIActionSheet+BlocksKit
UIActionSheetは delegateを指定し UIActionSheet#actionSheet:clickedButtonAtIndex でハンドリングします制御しますが、Controllerに複数のUIActionSheetが存在する場合は、インスタンス変数を利用したりTagを利用してDelegateメソッド内で分岐して記述する必要があります。また、タップされたButtonのIndexで管理しているために、条件によってボタン数が変わる場合などは表示する処理とタップ時の処理の箇所に、同じIndexに調整する処理を記述しなければなりませんでした。
ブロックを使わない場合のコード
- (void)showSheet1 { self.sheet1 = [[[UIActionSheet alloc] init] autorelease]; _sheet1.title = @"Sheet"; _sheet1.delegate = self; if (condition1) { [_sheet1 addButtonWithTitle:@"Action1"]; } if (condition2) { [_sheet1 addButtonWithTitle:@"Action2"]; } [_sheet1 addButtonWithTitle:@"Close"]; [_sheet1 showInView:self.view]; }
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (actionSheet == _sheet1) { NSInteger adjust1 = (condition1) ? 0 : 1; NSInteger adjust2 = (condition2) ? 0 : 1; if (buttonIndex == (0 - adjust1)) { //Action1の処理 } else if (buttonIndex == (1 - adjust1 - adjust2)) { //Action2の処理 } else if (buttonIndex == (2 - adjust1 - adjust2)) { //Close } } if (actionSheet == _sheet2) { //sheet2の処理 } }
UIActionSheet+BlocksKitを使用したコード
- (void)showSheet1 { UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet"]; if (condition1) { [sheet1 addButtonWithTitle:@"Action1" handler:^(void) { //Action1の処理 }]; } if (condition2) { [sheet1 addButtonWithTitle:@"Action2" handler:^(void) { //Action2の処理 }]; } [sheet1 setCancelButtonIndex:[sheet addButtonWithTitle:@"Close"]]; [sheet1 showInView:self.view]; }
ActionSheetの表示と同時に押下時の処理まで同じメソッド内に記述可能なので、ButtonIndexを管理しなくともよくなり、可読性も飛躍的に向上すると思います。
UIAlertView+BlocksKit
UIActionSheetと同じように、ボタン押下時の処理を表示時に記述できます。
UIAlertView *alert = [UIAlertView alertViewWithTitle:@"Alert" message:@"UIAlertView Test"]; [alert addButtonWithTitle:@"Action" handler:^(void) { NSLog(@"Action"); }]; [alert addButtonWithTitle:@"Close"]; [alert show];
UIAlertViewのボタン押下後に、続けてUIAlertViewを表示することも簡単に記述できます。
UIAlertView *alert1 = [UIAlertView alertViewWithTitle:@"Alert1" message:@"連続Alert表示"]; [alert1 addButtonWithTitle:@"Show Alert2" handler:^(void) { UIAlertView *alert2 = [UIAlertView alertViewWithTitle:@"Alert2"]; [alert2 addButtonWithTitle:@"Show Alert3" handler:^(void) { UIAlertView *alert3 = [UIAlertView alertViewWithTitle:@"Alert3"]; [alert3 addButtonWithTitle:@"Close"]; [alert3 show]; }]; [alert2 addButtonWithTitle:@"Close"]; [alert2 show]; }]; [alert1 addButtonWithTitle:@"Close"]; [alert1 show];
NSURLConnection+BlocksKit
通常、NSURLConnectionで実装する場合、delegateによりNSURLConnectionDelegate、NSURLConnectionDataDelegate などの複数のメソッドを実装してコールバックさせる必要があり結構な手間がかかります。
BlocksKitでは通信の処理を包括されているので、かなり簡潔に記述することが可能です。またリクエスト発行と同時に受信時の処理が同時にかけるので飛躍的に可読性は上がります。
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://d.hatena.ne.jp/h_mori/"]]; [NSURLConnection startConnectionWithRequest:req successHandler:^(NSURLConnection *con, NSURLResponse *res, NSData *data){ NSString *html = [[[NSString alloc] initWithData:data encoding:NSJapaneseEUCStringEncoding] autorelease]; NSLog(@"html=%@", html); } failureHandler:^(NSURLConnection *con, NSError *err){ NSLog(@"NSURLConnection failed : %@", [err localizedDescription]); }];
UIWebView+BlocksKit
UIWebViewも複数のdelegateメソッドを実装しますが、BlocksKitではUIWebViewのリクエスト時に受信時の処理を書くことができます。
UIWebView *webView = [[[UIWebView alloc] init] autorelease]; webView.didFinishLoadBlock = ^(UIWebView *v) { NSLog(@"didFinishLoadBlock"); }; webView.didFinishWithErrorBlock = ^(UIWebView *v, NSError *err) { NSLog(@"didFinishWithErrorBlock"); }; [webView.dynamicDelegate webViewDidFinishLoad:webView]; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://d.hatena.ne.jp/h_mori/"]]];
MFMailComposeViewController+BlocksKit
メーラーコンポーネントのMailComposerを使った場合も通常通りに実装すると処理部がバラバラとなりがちですが、BlocksKitを使うと簡潔に記述が可能です。
MFMailComposeViewController *ctl = [[[MFMailComposeViewController alloc] init] autorelease]; ctl.mailComposeDelegate = self; ctl.completionBlock = ^(MFMailComposeViewController *controller, MFMailComposeResult result, NSError *err){ if (result == MFMailComposeResultSent) { UIAlertView *a = [UIAlertView alertViewWithTitle:@"MFMailComposeViewController" message:@"sent mail."]; [a addButtonWithTitle:@"Close"]; [a show]; } }; [self presentModalViewController:ctl animated:YES];
UIControl+BlocksKit
基本的にはtarget、selectorの代替としてBlockで記述するのですが、コントロール設置と同時にそのアクションの処理がかけるのはやはり便利です。
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame = CGRectMake(40, 20, 240, 44); [button setTitle:@"Caption" forState:UIControlStateNormal]; [button addEventHandler:^(id sender) { //Action } forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button];
UIBarButtonItem+BlocksKit
UIControl+BlocksKitと同じでtarget、selectorの代わりにBlockを記述します。
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel handler:^(id sender) { //Cancel Action }] autorelease];
UIGestureRecognizer+BlocksKit
GestrueRecognizerもBlocksKitで拡張されています。例ではスワイプをハンドリングしています。
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithHandler:^(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location){ //Swipe Action }]; [self.view addGestureRecognizer:swipe];
NSObject+BlocksKit
基本的にはperformSelectorの代替ですが、performBlock:afterDelay: cancelBlock: が拡張されています。
[NSObject performBlock:^(void) { NSLog(@"fire"); } afterDelay:1];
NSTimer+BlocksKit
これも基本的にはtargetとselectorの代替です。
[NSTimer scheduledTimerWithTimeInterval:1 block:^(NSTimeInterval time) { //action every 1 sec. } repeats:YES];
ブロック実装上での注意点
ブロックはスコープ外の変数にアクセスできるように設計されているため、ブロック自身とブロック内で利用される変数はヒープメモリ領域にコピーされ、ブロック自身がブロック内で参照する変数を強参照を保持(retain)するようになります。通常ブロックは自コントローラ等で強参照で保持(retain)すると思いますが、ブロック内でselfや自コントローラ内の強参照のインスタンス変数を直接参照すると循環参照が発生し、メモリリークを引き起こします。
ブロックによる循環参照の例(メモリリーク)
UIButton *sheetButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; [sheetButton setTitle:@"UIActionSheet" forState:UIControlStateNormal]; [sheetButton addEventHandler:^(id sender) { UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet1"]; [sheet1 setCancelButtonIndex:[sheet1 addButtonWithTitle:@"Close"]]; [sheet1 showInView:self.view]; //ブロック内でselfを直参照 } forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:sheetButton];
この例では self > view > sheetButton > (block) > self と強参照されています。なのでこのコントローラのdeallocは永久に呼び出さずメモリリークとなります。
この循環参照を断ち切るにはブロックのスコープ外の変数にアクセスする場合に __block 指定子を利用し、弱参照にする必要があります。
iOS5で導入されたARC(Auto Reference Counting)が有効の場合は更に弱参照修飾子 (__weak) を使う必要があります。
ブロックによる循環参照の回避コード (※ARC無効)
__block MBBlocksViewController *weakSelf = self; // 循環参照回避のweakSelfを定義 UIButton *sheetButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; [sheetButton setTitle:@"UIActionSheet" forState:UIControlStateNormal]; [sheetButton addEventHandler:^(id sender) { UIActionSheet *sheet1 = [UIActionSheet actionSheetWithTitle:@"Sheet1"]; [sheet1 setCancelButtonIndex:[sheet1 addButtonWithTitle:@"Close"]]; [sheet1 showInView:weakSelf.view]; // weakSelfを参照するように変更 } forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:sheetButton];
ARC有効の場合
__block __weak MBBlocksViewController *weakSelf = self; //__weak修飾子 で定義
ブロックによる循環参照はコピペ等で簡単に発生してしまうので注意する必要があります。私の場合はブロックを利用する場合は dealloc にBreakPointを設定し、毎回開放されることを確認するようにしています。以前「※サウンドアクションによるデバッグ」で紹介した方法で、BreakPointの設定を変更し音のみの通知を行うようにすると楽になると思います。
サンプルソース
今回、BlocksKitを導入して色々と実験したソースをGithubにアップしました。
https://github.com/hmori/MyBlocks
MBBlocksViewControllerでは、Viewを配置するloadViewだけですべての処理をコーディングしてみました。ちょっと無理やり感がありますが処理が纏まっているので可読性はそう悪くないと思います。
この便利さを経験するとBlocksKit無しでは開発できなくなりそうです。新しいプロジェクトを開始する際に最初に導入するツールの定番になるかもしれないですね。
AutoUnboxingの落とし穴
久しぶりにJavaをデバッグしてて思わぬところで悩んだのでメモ。久しぶりのJavaネタです。
String s = convHoge(p.id);
スタックトレースを確認するとこの行でNullPointerExceptionが発生していました。メソッド内のトレースは含まれていないので間違いなくこの行が問題の部分です。
しかし、デバッグで変数 p を確認してみるとnullではなくインスタンスは存在している。そんな筈はないと暫くの間Inspectorとにらめっこしていたわけですが、問題点は思わぬ別のところにありました。
public class P { Integer id; }
public String convHoge(int i) { return String.valueOf(i); }
メソッドの第一引数の型がコールする側がInteger型、受ける側がint型となっていました。メソッドをコールするタイミングで代入時の暗黙の型変換が行われていて、そこでNullPointerExceptionが発生していたことになります。結論からするとp.idがnullだったわけです。
int i = p.id.intValue();
暗黙的にこのような挙動をしています。
ここで紹介しているソースはかなり簡略化していますがかなり悩みました。
Java1.4ではそもそもコンパイルエラーなのですが、S2SE5.0からautoboxing/unboxingという機構が加わり、数値ラッパ型とプリミティブ型が代入により許容される仕様となりました。
オブジェクト型とプリミティブ型はメモリ管理的にも異なっていることもあり、型変換を明示しないautoboxingの仕様は個人的には否定的です。暫くJavaから離れていた身なのでメリットもよくわからず、ただただ気持ち悪いのですが実際のところはどうなんでしょうかね?
アプリ内でパスワードロック機能を実現する
「アプリ内で無操作状態時にパスワードロックする」といったようなことを実装してみました。
PCで言うところのスクリーンロック機能はiOSにもありますが、基幹系システムのアプリ等の場合はセキュアなものが要求される場合があります。Webの世界ではSessionタイムアウトを利用するのが一般的ですが、これに近い仕掛けをiOSアプリで実装します。
要件
- タッチ無操作時間が一定時間を超えた時にパスワード入力画面をモーダルで表示する
- タッチイベントが検知された場合、無操作時間はクリアされる
実装方法
すべてのタッチイベントを検知する必要がありますが、おそらくUIWindow#setEvent: が一番適した位置かと思いますので、UIWindowを継承した自前のWindowを作成してタップイベントを検知し、タップの度にタイマーの時間を更新するようにします。
LTWindow
@implementation LTWindow static NSTimeInterval secInterval = 5.0f; - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self resetTimer]; } return self; } - (void)sendEvent:(UIEvent *)event { [self resetTimer]; [super sendEvent:event]; } - (void)resetTimer { [_checkExpireTimer invalidate]; _checkExpireTimer = [NSTimerscheduledTimerWithTimeInterval:secInterval target:self selector:@selector(expire) userInfo:nil repeats:NO]; } - (void)expire { [self resetTimer]; [(LTAppDelegate *)[[UIApplicationsharedApplication] delegate] lockScreen]; } @end
LTAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[[LTWindowalloc] initWithFrame:[[UIScreenmainScreen] bounds]] autorelease]; self.viewController = [[[LTViewController alloc] initWithNibName:@"LTViewController" bundle:nil] autorelease]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES; } - (void)lockScreen { if (!_viewController.presentedViewController) { id ctl = [[[LTLockViewControlleralloc] init] autorelease]; [_viewController presentModalViewController:ctl animated:YES]; } }
ダウンロード
サンプルコードをGithubにアップしてますので試してみたい方はご利用ください。
https://github.com/hmori/LockTest
UIWebViewのハンドリングもUIWindowのsetEventが利用できます。色々と便利なのでUIWindowは普段から拡張しておいてもいいのかもしれません。
インタラクションデザインの考察
先日、ソラコムさんの協力を得て第22回仙台スマート勉強会にて発表させていただきました。
私は絵を書けないプログラマなんですが、開発者視点で設計(デザイン)をどう考えるべきかをテーマにインタラクションデザインの考察という内容で行いました。予想以上に反響があったのでここで、幾つか掻い摘んで紹介したいと思います。
- 設計のポイント
- シンプルに分かりやすく、機能の整合性を保つ
- できるだけ標準のコントローラを使う
- 分かりにくい場所では文字より視覚効果・アニメーションを活用する
- メッセージを減らす工夫
- システムエラー等のメッセージは表示しない
- メッセージよりイメージ(画像)で表現する
- 入力チェックを行うよりも、未入力状態ではボタン自体を非活性化する
- 文言の工夫
- 専門用語は使わない
- ボタンの表記名は挙動を表す文言で表示する
- ✕ OK、キャンセル、はい、いいえ
- ◯ 閉じる、保存する、破棄する
- 入力補助の工夫
- 入力した履歴機能などを追加する
- ドリルダウンリスト、ピッカーなどでサジェスト機能を追加する
- 初期表示時のデフォルト入力値などを検討する
- GPSの活用(逆ジオコーディングなど有効に使う)
- ジェスチャの活用
- ショートカット的な目的で補助的なジェスチャを定義すると有効
- 標準的・一般的なジェスチャは再定義してはいけない
- ダブルタップ、トリプルタップよりはロングタップの方がベター
- ボタン配置
- iPhoneではユーザーが苦なく押せる最小サイズは44px
- 画面両サイドはケースで邪魔になることがある
- 主要なボタンは大きめのボタンにする
- 当たり判定を大きくするためにボタンサイズを大きくしボタン画像を小さくするなどの工夫
- ボタンのタップアクションはタッチアップ時に開始する
- アニメーション
- トランジション系は種類と方向に一貫性を持たせる
- その表現に意味をもたないアニメーションは避け、機能を視覚的に分かりやすくする目的で使用する
- UIAlertView、UIActionSheet
- ユーザーへの通知系はAlert、ユーザーへ選択させる場合はActionSheetを使う
- 端末によってはAlertViewは描画負荷が高いので注意する
- 加速度センサーの活用
- シェイク動作を操作補助に割り当てる(感度と検知する向きはじっくり調整する)
- デバイスの方向検知に利用しない方がいい場合もある
- ※家で使うアプリでは寝ながら使用する場合がある。
- 設定値はデフォルト値を熟慮
- 利用ユーザー層を想定し最適な値をよく検討する(特にフォントサイズ)
細かい手法論やポイントは幾つか存在しますが、その実装・デザインに正解はありません。理由は対象としている使い手によって使い勝手が変わるからです。例えばフォントサイズ一つとってみても、ユーザーの年齢やユーザーロケーション(歩いている/家でゆっくり)によって変わります。
重要なことは、対象としているユーザーの立場・視点にたってUI・機能の実現手段を考えるということだと思います。開発者はコンピュータリテラシーに長けていてアプリもたくさん触れています。常識だと思っていることで一般的にはそうではないという面が多々あります。B2Cのプロダクトを作る人はこのギャップは強く意識する必要があり、何も知らないことを前提として且つ使いやすく設計する必要があります。
実装などの技術論は先行していますがUI設計についてはデバイスと環境に深く依存するため、これから世界中で最適解を試行錯誤に模索していくのだと思います。これを機にインタラクションデザインを考えるキッカケになればと思います。
勉強会で使用したスライドを貼りつけておきます。
iPhoneアプリをHTTPデバッグする方法
PC上でWeb開発する場合のHTTPデバッグ方法としては色々とあります。
例えば、FirefoxのFirebug、IEのFiddler等のブラウザプラグインを利用する方法、横取り丸等でHTTPプロキシとしての利用方法があるのですが、iPhoneアプリではネイティブで直に動作するため、おそらくプロキシでのデバッグが一番確実かと思います。
今回は、Mac用のHTTPデバッガツール Charlesというアプリを利用したデバッグを紹介します。
http://www.charlesproxy.com/
デバッグ概要
MacにCharlesをHTTPプロキシとして起動させておき、iPhoneのWiFi設定でHTTPプロキシをCharlesのポート(default:8888)を指定させます。iPhoneからの全ての通信は全てMacのCharlesを経由することになるので、Charles側でハンドリングした通信ログを出力します。
- 概念図
iPhone <=> Charles (Mac:8888) <=> 接続先のサーバ
Macのローカルホスト名の設定
ルータがDHCPを使用している場合はMacのIPが変わると厄介なので、ローカルホスト名を指定します。
Charles設定
Charlesを起動し、設定アイコン>「Proxy Settings」を選択します。
port8888が既に使用されている場合は変更します。
iPhoneの設定
iPhoneの設定>「Wi-Fi」>ネットワークの詳細>HTTPプロキシ欄 でサーバとポートを指定します。
認証はオフで構いません。
サーバ名は先ほど指定したローカルホスト名、ポートはCharlesで設定したポート番号を入力します。
トレースの開始
この方法ではiPhoneのすべての通信をハンドリングしてしまうため、バックグラウンドで実行されているアプリをすべて終了させます。その後、対象のアプリを起動してiPhoneの通信ログをトレースしていきます。
SSL(port443)でなければ、全てのRequestとResponseの生データを確認することができます。
こんな感じにトレース表示されます。ステキ。
Charlesのログを止めたい場合は「Stop Record」ボタンを押します。
尚、プロキシとして動作するのはCharlesが起動している間だけなので、デバッグが終わったらiPhoneの設定をHTTPプロキシをオフに戻さないと当然ですが全く通信ができなくなります。
(番外編)Macと同じセグメントに参加できない場合の方法
iPhoneからMacにアクセスするためにはWi-Fiルータ等で同じセグメントに参加している必要がありますが、
iPhoneからWi-Fiに参加できない場合でもMacのEthernet経由でのインターネット共有を利用すれば同様のことが行えます。
- 概念図
iPhone <=(Macインターネット共有)=> Mac(8888) <=(Ethernet)=> サーバ
※ただし、この方法ではそのMacが参加しているネットワークをWi-Fiで簡易的に開いてしまうため
社内ネットワークだったり機密情報を持つネットワークの場合は十分注意してください。
機密情報を持つネットワークの場合はセキュリティ上に間違いなくNGだと思います。
Macのインターネット共有設定
インターネット共有の設定で「共有する接続経路」を「Ethernetアダプタ」、「相手のコンピュータが使用するポート」を「Wi-Fi」に設定します。
続いて、「Wi-Fiオプション」を押して、ネットワークの構築を行い、「インターネット共有」にチェックします。
iPhoneよりMacのインターネット共有に接続
iPhoneの設定>「Wi-Fi」よりインターネット共有で構築したネットワークを選択します。
続いてネットワークの詳細を選択し、同一セグメントの場合と同様にHTTPプロキシ欄のサーバとポートを指定すればCharlesでキャプチャが行えます。
概念さえ理解すれば、HTTPデバッグやインターネット共有は色々と応用が効くので一度経験しておくと何かの役に立つかもしれません。
Xcodeを使わずに起動アイコンを実機で確認する方法
iPhoneの起動アイコンは114px (iPhone3GSだと57px)と解像度が小さいため、いざ実機に入れると文字がつぶれたり色が予想と違っていたりします。通常アプリとして実機に入れるにはiOSデベロッパープログラムライセンスやらXcodeが必要となり、環境を作るにもデザイナーの人にとっては敷居が高いと思います。
今更かもしれませんが簡易的に実機で確認できるWebクリップのfavicon的な利用方法を紹介します。
HTMLファイルの用意
index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>アプリ名</title> <link rel="apple-touch-icon" href="icon.png" /> </head> <body> gloss effects<br/> <img src="icon.png"/> </body> </html>
Webサーバの配置
作成したindex.htmlファイルと起動アイコン用のicon.pngファイルをWebサーバに配置します。配置するサーバはPCのWebサーバを利用してもかまいません。Macの場合はシステム環境設定の共有でWeb共有をONにすれば簡単にWebサーバを起動できます。
ユーザーフォルダのサイト(Sites)フォルダがDocumentRootになります。サイトフォルダに"icontest"等のフォルダを作成し、"index.html"と"icon.png"を配置します。
iPhoneのSafariから配置したのindex.htmlにアクセスします。上記のMacのWebサーバを利用した場合は、Web共有に記載されたURLに/icontestを付加すればアクセスできます。
この場合だと "http://192.168.11.3/~hmori/icontest" になります。
表示後、下ツールバーの中央ボタンを押し「ホーム画面に追加」を押します。
光沢エフェクトの消し方
通常ではiPhoneアプリは通常では光沢処理(半円上の光源エフェクト)が自動で入りますが、これを消したい場合はlinkタグを下記のようにprecomposedに変更します。
<link rel="apple-touch-icon-precomposed" href="icon.png" />
サンプルを下記に用意しました。iPhoneのSafariで「ホーム画面に追加」するとアプリと同様に見えます。
http://hmori.s3.amazonaws.com/icontest/normal/index.html
http://hmori.s3.amazonaws.com/icontest/precomposed/index.html
Safariのlinkタグの公式ドキュメント
Safari Web Content Guide
デザイナーの方に毎回ファイルを送ってもらい自分の方で実機に入れて確認していましたが、この方法を教えておけばデザイナー自身で確認ができるようになりますね。