ラベル iPhone の投稿を表示しています。 すべての投稿を表示
ラベル iPhone の投稿を表示しています。 すべての投稿を表示

2012年4月15日日曜日

iphone_dev_jp 東京iPhone/Mac勉強会でしゃべってきた & 反省

昨日開催されましたiphone_dev_jp 東京iPhone/Mac勉強会で1セッションほどお話しさせて頂きました。
http://atnd.org/events/26946

当日の様子がニコ生で配信されていて、現在タイムシフトで視聴することができます。一週間視聴できるので、4/20までですかね。
http://live.nicovideo.jp/watch/lv88663921

このタイムシフト視聴であとから録画されたプレゼンが見られるというのが実に素晴らしくて、具体的には以下のようなご利益があるわけです。
  • 遠隔地の人(今回は大阪の人)が同時に勉強会に参加できる
  • 参加できなかった人があとから当日の雰囲気を見るのに使える
  • 発表者があとから自分のプレゼンを見て反省するのに使える
主催していただいた岸川さん、機材を準備していただいたshachiさんをはじめ運営の方々、ありがとうございます!この仕組が他の勉強会でも流行ればなぁとか思ってます。(自分で主催する気がなくてすんません><)

さてここからは反省会です。タイムシフトのお陰で普段できない「自分のプレゼンはどんな具合だったか」をあとから客観的に見直すことができるので、これは大いに捗るぞと思いちょっと見てみました。2時間30分ちょいぐらいから出てくる一番煩いのが私なのですが。

うん。これはひどい><
ともかく、悪かったところとまぁ良かったんじゃないってところを列挙してみます。
  • 喋るの早すぎ。テンションが上がると更に早くなって困る。
  • 落ち着きなさすぎ。動きがなさすぎるのもつまらないので動くの自体は良いと思っているけれども、動きに変な癖があってそれが煩い。
  • プレゼンの資料で前提知識というか説明が抜け落ちている箇所が多々ある。急ぎで作ったBlocksの資料とかが顕著で、なぜ最初にアプリをクラッシュさせてるのとか説明がない。唐突すぎる。
  • 超テンションで超フランクにやったおかげかツッコミがいただけた、ネタ議論っぽくなったのはすごく良かった。だが、うまく広げられてないのがもったいない。テンパりすぎ。
逆にプレゼンが非常にうまかったsonsonさんやfladdictさんのプレゼンを見て、自分のプレゼンと比較して気づいたところを列挙してみます。
  • 発表に安心感というか安定感というか落ち着きがある。このあたりは場慣れしていきたい。
  • 図の使い方、図による説明の仕方が上手い。直前まで作っていたとか手書きで仕上げたのはやっぱダメですね><
  • デモの回数・実施方法が適切。最もキモとなる1回か2回程度に抑え、あまり資料から離れないため、流れが中断されない。ちょっと自分のプレゼンはコードとデモに逃げすぎたため流れが悪くなっていた。必要であればコードは資料の方に書いておいて流れを切らないようにする(要するに手抜き資料を作ってはならない)

あとはこれを踏まえてどんどん場数を踏んで上達していきたいですね。ということでiOSの勉強会とかで発表者募集している方がいらっしゃいましたら是非宜しければ喋らせてください!

2011年11月19日土曜日

UIWebView.scrollView に対して KVO を使うと色々面白い

iOS 5より、UIWebViewにscrollViewプロパティが追加され、たとえばスクロールを無効にしたりステータスバーをタップしても一番上に戻らないようにしたりなど、UIWebViewのスクロール周りの処理を外から自由に触れるようになりました。ですが便利なのはこれだけではありません。KVOの仕組みを使うことで、さらにUIWebViewを便利に使うことができます。ここでは私が使っている中で一番のおすすめをご紹介します。

■UIWebViewの描画しているHTMLのcontentSizeを非同期的に、リアルタイムで取得する
UIWebview.scrollViewのcontentSizeプロパティは、UIWebViewの描画しているHTMLの大きさ(contentSize)と同じ値になります。この性質を利用して、contentSizeプロパティにKVOを貼ると、UIWebViewの描画しているHTMLの大きさ(contentSize)が変わったタイミングで通知を受け取ることができます。こんなコードになります。
- (void)viewWillAppear:(BOOL)animated
{
  [self.webView.scrollView addObserver:self forKeyPath:@"contentSize" options:0 context:NULL];
  [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://akisute.com"]]];
}
- (void)viewWillDisappear:(BOOL)animated
{
  [self.webView.scrollView removeObserver:self forKeyPath:@"contentSize"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  NSLog(@"%s", __func__);
  NSLog(@"  * contentSize = %@", NSStringFromCGRect(self.webView.scrollView.contentSize));
}
このテクを使うことで、例えばUIWebView自体をスクロールさせないようにした上で通知を受け取ってUIWebViewのframeをどんどん更新するようにし、UIScrollViewの中に埋め込んでしまってUIImageViewやUILabelのようにして使うみたいなUIを作ることが簡単にできます。HTMLの高さに依存するコードも作れますし、なかなか面白いですよ。iOS 5はもちろん、iOS 4でも動作します。(余談で解説)

■余談
UIWebViewのscrollViewプロパティは実はiOS 4のころからPrivate APIとして存在します。ですがiOS 5よりパブリック扱いになったおかげか、iOS 4向けのアプリでscrollViewプロパティを触っていてもリジェクトされたりクラッシュすることなく普通に使えるので非常に嬉しいです。iOS 3では使えませんのであしからず。

iOS 5でのSSL/TLS通信時にエラーが発生した場合のエラーコードの調べ方

iOS 5より新たにSecurity.frameworkというフレームワークが追加されました。このフレームワークはAppleが実装したSSL/TLS用のライブラリで、iOS 5よりCFNetwork系のクラス(NSURLConnectionなどの内部実装にも使われています)のSSL/TLS通信時に使われるようになったみたいです。

http://developer.apple.com/library/ios/#releasenotes/General/WhatsNewIniPhoneOS/Articles/iOS5.html の一番下にちょこっとだけ説明があるので、見てみると、
Security
The Security framework (Security.framework) now includes the Secure Transport interfaces, which are Apple’s implementation of the SSL/TLS protocols. You can use these interfaces to configure and manage SSL sessions, manage ciphers, and manage certificates.

For information about the Secure Transport interfaces, see the SecureTransport.h header file of the Security framework.
うむ、ドキュメント無いからヘッダ読めとはなかなか適当ですね。

普通にアプリを作っている最中にこのSecurity.frameworkを直接触る必要は全くないのですが、問題はこのSecurity.frameworkを使っているAppleのframeworkが通信時にSSL/TLS絡みのエラーを吐いたときに発生します。このとき、それらのフレームワークは以下のようなNSErrorを返してきます。
Error Domain=NSOSStatusErrorDomain Code=-9830 "The operation couldn’t be completed. (OSStatus error -9830.)
NSErrorの読み方はErrorDomainに指定されている文字列に応じてエラー番号が定義されているドキュメント/ヘッダファイルを見るというものなのですが、このNSErrorはNSOSStatusErrorDomainのErrorDomainを指定しているにも関わらず<CarbonCore/MacErrors.h>にエラー番号の定義が全く書いていないのです!

色々原因を調べた結果、以下のQ&Aが見当たりました。
http://developer.apple.com/library/mac/#qa/qa1499/_index.html
どうやらSecurity.framework絡みのエラーはNSOSStatusErrorDomainを使っているがMacErrors.hではなく<Security/SecureTransport.h>などに定義が書いてあるみたいなのです。見てみると・・・
/*************************************************
 *** OSStatus values unique to SecureTransport ***
 *************************************************/

/*
    Note: the comments that appear after these errors are used to create SecErrorMessages.strings.
    The comments must not be multi-line, and should be in a form meaningful to an end user. If
    a different or additional comment is needed, it can be put in the header doc format, or on a
    line that does not start with errZZZ.
*/

enum {
 errSSLProtocol    = -9800, /* SSL protocol error */
 errSSLNegotiation   = -9801, /* Cipher Suite negotiation failure */
 errSSLFatalAlert   = -9802, /* Fatal alert */
 errSSLWouldBlock   = -9803, /* I/O would block (not fatal) */
    errSSLSessionNotFound   = -9804, /* attempt to restore an unknown session */
    errSSLClosedGraceful   = -9805, /* connection closed gracefully */
    errSSLClosedAbort    = -9806, /* connection closed via error */
    errSSLXCertChainInvalid  = -9807, /* invalid certificate chain */
    errSSLBadCert    = -9808, /* bad certificate format */
 errSSLCrypto    = -9809, /* underlying cryptographic error */
 errSSLInternal    = -9810, /* Internal error */
 errSSLModuleAttach   = -9811, /* module attach failure */
    errSSLUnknownRootCert  = -9812, /* valid cert chain, untrusted root */
    errSSLNoRootCert   = -9813, /* cert chain not verified by root */
 errSSLCertExpired   = -9814, /* chain had an expired cert */
 errSSLCertNotYetValid  = -9815, /* chain had a cert not yet valid */
 errSSLClosedNoNotify  = -9816, /* server closed session with no notification */
 errSSLBufferOverflow  = -9817, /* insufficient buffer provided */
 errSSLBadCipherSuite  = -9818, /* bad SSLCipherSuite */
 
 /* fatal errors detected by peer */
 errSSLPeerUnexpectedMsg  = -9819, /* unexpected message received */
 errSSLPeerBadRecordMac  = -9820, /* bad MAC */
 errSSLPeerDecryptionFail = -9821, /* decryption failed */
 errSSLPeerRecordOverflow = -9822, /* record overflow */
 errSSLPeerDecompressFail = -9823, /* decompression failure */
 errSSLPeerHandshakeFail  = -9824, /* handshake failure */
 errSSLPeerBadCert   = -9825, /* misc. bad certificate */
 errSSLPeerUnsupportedCert = -9826, /* bad unsupported cert format */
 errSSLPeerCertRevoked  = -9827, /* certificate revoked */
 errSSLPeerCertExpired  = -9828, /* certificate expired */
 errSSLPeerCertUnknown  = -9829, /* unknown certificate */
 errSSLIllegalParam   = -9830, /* illegal parameter */
 errSSLPeerUnknownCA   = -9831, /* unknown Cert Authority */
 errSSLPeerAccessDenied  = -9832, /* access denied */
 errSSLPeerDecodeError  = -9833, /* decoding error */
 errSSLPeerDecryptError  = -9834, /* decryption error */
 errSSLPeerExportRestriction = -9835, /* export restriction */
 errSSLPeerProtocolVersion = -9836, /* bad protocol version */
 errSSLPeerInsufficientSecurity = -9837, /* insufficient security */
 errSSLPeerInternalError  = -9838, /* internal error */
 errSSLPeerUserCancelled  = -9839, /* user canceled */
 errSSLPeerNoRenegotiation = -9840, /* no renegotiation allowed */

 /* non-fatal result codes */
 errSSLPeerAuthCompleted     = -9841,    /* peer cert is valid, or was ignored if verification disabled*/
 errSSLClientCertRequested = -9842, /* server has requested a client cert */

 /* more errors detected by us */
 errSSLHostNameMismatch  = -9843, /* peer host name mismatch */
 errSSLConnectionRefused  = -9844, /* peer dropped connection before responding */
 errSSLDecryptionFail  = -9845, /* decryption failure */
 errSSLBadRecordMac   = -9846, /* bad MAC */
 errSSLRecordOverflow  = -9847, /* record overflow */
 errSSLBadConfiguration  = -9848, /* configuration error */
 errSSLLast     = -9849, /* end of range, to be deleted */

    /* DEPRECATED aliases for errSSLPeerAuthCompleted */
    errSSLServerAuthCompleted   = -9841, /* server cert is valid, or was ignored if verification disabled DEPRECATED */
 errSSLClientAuthCompleted   = -9841,    /* client cert is valid, or was ignored if verification disabled; reusing error as you can only be client or server - DEPRECATED */

};
ばっちり書いてますね。これでエラーの原因も特定できます。今回のケースはerrSSLIllegalParamだったみたいですね。

2011年11月18日金曜日

iOS 5の日本語キーボードの高さに対応する (iOS 3, 4, 5全対応)

iOS 5より日本語キーボードの高さが変わっているので、今まで決め打ちで高さ216pxとかやってレイアウトしていたビューが軒並み使えなくなってしまいました。今後はキーボードが出たり引っ込んだり種類が切り替わったりのタイミングできちんとキーボードの大きさを調べて適切にビューをレイアウトしてやる必要があります。ということでその対応をしたのでメモ。

前提条件として、以下の要件を満たすように作りました。
  • iOS 3, 4, 5全てで正常に動作すること。iOS 3.0でも動作しなければならない。
  • キーボードのframeを適切に取得できること
  • キーボードが出てくるタイミング、消えるタイミング、キーボードの種類が変わるタイミング、全て取れること

■まずはログを見てみる
キーボードの動作のタイミング、およびキーボードのframeは、NSNotificationを使って取得することができます。使用するNotification名はUIWindowのドキュメントに以下のように定義されています。
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIWindow_Class/UIWindowClassReference/UIWindowClassReference.html
// Available from iOS 2
UIKeyboardWillShowNotification
UIKeyboardDidShowNotification
UIKeyboardWillHideNotification
UIKeyboardDidHideNotification
// Available from iOS 5
UIKeyboardWillChangeFrameNotification
UIKeyboardDidChangeFrameNotification
で、これらのNotification名でNSNotificationCenterにobserverを追加すると、通知が飛んできます。飛んできたNSNotificationオブジェクトのuserInfoプロパティに特定のキーでキーボードのframeが格納されているというしくみです。使えるキーは以下のとおり。
// Available from iOS 3.2
UIKeyboardFrameBeginUserInfoKey
UIKeyboardFrameEndUserInfoKey
// Available from iOS 3.0
UIKeyboardAnimationCurveUserInfoKey
UIKeyboardAnimationDurationUserInfoKey
// Available from iOS 2.0 ~ 3.2 (Deprecated in newer versions)
UIKeyboardCenterBeginUserInfoKey
UIKeyboardCenterEndUserInfoKey
UIKeyboardBoundsUserInfoKey
iOS 3, 4, 5すべてできちんと動作しなければならないので、これらをふまえて、以下のように実装します。
  • Notification名にはUIKeyboardWillShowNotification, UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, UIKeyboardDidHideNotificationを使う。
  • userInfoのキーには、iOS 3.0/3.1のみUIKeyboardBoundsUserInfoKeyを使い、それ以外のバージョンではUIKeyboardFrameEndUserInfoKeyを使う。

■実装してみる
ということでまずはサンプルアプリを作って動かしてみて、実際に動作を見てみることにしました。大体こんな感じのコードです。
- (void)viewWillAppear:(BOOL)animated
{
  // Notification observerを追加する
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onUIKeyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
  // Notification observerを削除する
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)onUIKeyboardWillShowNotification:(NSNotification *)notification
{
  NSLog(@"%s", __func__);
  NSLog(@"  * userInfo = %@", notification.userInfo);
  // UIKeyboardFrameEndUserInfoKeyが使える時と使えない時で処理を分ける
  CGRect bounds;
  if (&UIKeyboardFrameEndUserInfoKey == NULL) {
    // iOS 3.0 or 3.1
    // bounds
    bounds = [[notification.userInfo objectForKey:UIKeyboardBoundsUserInfoKey] CGRectValue];
  } else {
    // それ以外
    // frameだがoriginを使わないのでbounds扱いで良い
    bounds = [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
  }
  // boundsを使った処理をここに書く
}

ログはこんな感じに。

iOS 5.0, iPhone 4S
2011-10-19 10:35:15.007 SampleApp[5675:707] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきている
2011-10-19 10:35:15.502 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:15.506 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:15.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:15.510 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:15.513 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:15.514 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.35";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:16.162 SampleApp[5675:707] -[SampleViewController viewDidAppear:]
// キーボード引っ込める
2011-10-19 10:35:44.246 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:35:44.247 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
2011-10-19 10:35:44.509 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:35:44.511 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
}
// キーボード出す
2011-10-19 10:35:55.135 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:55.136 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:35:55.397 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:55.398 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = "0.25";
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 588}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 480}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 日本語キーボードに変更
2011-10-19 10:35:58.167 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:35:58.168 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
2011-10-19 10:35:58.170 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:35:58.171 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 390}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 354}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
}
// 英語キーボードに変更
2011-10-19 10:36:00.483 SampleApp[5675:707] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:36:00.484 SampleApp[5675:707]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0;
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
2011-10-19 10:36:00.485 SampleApp[5675:707] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:36:00.486 SampleApp[5675:707]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
    UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 336}";
    UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 372}";
    UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 228}, {320, 252}}";
    UIKeyboardFrameChangedByUserInteraction = 0;
    UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 264}, {320, 216}}";
}
// 画面を抜ける、キーボードが隠れる通知より先にviewWillDissappearされるので通知が来ない
2011-10-19 10:36:03.570 SampleApp[5675:707] -[SampleViewController viewWillDisappear:]
2011-10-19 10:36:03.986 SampleApp[5675:707] -[SampleViewController viewDidDisappear:]
iOS 3.1.3, iPhone 3G
2011-10-19 10:43:46.548 SampleApp[352:207] -[SampleViewController viewWillAppear:]
// 前の画面の英字キーボードが一旦引っ込んで出てきているのだが、iOS 3.1.3では引っ込む側の挙動が見られない。出てくるだけになっているようだ。
// さらにDidShowの通知がキーボードがviewDidAppearの呼び出しのあとに行われるようになっている。
// どうやらキーボードがどこに属しているのかが違うみたいだな。
2011-10-19 10:43:47.202 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:47.210 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {480, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:48.253 SampleApp[352:207] -[SampleViewController viewDidAppear:]
2011-10-19 10:43:48.315 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:48.336 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボード隠す
2011-10-19 10:43:52.854 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:43:52.862 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
2011-10-19 10:43:53.183 SampleApp[352:207] -[SampleViewController onUIKeyboardDidHideNotification:]
2011-10-19 10:43:53.188 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 588};
}
// キーボード出す
2011-10-19 10:43:54.621 SampleApp[352:207] -[SampleViewController onUIKeyboardWillShowNotification:]
2011-10-19 10:43:54.629 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.300000011920929;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
2011-10-19 10:43:54.972 SampleApp[352:207] -[SampleViewController onUIKeyboardDidShowNotification:]
2011-10-19 10:43:54.977 SampleApp[352:207]   * userInfo = {
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 588};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {160, 372};
}
// キーボードの種類をここで英語→日本語、日本語→英語に変えているのだが、通知が来ない
// だがiOS 3ではキーボードの種類によって高さが違うということが(基本)ないので気にしなくて良い
[Switching to process 11779 thread 0x2e03]
warning: No copy of  found locally, reading from memory on remote device.  This may slow down the debug session.
// 画面抜ける、viewWillDisappearより先にKeyboardが隠れる通知が来る
2011-10-19 10:44:07.442 SampleApp[352:207] -[SampleViewController onUIKeyboardWillHideNotification:]
2011-10-19 10:44:07.454 SampleApp[352:207]   * userInfo = {
    UIKeyboardAnimationCurveUserInfoKey = 0;
    UIKeyboardAnimationDurationUserInfoKey = 0.35;
    UIKeyboardBoundsUserInfoKey = NSRect: {{0, 0}, {320, 216}};
    UIKeyboardCenterBeginUserInfoKey = NSPoint: {160, 372};
    UIKeyboardCenterEndUserInfoKey = NSPoint: {480, 372};
}
2011-10-19 10:44:07.472 SampleApp[352:207] -[SampleViewController viewWillDisappear:]
2011-10-19 10:44:08.374 SampleApp[352:207] -[SampleViewController viewDidDisappear:]
問題なさそうですね。キーボードの種類が切り替わったタイミングにもUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationがきちんと呼ばれているようです。これならUIKeyboardWillChangeFrameNotificationを使う必要はあんまり無いように思えます。実際、UIKeyboardWillChangeFrameNotificationを使わないでUIKeyboardWillShowNotificationとUIKeyboardWillHideNotificationだけを使ったアプリをリリースしていますが、特に問題なさそうです。

2011年9月12日月曜日

UIWebView の Private API を使って BASIC認証のあるページにアクセスする

元ネタはこちら: http://d.hatena.ne.jp/KishikawaKatsumi/20090603/1243968707

仕事でどうしても以下の要件を満たすUIWebViewが必要になったので作りました。
  • 開発環境にBASIC認証がかかっており、そこにUIWebViewでアクセスしたい。
  • アクセス先のHTMLにリンクが埋め込まれているため、URLをhttp://username:password@example.comのように変換することができない。webView:shouldStartLoadWithRequest:navigationType:で頑張ればいける気がしなくともないのですが結局断念しました。
  • 諸事情によりNSURLConnectionが使えない(当然ASIもダメ。あくまでUIWebViewでアクセスする必要がある)。
普通につくるとどうにもうまくいかなかったので、結局UIWebViewをオーバーライドしてPrivate APIを叩く作戦を取ることにしました。

というわけで出来上がったソースコードはこちら。MITライセンスです。
https://gist.github.com/1210372
iOS 4.3.5で動作確認しています。iOS 5でも多分動作します。 iOS 3以下はわかりません><

注意: このコードにはPrivate APIが多分に含まれています。このコードが含まれたアプリをApp Storeに提出しても十中八九審査に通りません。あくまで開発環境での検証用または自分のiPhoneに入れてニヤニヤするコードにのみお使いください。


■しくみ

UIKitをclass-dumpしてたら使えそうなシグネチャを見つけたので、継承してオーバーライドしてゴニョゴニョしてたらできちゃった!って感じです。それにしてもWebKitはPrivate APIにしておくにはもったいない出来の良さですね。このAPIへのフルアクセスがあればAndroidのWebViewと同等かそれ以上に自由に使えるのですが。

2011年8月22日月曜日

UDIDが使えなくなりそうなので、UIIDを使えるようにしました

■2012/11/11追記
iOS 6より[[UIDevice currentDevice] identifierForVendor]というAPIがAppleより提供され、よりプライバシーに配慮した上により安全な方法で自分の開発したアプリケーションを利用するユーザーを個別に認証することが可能になりました。それに伴い拙作のライブラリもidentifierForVendorが利用可能であればこちらを利用するように修正いたしました。今後はこのidentifierForVendor(または広告APIなどを作る場合であれば[[UIDevice sharedManager] advertisingIdentifier])が個体認識の主流になっていくと思われます。identifierForVendorとadvertisingIdentifierの仕様まとめは http://stackoverflow.com/questions/11836225/ios6-udid-what-advantages-does-identifierforvendor-have-over-identifierforadve が一番詳細かなと思います。
追記終


つい先日TechCrunchがiOS5よりUDIDの使用が非推奨になると報道し、巷はiOSでのUDIDの使用についての話題で俄然盛り上がっています。セキュリティ的によろしくないから良い変更だという声もあれば、すでに認証用として使っていてシステム改修が必要という悲観の声もあります。しかし私はどちらかというとUDIDをバリバリ使っちゃってる方陣営の人なので、セキュリティの問題については知っていても、やはりUDID相当の物が無いと不便だなぁと思ってしまうのです。そこでプログラマらしくコードで解決することにしました。

UDIDにはセキュリティの懸念があるし、もう使えなくなる。だったらもっといいIDを超簡単に生成して使えるようにすればいいじゃない。ということでUnique Installation Identifier (インストール毎ID、UIID) を生成するライブラリを早速書いてみました。
ソースコードはこちら。MITライセンスです。
https://github.com/akisute/UIApplication-UIID
昔のgistはこちら

使い方は超簡単で、
  1. アプリが一度削除されたとしても同一のUIIDを返すようにしたいのであれば、#define UIID_PERSISTENT=1して、Security.frameworkをプロジェクトに追加する
  2. UIApplication+UIID.hをimport
  3. [[UIApplication sharedApplication] uniqueInstallationIdentifier]で取れます
はいこれだけです。


■そもそもどうしてUDIDを使うのか

たいていの場合は以下のような理由です。
  • ログイン機構なしに簡易にユーザーの識別をしたいときに、毎回同じ値を返し、かつユーザー事に異なる値が取得出来る何かが必要になるので、UDIDを使う
  • UDID値の取得がいつでもどこでも可能、さらに超簡単で外部ライブラリのインストールなど面倒なことが一切無い
  • しかもAppleのドキュメントにそう使えって書いてある。・・・補足すると使ってもいいよ、ぐらいのニュアンスで、使えという風に推奨はしてないみたいです>< またUDIDのみをキーとしてユーザー情報を保存するな、とも書いてますね。


■UDIDは何がまずいの

主にまずい理由はセキュリティです。特にガラケーのかんたんログインの問題が参考になるのですが、iOSの場合は以下のようなセキュリティ問題が発生します。
  • 相手のUDIDがわかってしまえば、簡単になりすましが可能
  • UDIDの値を返すメソッドの実装を差し替えて、任意の値を返すように出来る。JailBreakしているユーザーであれば誰でも簡単に実行可能、そのためのアプリもCydiaで検索すれば転がっている
  • UDID自体、様々な方法で取得可能。アプリをインストールさせてそこ経由で集めてみたりはもちろん(その際値を取得することに対する警告すら出ない)、iTunesからでも値を確認できる
  • 上記三つの理由のため、その気になればたやすく任意のユーザーになりすませる
  • さらに恐ろしいことにUDIDは端末事に一意になるため、一度値が特定されてしまうと端末を買い換えない限りずっとなりすまされてしまう、リセットできない


■UIIDだとどうなる

Unique Installation Identifier, UIIDはUDIDと異なり「アプリのインストール基盤毎」に一意な値を返すような実装になっています。簡単にまとめると、あるアプリAが、デバイス1, 2, 3にインストールされた場合、UIIDは1, 2, 3の全てで異なる値になります。ここまではUDIDと同じですが、UDIDと違うのはデバイス1に別々のアプリA, B, Cがインストールされた場合、それぞれ異なる値になります。中身は単にCFUUIDというiOSに元からある良くできたID生成ルーチンなんですが、これにより以下のようなメリットが得られます。
  • 外部からIDの値を取得するのが困難。推測も十分に長い上に独立な値なため困難、取得も(UIID_PERSISTENT=1でビルドすればKeychainを使うので)困難です。
  • 万が一何かの間違いで流出したとしても、アプリごとに異なる値になるため他のアプリのセキュリティが犯されたりはしない。また同様の理由で他の悪意のあるアプリからUIIDを取得して攻撃することもできない。
  • プログラム的にUIIDをリセット可能。UUID_PERSISTENT=0であればアプリを消せばユーザーが任意にリセット可能。
  • Keychainを使ったメリットとして、ユーザーがデバイスを乗り換えてもiTunesのバックアップに値が保持されるため、それから復元を行った場合UIIDの値が引き継がれる。
  • 通常のアプリであればUDIDを使っていたケースのほとんどはこのUIIDでそのまま代用可能、あとは過去のUDIDとUIIDのヒモ付だけサーバー側でやってしまえば完全に乗り換えられる
  • それでもどうしてもデバイスごとに一意な値が欲しいならhttps://github.com/gekitz/UIDevice-with-UniqueIdentifier-for-iOS-5/blob/master/Classes/UIDevice+IdentifierAddition.mなど使えばよいのかなと。ただしこの実装はMACアドレスの値を使っているため、MACアドレスの値がわかってしまえばIDをたやすく生成可能で、UDIDの持つセキュリティ上の問題は残ります。


■使用上の注意

UDIDよりは問題が少ないですが、それでもこのUIIDだけで認証を行うような作りのアプリには間違ってもしないように!以下のような問題があります。
  • そもそもこの値は完全にユーザーと一意にヒモ付いた値ではありません。UIIDはあくまでアプリのインストール単位とヒモ付いているだけです。複数のデバイスを一人のユーザーが所有していたりすると完全にアウトですし、インストールされた端末が譲与された場合も対応できません。
  • プログラム的にUIIDはリセット可能なので最悪の場合でもずっとなりすまされるのは回避できるのですが、その際にヒモ付けられていたユーザー固有の情報が消えてしまいます。ログインIDとパスワードで認証をしているのであれば、ログインIDはそのままにしてパスワードだけをリセットすることで、なりすましの問題を解決しつつ、ユーザーの一意性は保ったままにできるため、万が一の際はログインIDとパスワードを使っている方が圧倒的に利便性が高まります。
  • 以上の理由により、UIIDはあくまで簡易的・一時的にユーザー認証をする時に使用し(たとえばユーザーが購入したアプリ内課金の商品をアプリが消されるまでの間だけ履歴として持っておいたり、ゲームのランキング等で匿名だけれど点数ランキングに参加できるようにしたり)、正式で完全なサービスはログインIDとパスワードによって提供するべきです。そうすることで一人のユーザーが複数のデバイスでサービスを使えるようになります。さらにはAndroidとも連携できたりしますしお得です。


■っていうか

全部高木先生が一年前に言っている通りになっちゃってるじゃないすか!っていうか私が作った物もこの高木先生がおっしゃってる「アプリ専用の(セキュアな)独自IDを生成してそれを保存して使う」というものの実装にすぎません。しかしまったく、せっかく警告してもらっても、人間実際に問題に直面しなければなかなか手をつけないものですね><

しかし、しかしですね、あえてここで一言、エンジニアとして申させていただきたい。

私、エンジニアが欲しいのはセキュリティ上正しい実装の方法だとか、概念だとか、ましてやどこそこのログイン方法はいけてないから直せや、などという文章でもないのです。我々が欲しいのは、「すでに実装されている、セキュリティ上正しくて、猿でも理解できて、1分で組み込めて、どのような環境でも動き、ユーザーが会員登録なんて面倒極まりないことをしなくても済むユーザー認証の手段」なんです。要するに、
// 何か良くわからんけどこのトークンをHTTPS経由でPOSTして認証しておけば超スーパー確実かつセキュアで猿でも実装できてハッピーになる
[[NSAuthentication sharedUser] authenticationToken]
↑コレが今すぐ欲しいんですよ、我々エンジニアというのは。そうすれば誰だってUDIDを使って認証するみたいな面倒くさいことするわけ無いじゃないですか。頼まれたってやりませんよ。

私はユーザー認証をしたいだけなんです、それも可能な限り楽に。口で何と批判しようが、正しい方法を教えようが、世の中は決して変わらないと思います。みんな楽をしたいから。なのでAppleには是非UDIDを廃止するこの機会にぜひ上記のような何かをUIKitなりFoundationなりに組み込んで欲しいですね。こういうところも、良いプラットフォーマーの責務の一部じゃないかなと。

2011年8月12日金曜日

iOS で ImageIO を使ってアニメーションGIFファイルを生成してみる

参考にしたのはこちら。
http://pojos-devlog.blogspot.com/2005/08/saving-animated-gif-using-coregraphics.html

iOS 4以降でよければImageIOフレームワークが使えるためむちゃくちゃ簡単です。任意のUIImage / CGImageRefから好きなようにアニメーションGIFを生成できます。



iOS 3以前の場合は・・・頑張れとしか・・・

メモ: CoreDataで更新処理をするときは、lockをわすれずに

単なるメモ書きです><

http://twitter.com/#!/akisutesama/status/83521489382555650
http://twitter.com/#!/akisutesama/status/83521729380626433
ある一つのCore Dataのモデルを非同期的に複数箇所から更新するときは、たとえどんなに軽微な、プロパティ一つだけの、他からは触られない様な変更ですら、きちんとlockを取らないと危険ということがわかった。API実行クラスだけでは不十分であった。非同期であればロック必須。
変更を行うコードブロックを渡して、内部で安全にロックして実行、必要に応じてロールバックや失敗通知も行える様にする仕組みを作ろうと思った。
CoreDataのモデルオブジェクトの更新はただのsetterプロパティの使用だけで発生してしまうのでついつい忘れがちになるのですが、これが原因で実際にクラッシュしたアプリもあるので油断禁物。

UIPanGestureRecognizerはiOS4.0ではtranslationプロパティを正しく返さない

UIPanGestureRecognizerのtranslationプロパティは、iOS 4.0でかつUIScrollViewの配下になっているviewに対して取り付けた場合、常にCGPointZeroを返してしまうようです。iOS 4.1のシミュレータで確認したら直ってましたので、iOS 4.0限定だと思われます。というわけでvelocityプロパティをかわりに使っておくことをお進めします。

検証結果はこちら

[NSObject load] と [NSObject initialize] の違い

クラスがObjective-Cのランタイムにロードされ利用可能になったタイミングで、そのクラス全体の初期化を行いたいということはよくあると思います。Objective-CではNSObjectクラスの以下のメソッドを用いてクラス全体の初期化を行うことができます。
  • + load
  • + initialize
この2つですが、結構挙動が異なります。詳細については以下のとおり。
http://cocoawithlove.com/2008/03/cocoa-application-startup.html
  • loadメソッドはクラスがロードされて利用可能になったら即座に呼び出される。
    • このとき、自分以外の他のクラスはまだロードされていない可能性があるので、自分以外のクラスを利用するような初期化はできない。
    • main関数の内部のNSAutoReleasePoolが用意されるよりも先に呼び出されるので、autoreleaseを使うような初期化を行う場合には自分でNSAutoReleasePoolを生成して管理する必要がある
  • initializeメソッドはそのクラスに実際のアクセスが最初に発生したタイミングで呼び出される。
    • 要するに一度も使われないクラスでは呼び出されない。
    • 自分以外のクラスもロードが完了しているので、自由に他のクラスを利用できる。
    • autoreleaseについても特に気にしなくて良い。
基本はinitializeメソッドを使うほうがより安全で確実なうえに、使われないなら初期化されないので経済的でいい感じです。こちらを使うことをお勧めします。

またloadメソッドについては、iOS実機で自家製frameworkを使っているを使っているとき、framework内部にビルドされているクラスのloadメソッドが呼び出されないという問題があります(静的ライブラリ.aについては未検証)iOSシミュレータおよびMacではきちんとframeworkに含まれているクラスについてもloadメソッドが呼び出されるのですが・・・ともかく地雷が大きいので避けたほうが懸命です。

[UIView willMoveToSuperview:] が便利です

UIKitやFoundationには、iOS 2.0のころから存在するのに、意外と知られていない便利なメソッドやプロパティがたくさんあります。今回はUIViewのメソッドをご紹介します。

UIViewはUIViewControllerと違ってライフサイクルが単純で、どのタイミングで自分自身が画面上に追加されたのか、どのタイミングで自分自身が画面から外されたのか、などを把握しづらいとお嘆きの方がいらっしゃると思います。事実その用途のためだけにUIViewControllerを使ってプログラミングをしている人も見かけます。そこで以下のメソッドをご紹介です。
  • willMoveToSuperview:
    • 自分自身が新しいSuperview以下に移動しようとしたとき(新しいSuperviewに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToSuperview
    • 自分自身が新しいSuperview以下に移動したとき(新しいSuperviewにaddSubview:されたとき)に呼び出されます。
  • willMoveToWindow:
    • 自分自身が新しいWindow以下に移動しようとしたとき(新しいWindowに対してaddSubview:されようとしたとき)に呼び出されます。
  • didMoveToWindow
    • 自分自身が新しいWindow以下に移動したとき(新しいWindowに対してaddSubview:されたとき)に呼び出されます。
  • didAddSubview:
    • 自分自身に他のviewがsubviewとして追加されたときに呼び出されます。
  • willRemoveSubview:
    • 自分自身のsubviewsから他のviewが取り除かれようとしているときに呼び出されます。
これらのメソッドをUIViewのサブクラスでオーバーライドすることにより、かなりの自由度でviewの動きをコントロールすることができます。
たとえば自作のUIViewで、画面にviewが追加されたタイミングで何かしたい・・・というときなどは以下のようにできます:
- (void)willMoveToSuperview:(UIView *)newSuperview

{
NSLog(@" * superview = %@", newSuperview);
NSLog(@" * superview's window = %@", newSuperview.window);
// UIViewControllerでいうところの loadView 兼 viewDidLoad 兼 viewWillAppear 兼 viewWillDisappearみたいなタイミング
}

- (void)didMoveToSuperview
{
// UIViewControllerでいうところの viewDidAppear 兼 viewDidDisappear みたいなタイミング
// ここで、もしsuperviewがあり(画面に表示される可能性があり)、まだ自分自身のデータが初期化されていない場合には
// reloadDataして初期表示データを読み込む
// superviewがない場合には画面から外されたのですべてのビューまわりをリセットして、次の表示に備えるようにしておく
if (self.superview) {
if (!self.someData) {
[self reloadData];
}
} else {
self.someData = nil;
[self __resetOutlets];
}
}

- (void)willMoveToWindow:(UIWindow *)newWindow
{
NSLog(@" * window = %@", newWindow);
// いまいち使いづらいのでwillMoveToSuperviewとかを使うようにしてます
}

- (void)didMoveToWindow
{
// いまいち使いづらいのでdidMoveToSuperviewを使うようにしてます
}
これでUIViewの使い勝手もアップ!ですね。

2011年8月9日火曜日

[UITableViewController scrollToRowAtIndexPath:atScrollPosition:animated:] の挙動まとめ

UITableViewController の scrollToRowAtIndexPath:atScrollPosition:animated: メソッドは、対象のテーブルビューのセクションにヘッダ・フッタが付いている場合挙動が変化する事がわかったので、ちょっと調査してまとめてみました。具体的には以下のような動きをするようです。

  • このメソッドは自分で呼び出すか、またはテーブルビューのセルの中に UITextField のようなフォーカスを取るコントロールを配置して、それが選択されたときに呼び出される
  • このメソッドで指定した indexPath の section に Header View or Header Text / Footer View or Footer Text が指定されているとき、このメソッドは選択された indexPath の row だけではなく、それらのヘッダやフッタも同時に表示される位置にスクロールしようとする
  • ということであんまり長い Section Header / Section Footer を作ると scrollToRowAtIndexPath:atScrollPosition:animated: の挙動がおかしくなる
  • Table View Header / Table View Footer については全然無関係なので長くしても大丈夫
画像にすると以下のような感じになります。

初期状態

section3つ、row3つ、合計9行のtable viewを作って、それぞれにsection header / section footerを追加しました。このテーブルビューを使って実験を行います。

UITableViewScrollPositionTopを指定してスクロール
section0, row0section0, row1
section0, row2
section1, row0

UITableViewScrollPositionTopを指定すると、sectionの一番上のrowが指定された場合のみ、そのsectionのsection headerの高さを考慮してスクロールするようになります。

UITableViewScrollPositionMiddleを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1section1, row2

UITableViewScrollPositionMiddleの場合は特にsection header / section footer関係なく、中央に選択された行が来るようにスクロールするようです。

UITableViewScrollPositionBottomを指定してスクロール
section0, row0section0, row1section0, row2
section1, row0section1, row1
section1, row2

UITableViewScrollPositionBottomを指定すると、sectionの一番下のrowが指定された場合のみ、そのsectionのsection footerの高さを考慮してスクロールするようになります。

2011年8月5日金曜日

自分流 View Controllerの作り方 その2



その1はこちら

ぼくのかんがえたさいきょうのせっけいです
主に以下の書籍に影響受けまくりであります
0321127420Patterns of Enterprise Application Architecture (Addison-Wesley Signature Series (Fowler))
Martin Fowler
Addison-Wesley Professional 2002-11-05

by G-Tools
4798116831レガシーコード改善ガイド (Object Oriented SELECTION)
マイケル・C・フェザーズ ウルシステムズ株式会社
翔泳社 2009-07-14

by G-Tools


図を適当に補足
ViewWrapperは既存のすでにあるどうしようもない設計のViewを何とか救いたいときに非常に便利、Wraper / Decoratorパターンを適用してボタンのタップを奪い取ってViewHelperに流すみたいな役目をする
ViewHelperは簡単に言うならUITableViewControllerのdelegate, datasourceだけを担うオブジェクトみたいな感じ。要するにView専用のドメインロジックを書くオブジェクト
Viewの表示を制御するドメインロジックが途方もなく大きくてViewControllerに納めるのが不可能になってしまったときに超便利

Serviceは準ドメインロジックだと思っている、たいていの場合セミシングルトンみたいにする(通信が絡むので複数画面をまたいで使うことがほとんど)
Androidの場合はここ、普通にServiceクラスでいいんじゃないでしょうかね

Managerはドメインロジックというよりもプロセス外へのリソースアクセスを行うためのクラスというイメージ、個人的にはゲートウェイみたいな感じ
  • NSFileManager
  • NSUserDefaults
  • KeychainAccessManager
  • InAppPurchaseManager
  • APIConnectionManager
Modelはドメインモデルです、ほとんどの場合はCoreDataのNSManagedObject。用がないときでもCoreData使っておけ(超便利)
と思ってましたがCoreDataのモデルは特にN:N関係を正しく扱わないと簡単に問題が発生してしまいますので、安易に採用すると危険かも。

Task, Operationってのは非同期で実行されていく特別なドメインロジックのイメージ。要するにAPI通信みたいなもんです
API通信を複数束ねて使ったりとか並列実行したりとかの制御が絶対必要になるのでそういうときに使う via @monjudoh
// MyTaskが終わったらMySuperTaskを実行して、それが終わったらさらにMySuperDuperTaskを連続して実行したい
// 終わったらselfに通知させたい
Task *root = [[[MyTask alloc] init] autorelease];
root.nextTask = [[[MySuperTask alloc] init] autorelease];
root.nextTask.nextTask = [[[MySuperDuperTask alloc] initWithId:100] autorelease];
root.delegate = self;
[root start];

自分流 View Controllerの作り方 その1



その2はこちら

以前勉強会の際に発表した View Controller の作り方のメモをまとめてみました。あくまでメモなので中身はうまくまとまっていませんが、何かのご参考になればと思います。




通信が絡んでくると、たいていの人がやりがちな問題(実例)
  • API通信のレスポンスを処理するコードがViewControllerの中に入っている
  • API通信が3種類必要で、Aを実行したあとにBとCを実行しなければならないとか
  • ABCのレスポンスJSONのパースまでViewControllerでやっている
  • というかAPIの呼び出しの組み立てだとかURLの指定だとか自体がIBActionの中に入っていたりする
API通信だけじゃなくてIn App Purchaseなどでも同様の事例が見られる

それに対する対応策。そもそもなぜこのような問題が発生するのか?
  1. Outletの生成・更新・レイアウトが分離されていない
    • そのため複数回画面が更新されるタイミングが発生するととたんに破綻する
    • 大変よく見かける初心者コードが"drawXXX"という名前のOutletを生成してデータをセットしてframeまでセットして画面に配置するコード
    • Outletを描画コードと勘違いしている。Outletはペンやブラシに相当するものであって、実際に線を書くコードではない
    • この初心者コードでも動く唯一の理由は画面が一回(viewWillAppear時)しか更新されないから
  2. 通信という(比較的大きくなりがちな)ドメインロジックがViewControllerに混入している
そこで問題1.に対応するためにViewControllerの中でやる作業を以下のように分割する
  • Outletを生成する
    • preload(一度に生成する方法)
    • lazy load(必要になったら生成し、必要でなくなったら捨てる方法)
  • Outletのプロパティを更新する
  • Outletをレイアウトする
これらはそれぞれ(基本的に)以下のようなUIViewControllerのメソッドが対応する
  • loadView
    • Outletをpreloadする場合はコレで全く問題ない。このとき、self.viewとここで作られたOutletの生存期間は等しくなる
    • Outletをlazy loadする場合はOutletを生成するコードと削除するコードを用意しておいて、必要なタイミングで呼び出すとかする
  • なし
    • プロパティを更新するために、たとえばupdateOutletsみたいなメソッドを自分で用意してやる
  • 各種willRotate...系メソッド
    • willRotateほげほげの中にレイアウトコードを入れておくと自動的に画面の回転にも対応できて超便利
    • 自動的に必要なタイミングで適切に呼び出ししてくれて超便利
    • そういうのが嫌いな人はlayoutOutletsForInterfaceOrientation:みたいなメソッドでも作ればいいんじゃないでしょうか
次に問題2.に対応するためにAPIの呼び出しやファイルアクセスなどはService, Managerなどの層を作ってそちらに任せる
決してViewControllerの中にドメインロジックを混入させないのが大事
→混入するとドメインロジックとビューナビゲーションロジックが混ざって大爆発する
→さらにドメインロジック自身も複雑な通信が必要になると大爆発する

それとは別にイベントを受け取ってViewの状態を制御する大事なお仕事をする必要がある
  • ボタンタップしたり
  • 画面をタップしたりパンしたり
  • スクロールが発生したり
  • APIコールが完了したり
  • In App Purchaseが完了したり
ここまでがView Controllerのお仕事。決してドメインロジックを混ぜないのがポイント

2011年5月29日日曜日

iPhone iPad 向けスタイラスで書き味勝負をしてみた 2回戦


写真は左から順に、 Pogo SketchoStylus 初期限定生産型パワーサポート スマートペン PBJ-9XシリーズBamboo Stylus

iPhone/iPad向けのスタイラスがいくつも発売されていますが、実際にスタイラスを使ったらどんな具合なの?指でいいんじゃないの?と疑問をお持ちの方と、 私のようなスタイラスマニアの方 のために、iPhone/iPad向けのスタイラスの書き味を実際に書いてみて試してみるという企画です。

前回の記事はこちら: http://akisute.com/2011/02/iphone-ipad.html
おすすめスタイラスまとめ記事はこちら: http://akisute.com/2010/06/ipad.html
Bamboo Stylusのレビューはこちら: http://akisute.com/2011/05/ipad-bamboo-stylus.html
oStylusのレビューはこちら: http://akisute.com/2010/10/ipad-ostylus.html


■比較方法

iPad上で実際にスタイラスでいろいろ書いてみて、書き味を見て比較します。
  • 使用するiPadは iPad 2 Black 16GB (WiFi+3G GSM)
    • 表面に保護フィルムは貼っていません
    • 保護フィルムがある場合、保護フィルムのためにペン先の滑りおよび感度が変化するため、結果が異なってくる可能性があります。
    • 一般的に、保護フィルムがある方が感度が悪くなります。滑りはフィルムの材質により変化します。
  • 使用するアプリは Noteshelf
    • 無地ノート
    • 黒インク
    • ペン先10pt
    • 拡大は使用しない
  • すべて一発勝負
    • undoなし
    • 書き損じたり、ペン先の感度が落ちて線が飛んでもやり直さない。それは感度が実際に悪いという証拠になるため。

■結果

書いてみたノートをpdfにして出力してみました。以下のURLからご覧になれます。
https://docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxha2lzdXRlc2FtYXxneDo0OTI3MWU0MjRlMzI2ZjNm

複数同じ文言が並んでいるページは、全て上から順に、
pogo sketch, oStylus, スマートペン, Bamboo Stylus
の順に並んでいます。


■考察

以下、いくつかのページをピックアップして考察。


pogo sketchは第一世代のスポンジ式ペン先であるため、やはりペン先感度が悪く線が飛びまくります。線自体は自然に書けているのですが。


oStylusは感度はよいものの、いかんせん書きづらい。曲線が特に不自然です。しかも書くのに時間がかかります。多分一番時間がかかりました。


スマートペンはさすがの高性能。最高の感度と軽い軸の恩恵で、一番楽に書けます。ただし線自体は細かいところが弱いです。



今回新顔のBamboo Stylus。ペンが重く、スマートペンより筆圧が必要なため疲れますが、スピードのある細かい線が魅力。一番速く書けたのもこのペンです。


並べてみるとこんな感じです。上からpogo sketch, oStylus, スマートペン, Bamboo Stylus。良いと思ったところには緑○を、悪いと思ったところには赤×を引いています。
pogoは線が飛びさえしなければ・・・
oStylusはとにかく線をゆっくり引いてしまう。 Noteshelf アプリは Penultimate 同様、線を高速に引けば美しく細くなるように出来ているのですが、見てのとおり全ての線が太くてゆっくり引いてしまっている。Zとかへろへろですよ、もう。
スマートペンとBamboo Stylusは安定して良いですね。高速に美しく書けています。


■まとめ

やはり第二世代のシリコンペン先を装備したスタイラスが圧倒的に良いです。その中でも自分の好みや用途で選べる次代になってきたのがすごくいいですね。次回はプリンストンテクノロジーの専用タッチペンやAcase Stylusなんかも加えてレビューしてみたいです。

iPad 用スタイラス Bamboo Stylus を試してみた



つい先日いよいよ発売された Bamboo Stylus, 早速買って試してみました。


■Bamboo Stylus とは?

http://wacom.jp/jp/company/news_detail.php?id=355
http://japanese.engadget.com/2011/04/19/bamboo-stylus/

B004XF0FQWWacom iPad/IPad2/iPhone4対応 描画、ポインティングに最適なタッチペン Bamboo Stylus CS-100/K0
ワコム 2011-05-27

by G-Tools

PC向けペンタブレットではおそらく業界最大手の Wacom が、満を持して投入してきた静電誘導式タッチディスプレイ用のスタイラスペンです。ペンタブレットの開発でおそらくこの手のスタイラスペンに関しては相当なノウハウがあるのではないかと思われ、非常に期待が持てます。プレスリリースで、特に以下のような点が既存のスタイラスとは異なると強調されています。
  • 手に持った時の心地よさと書きやすさを両立する重さと重心のバランス。
  • 上質なサテン調のブラックとシルバー色を採用し、高い質感を実現。
  • ペン先は主な他社製品よりも直径が約25%小さく、より自然で直感的な描画が可能。
  • スムーズで快適な書き心地を実現するペン先の実現。(磨耗した際には交換が可能)
  • iPadカバーに留めたり、ポケットに留めたりするなど、携帯や保管にも便利なクリップ。(脱着可能)

■最初に結論

良い点
  • 第二世代のシリコンペン先を採用している他のスタイラスペンと比べ、細くて滑りが良くしっかりとしたペン先。
  • サイズの割に軸がやや太めで、しっかりと握ることができる。
  • 上記の理由により、実物のペンと非常に近い筆書感覚が得られる。
  • クリップが丈夫で脱着可能。取り外してストラップを代わりにつけることもできる。
悪い点
  • 同サイズのスタイラスペンの中では圧倒的に重く、書く際に手が疲れる。
  • 第二世代のシリコンペン先を採用している他のスタイラスペンと比べ、感度がやや悪い。
  • 3000円という、やや高価な値段設定。

■外見、持った感触

まずは外見から。値段設定がやや高めで、かつBambooブランドの名前を冠していることからもわかるとおり、iPad向けスタイラスとしてはハイエンドを狙っている物と思われます。そのためデザインにも力が入っており、表面がiPadやMacのように梨地加工されています。単なる金属の棒という体の パワーサポート スマートペンプリンストンテクノロジー 専用タッチペン に比べ断然高級感があり美しく感じられます。

長さは12cmということで、他社のスタイラスと比べても数ミリ長い程度で標準的だと思います。軸は他社の製品に比べてやや太く、良くコンビニなどで売っているボールペンよりわずかに細い程度になっています。そのため手に持った際の感覚が非常にペンらしいです。また、重さが非常に特徴的で、他のスタイラスと比べて明らかに密度のある重さがあります。体感だと パワーサポート スマートペン より二倍近く重いように感じます。

上部とペン先はねじになっていて、それぞれクリップとペン先が取り外せるようになっており、メンテナンスのことを考えた作りになっているのが心憎いです。消耗品ではなく、長期間使わせる物という印象を受けます。また、取り外した上部のクリップの部分にストラップをつけて持ち歩くことも出来そうです。


長さ比較。左から右に短い物順。pogo sketch, スマートペン, Bamboo Stylus, oStylus


太さ比較。先ほどと同じ順序。pogo sketch, スマートペン, Bamboo Stylus, oStylus


■動作、書き味

買って最初に一筆書いてみた時にすぐ気づいたのが、まず感度が悪いです。ほとんど載せるだけで反応する超感度の パワーサポート スマートペン に対して、ほんの少しだけ力を込めないと反応しません。しかしながらペン先が細くてしっかりしており、iPadの表面を非常になめらかに滑ります。滑りがよいので実際に紙で書いているよりちょっと違う感じがしますが、ペンタブレットに慣れている人にはちょうどいいぐらいだと思います。先端がふにゃふにゃしてやや抵抗がある滑りの パワーサポート スマートペン とは対照的な印象です。

個人的には長時間書いていると疲れます。感度が悪く抵抗が弱いのと重いペンが相まって、腕の力を使うためだと思います。


実際に Noteshelf アプリ上で書いてみました。実際のペン同様、非常になめらかに書けています。


■まとめ

WacomとしてはiPad向けスタイラスのハイエンドモデルとして位置づけているこの商品ですが、実際に使ってみるとむしろ パワーサポート スマートペン と対になるような感じの印象を受けました。すなわち、以下のように棲み分けられます。

Bamboo Stylus をオススメする人
  • ペンに近い持ち味、書き味が欲しい
  • 細くてよく滑るペン先が何より欲しい!
  • 値段は高くても気にしない
パワーサポート スマートペン をオススメする人
  • 軽くて、書いていて疲れないペンが欲しい
  • 感度の良くて、実際のペンみたいに少し抵抗がある滑りのペン先が何より欲しい!
  • 値段は安い方がいい、または初めてスタイラスを試してみるので失敗しても後悔しない値段の方がいい
私自身はどっちを選ぼうか悩んでますが、せっかく買ったし、しばらくはBamboo Stylusを使ってみようかと考えてます。

2011年2月4日金曜日

TestFlight 経由のアプリのダウンロード速度を調べてみた




最近 iOS 界隈を賑わせている TestFlight というWebサービスがあります。詳細についてはまぁ TechCrunch さんでも読んでいただくとしまして、この TestFlight が非常に便利そうなので私のほうでも早速試してます。登録も簡単、アプリのマネジメントも良くできていますし、チームを複数用意して管理できるというのも良いです。なんで Apple がデフォルトでこれを用意してくれないのかというぐらい便利です。

が、一つだけ気になったことがあって、まぁタイトル通りなのですが、TestFlight経由だとアプリのダウンロードが遅いのではないか?ということです。ということで実際にやってみました。


■条件

対象のアプリのサイズは17.7MB。
以下の2カ所の回線で計測しました。
  • 会社の回線(確かバッファロー, IEEE 802.11g, だいたい300KB/s程度)
  • 自宅の回線(AirMac Extreme, IEEE 802.11n, だいたい1MB/s程度)

■結果

会社の回線・・・ダウンロードに12分~15分程度
自宅の回線・・・ダウンロードに1分前後、インストール完了まで2分ちょっと

それぞれ別の日に計測したので何とも言えませんし、会社の回線は自分以外の人も使うので遅いときがあるのですが、およそ

遅いとき・・・20KB/s程度
早いとき・・・300KB/s程度

とまぁずいぶん幅がある結果になってしまいました。どうやら時間帯や相手のサーバー側の混雑具合によっても速度が違ってくるようです。いずれにせよそんな猛烈に速いわけではないので注意が必要そうです。たとえば100MBぐらいあるゲームアプリをテストしたいときなどは、今まで通りの地味な配布のほうがよい・・・ということもあるかも。


■余談:TestFlightってどれぐらいのファイルサイズまでアップロードできるのか?

と思って調べてみたらありました。500MBだそうです。400MBのファイルを実際にアップした人がいるみたいです。
http://support.testflightapp.com/discussions/questions/60-max-file-size-on-build-upload

2011年2月2日水曜日

"Failed to launch simulated application: Unknown error." が発生したときの対処法



iPhone シミュレーター起動時に表題のエラーが発生したときの対処法です。


表題のエラーは文字通り原因不明の問題が発生したときに表示されるのですが、問題のうちの一つに「バンドルリソース内にMac OSが使用する予約されたリソース名が含まれていると発生する」というものがあります。

たとえば
Contents
Resources
などの名前のディレクトリやファイルをバンドルリソース内に作成してしまうと、ビルドは通るのですがシミュレーター実行時にエラーが発生するようです。これらの予約された名前は使わないようにすると良いです。

2011年2月1日火曜日

iPhone iPad 向けスタイラスで書き味勝負をしてみた


写真は上から順に、 パワーサポート スマートペン PBJ-9XシリーズPogo SketchoStylus 初期限定生産型


2回戦の記事はこちら: http://akisute.com/2011/05/iphone-ipad-2.html
おすすめスタイラスまとめ記事はこちら: http://akisute.com/2010/06/ipad.html
Bamboo Stylusのレビューはこちら: http://akisute.com/2011/05/ipad-bamboo-stylus.html
oStylusのレビューはこちら: http://akisute.com/2010/10/ipad-ostylus.html

このたび自分が納得できる完成度の iPad 向けスタイラスが3種類揃ったのを機会に、ちょっと書きくらべをしてみる事にしました。


■さっそく試してみた

以下、実際に手書きで字を書いてみたスクリーンショットを貼り付けます。全て一発勝負で書いています。それぞれ上からスマートペン、Pogo Sketch、oStylusです。

neu.Notes



Penultimate




■考察

スマートペンは本当にまるで普通のペンで紙に書いているように書けます。特に「返」のしんにょうあたりを見るとよくわかるのですが、このような綺麗な曲線が非常に楽に書けます。ただしやや太いためか細かい筆跡が難しいです。

Pogo Sketchはスマートペンより細く、やや滑りが悪いため細かい線に強いですし、曲線もしなやかです。しかしながらよく見ると所々線が途切れていたり短かったりすることがわかると思います。これはタッチの感度が悪いためで、タッチを認識せず線が途切れてしまっているためこのようになってしまっています。

oStylusは感度の良い金属製のペン先とペン先が見えるOリング構造のおかげで、細かい字に圧倒的な強さを誇ります。Pogo Sketchのように感度が足りず線が途切れることもありません。弱点として、全体的に長く重くペン先が平たいため、普通のペンと同じようには書けず、線が直線的になってしまっています。

一番書くのが楽だったのはスマートペン、書くのに時間がかかっているのはoStylusだったと思います。


■まとめ

それぞれ一長一短、特徴ある iPad / iPhone 向けのスタイラスが市場に出回るようになってきて、すごくわくわくする感じです!つい1年2年前まではろくな商品がないと嘆いていたのですが、いまや自分の書きたい物に応じてスタイラスを選ぶことさえできるようになりました。素敵!

2010年12月29日水曜日

はじめての iPhone 開発時にお勧めの本 3 冊

2010/12/29追加: 本を追加したり中身を最新のものにしたりしました。

@monjudoh に社内チャットで
[10/09/17 17:18:12] 文 殊堂: heyあきすて
[10/09/17 17:18:32] 文 殊堂: iPhoneアプリ開発でいい本とかある?
[10/09/17 17:19:42] akisute: yoじゅどうもん
[10/09/17 17:19:43] akisute: たくさんあるぜ
[10/09/17 17:19:52] wozozo ☿: yo
[10/09/17 17:19:53] akisute: なんかみんなから同じ質問されるからブログに書くわ
とか言われたので、お勧め本をまとめてみることにしました。2年前にも似たようなものを書いたような気がしますけど、2年が経過していますから、いろいろと状況も変化していますしね。

4797361786詳解 Objective-C 2.0 改訂版
荻原 剛志
ソフトバンククリエイティブ 2010-12-17

by G-Tools


2年前にも詳解しましたがこいつはバイブルです。改訂版になってBlockがらみの記述が増えたり、iOSについての言及が増えて、さらに良書になりました。四の五の言わずに買っておくと良いです。ただし、どちらかというとObjective-Cに焦点を置いている本であり、iOSで何ができるか、ということについてはほとんど記載がないので注意してください。

4797358106はじめてのiPhone3プログラミング
Dave Mark Jeff LaMarche 鮎川 不二雄
ソフトバンククリエイティブ 2009-12-17

by G-Tools


こちらは iOS のバイブルです。初めて挑戦する人に最適ですが、ある程度熟練した人が見ても発見がある良書です。およそ必要なことはすべて得られると思います。

4897978440iPhoneプログラミングUIKit詳解リファレンス
所 友太 京セラコミュニケーションシステム株式会社
リックテレコム 2010-01-12

by G-Tools


UIKitに焦点を置いて書かれている本です。図解が多く、全くiOS開発をしたことがない人でもどのようなUI要素が使えるのか見て学べるため大変おすすめです。