Pages

2011年11月25日金曜日

iOS5とNSOperation

NSOperationQueueとNSOperationとNSURLConnectionを使って非同期に画像やデータをダウンロードするという処理については、いろいろなところで紹介されていたりして結構メジャーな処理の一つだと思います。

御多分に漏れず私もこの手法の常習者のひとりであります。
この処理を使うアプリもいくつか開発した経験があります。

この処理ではNSOperationのサブクラスを作って各メソッドをオーバーライドするわけですが、cancelの中では、だいたい下のコードのようにプロパティを書き換えるのが普通じゃないでしょうか?

- (void)cancel {
  .....
  [self setValue:[NSNumber numberWithBool:YES] forKey:@"isFinished"];
  .....
}


ところが、これ、iOS5では[MyOperation isFinished = YES without being started by the queue it is in]なんてメッセージを吐きよる。

つまりは、startメソッドが実行されていないのにisFinishedをYESに設定するとは何事だ!と怒られてしまったわけです。iOS4までではこんなことで怒られることはありませんでした。まぁ、そう言われればそうやなって気もしないではないですが・・・

特に、[queue cancelAllOperations]とかを実行して大量の非同期オペレーションをキャンセルする場合なんかは大量にこのメッセージが出ます。しかも、[An instance 0xxxxxxxxx of class MyOperation is being deallocated while key value observers are still registered with it]とかいうメッセージも吐きだされて、これが直接的に関係しているかどうかは不明ですがアプリが落ちてしまうこともしばしば。

ではメッセージを回避しようということで、cancelメソッドの中を以下のように書きえてみました。



はい。メッセージは吐きだされなくなりましたが、今度はNSOperationがNSOperationQueueから削除されずに残り続ける結果に。

う~ん。どうしたものか。正しいお作法が見つかるまでは、とりあえず自作の疑似キューを作成して非同期通信キューの処理を実現することにしました。

松竹芸能 不公平ワリカン リリース

松竹芸能 不公平ワリカン」というiPhoneアプリをリリースしました。
11月25日からAppStoreで購入可能です。

http://itunes.apple.com/jp/app/id480923725?mt=8

ゲーム感覚の割り勘アプリです。
ただの割り勘はつまらないんで、ちょっとスリリングに不公平な割り勘が楽しめるアプリ。

これ、僕の中ではかなり前から案があってアプリ化したかったんですが、どうやったらより楽しくなるかなっていうのが悩みの種でした。

堅いのもいややし、ギャグにするとしてもデザインとかがキャッチーな方がいいし・・・とかいろいろ考えて長い時間が経ってしまいました。

そんな中、ひょんなことから松竹芸能さんと一緒にできるチャンスが訪れまして、とてもラッキーでした。

ということで、よかったらどうぞ。

2011年11月16日水曜日

iOS5のステータスバーどうなってんの?

以前に作ったiPhoneアプリをiOS5で実行してみたら「あれっ???」って思う事がありました。

半透明に設定したステータスバーの後ろの描画がiOS4のときと見え方が違うのです。1ピクセル下にずれてるのです。

どういうことなんだってことで簡単な実験をしてみました。

まずは、ステータスバーをUIStatusBarStyleBlackTranslucentで黒の半透明に設定してビューをウィンドウいっぱい(320x480)に表示させてビューの描画ルーチンを以下のように記述します。

- (void)drawRect:(CGRect)rect {
  CGContextRef context = UIGraphicsGetCurrentContext();

  CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 0.5);
  CGContextFillRect(context, CGRectMake( 80.0,  0.0, 16.0, 20.0));
  CGContextFillRect(context, CGRectMake(100.0,  0.0, 16.0, 19.0));
  CGContextFillRect(context, CGRectMake(120.0, 20.0, 16.0, 30.0));
}


まずは、iPhone 4.3 Simulatorで実行します。
結果は以下の通り。予想通りです。


で、次にiPhone 5.0 Simulatorで実行してみます。
結果は以下の通り。


おいおい、明らかに隙間が空いてるやないか!

で色々、書き加えたりしているうちに再描画(setNeedDisplay)を実行したらなぜか普通の状態に変わるということが分かりました。他にもきっかけはあるかもしれませんが。

とりあえず、ボタンを付けてボタンのセレクタで再描画を実行するようにしてみました。

そのときの動きがこれ。※ちょっと分かりにくくてすいません。



動いてる〜。
再描画後は正常というか想定していた状態になりました。


2011年11月13日日曜日

CoreAnimation CATransaction

CoreAnimationで、CATransactionを使う方法はシンプルで手軽なんで結構楽しめますよね。

もう識者の方にとっては当たり前で知ってることだとは思いますが、自分なりに簡単な実験を行ってみたので、ここに書き留めておきます。

とりあえず、100x100の青背景のCALayerを作ってアニメーションさせます。
アニメーションの開始はviewDidAppearとします。

まずは、5秒かけてゆっくり左上から右下にレイヤーを移動させながら、最初の1秒間でレイヤーの透明度を変更するという二つのアニメーションの組み合わせのパターン。
これは、CATransactionをネストすることで可能です。

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

  [CATransaction begin];
  [CATransaction setAnimationDuration:5.0];
  animLayer.frame = CGRectMake(200.0, 340.0, 100.0, 100.0);
  [CATransaction begin];
  [CATransaction setAnimationDuration:1.0];
  animLayer.opacity = 0.3;
  [CATransaction commit];
  [CATransaction commit];
}


これをiPhoneシミュレータで実行させたらこうなります。


では、この透明度を変更するのがアニメーションの開始から3秒後に始めたい。こんな場合はどうするのってことで、以下のようなコードを書いてみました。タイマーを使用します。

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

  [CATransaction begin];
  [CATransaction setAnimationDuration:5.0];
  animLayer.frame = CGRectMake(200.0, 340.0, 100.0, 100.0);
  [CATransaction commit];
  //
  [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(onTimer:) userInfo:nil repeats:NO];
}

- (void)onTimer:(NSTimer*)timer {
  [CATransaction begin];
  [CATransaction setAnimationDuration:1.0];
  animLayer.opacity = 0.3;
  [CATransaction commit];
}




CATransactionはあとから実行しても追加して複数のアニメーションを組み合わせて実行くれるみたいですね。
では、こんなことしてみたらどうでしょうか?

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

  [CATransaction begin];
  [CATransaction setAnimationDuration:5.0];
  animLayer.frame = CGRectMake(200.0, 340.0, 100.0, 100.0);
  [CATransaction commit];
  //
  [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(onTimer:) userInfo:nil repeats:NO];
}

- (void)onTimer:(NSTimer*)timer {
  [CATransaction begin];
  [CATransaction setAnimationDuration:1.0];
  animLayer.frame = CGRectMake(20.0, 20.0, 100.0, 100.0);
  [CATransaction commit];
}



まぁ当然そうなりますよね。
CABasicAnimationでいうところのanimationWithKeyPathのkeyPathが同じものを重ねて追加すると上書きというか前のものが取り消されて後から追加したアニメーションが実行されます。

で、途中でアニメーションを中止させるには以下のようにすれば可能です。

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

  [CATransaction begin];
  [CATransaction setAnimationDuration:5.0];
  animLayer.frame = CGRectMake(200.0, 340.0, 100.0, 100.0);
  [CATransaction commit];
  //
  [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(onTimer:) userInfo:nil repeats:NO];
}

- (void)onTimer:(NSTimer*)timer {
  [animLayer removeAllAnimations];
}


2011年11月7日月曜日

Xcode 4.2 ARCの挙動を少し調べてみました

Xcode4.2から採用されたARC(Automatic Reference Counting)ですが、
ちょっと「おっ!」と思ったところがあったので簡単なテストをしてみました。

まずは、簡単なプロジェクトを作成します。
Xcode4.2で言うところのEmpty Applicationでプロジェクトを作成します。
とりあえずは、Use Automatic Reference Countingのチェックを外しておきます。


iOS4の挙動も試したいので、iOS Deployment Targetは、4.2としておきます。


今回は、超簡単なナビゲーションベースのアプリを作りますので、
RootViewControllerというビューコントローラを一つだけ作成します。
xibファイルも一緒に。


ビューコントローラの中身は特に何も配置しませんが、寂しいので背景だけ設定しておきます。


AppDelegateのdidFinishLaunchingWithOptionsでRootViewControllerとUINavigationControllerを追加します。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  self.window.backgroundColor = [UIColor whiteColor];
  //
  RootViewController* root = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil];
  UINavigationController* navi = [[UINavigationController alloc] initWithRootViewController:root];
  [root release];
  [self.window addSubview:navi.view];
  //
  [self.window makeKeyAndVisible];
  return YES;
}


これ、UINavigationControllerをスタック変数に代入していますので、後々ポインタが行方不明になる悪いコードですが、そこは目をつぶってくださいね。

それから、RootViewController.mに次のように記述して、ViewDidLoadとその1秒後にログを出力させます。ViewDidLoadはAppDelegateのdidFinishLaunchingWithOptionsと同じRunLoop内で処理されているかもしれませんので、そこを抜けて、その後のRunLoopから呼び出されるように1秒後にも表示させるようにしています。

- (void)viewDidLoad {
  [super viewDidLoad];
  NSLog(@"ViewDidLoad: %@", self.navigationController);
  [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimer:) userInfo:nil repeats:NO];
}

- (void)onTimer:(NSTimer *)timer {
  NSLog(@"onTimer: %@", self.navigationController);
}


これで、iPhone 5.0 Simulator、iPhone 4.3 Simulator、どちらで実行させても普通にログを出力します。

2011-11-07 16:30:45.435 ARCtest[21675:10103] ViewDidLoad: <UINavigationController: 0x6a5e0d0>
2011-11-07 16:30:46.438 ARCtest[21675:10103] onTimer: <UINavigationController: 0x6a5e0d0>


次に、いよいよARCを有効にしてみます。


で、先ほどのコードでreleaseを使っていたので、少しだけ変更します。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  self.window.backgroundColor = [UIColor whiteColor];
  //
  RootViewController* root = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil];
  UINavigationController* navi = [[UINavigationController alloc] initWithRootViewController:root];
  [self.window addSubview:navi.view];
  //
  [self.window makeKeyAndVisible];
  return YES;
}

iPhone 5.0 Simulatorで実行した場合のログは次のようになります。

2011-11-07 16:49:12.542 ARCtest[21848:10103] ViewDidLoad: <UINavigationController: 0x6c94e30>
2011-11-07 16:49:13.544 ARCtest[21848:10103] onTimer: (null)


これは予想通り。こうなるような気がしてました。

そして、iPhone 4.3 Simulatorで実行した場合には、そもそもRootViewController自体が表示されません。

iOS4とiOS5では、ARCで自動リリースするタイミングが違うのか、実際にビューを表示するタイミングが異なっているということなのでしょうか。

まぁ、あんまり深く掘り下げるつもりはありませんので、この辺で止めときます。
ARCでの正しい記述は次の通りです。

@implementation AppDelegate {
  UINavigationController* navi;
}

としてクラスのプロパティとして宣言しておきます。
AppDelegateのdidFinishLaunchingWithOptionsは、ちょっとだけ変更されて下のようになります。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  self.window.backgroundColor = [UIColor whiteColor];
  //
  RootViewController* root = [[RootViewController alloc] initWithNibName:@"RootViewController" bundle:nil];
  navi = [[UINavigationController alloc] initWithRootViewController:root];
  [self.window addSubview:navi.view];
  //
  [self.window makeKeyAndVisible];
  return YES;
}

iOS開発の準備

お客様のお手伝いで、iOS Dev Centerで証明書の登録などの作業をやりました。
過去に2回ほどやっているのですが時間が立って忘れてる部分も結構ありました。

今後、またお手伝いの機会があるかもしれませんので
思い立ったが吉日、自分メモとして書き留めておきます。

まず、Macでユーティリティのキーチェーンアクセスを起動します。


上の図のようにメニューから
キーチェーンアクセス>証明書アシスタント>認証局に証明書を要求
を選択します。


上のようなウィンドウが表示されるので、ユーザのメールアドレスにiOS Dev Centerに登録しているメールアドレス、通称にiOS Dev Centerに登録している氏名を指定します。
ディスクに保存のラジオボタンを選択して、鍵ペア情報を指定にチェックして続けるをクリックします。
このメールアドレスや通称がDev Centerのものと一致していないのかどうかは分かりませんが、なんとなく気持ちの問題で、いつも統一しています。


次のウィンドウでは、デフォルトのまま、鍵のサイズ:2048ビット、アルゴリズム:RSAを選択して続けるをクリックします。

ファイル保存ダイアログが出てきますので適当な場所にファイルを保存します。

次にiOS Dev Centerにアクセスして、iOS Provisioning Portalへ移動します。
左のメニューからCertificatesを選択します。


証明書のところに"You currently do not have a valid certificate"と表示されています。
Request Certificateボタンをクリックします。

進んだ画面で、さきほど作成した証明書ファイルをアップロードします。



何度かちょこちょこページ遷移を繰り返しているうちに(時間かせぎです)
Downloadボタンが表示されているので、ファイルをダウンロードしてダブルクリックしてキーチェーンアクセスに登録します。

これで、Development環境はOK。
Distributionも同じ操作を繰り返し、両方の証明書がキーチェーンアクセスに登録されていることを確認して無事完了。