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

10分でおぼえるBlocksの書き方

2013年7月18日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Blocksが使えるようになってはや数年。未だに書式が覚えられないわたし(あなた)の為に書き方をまとめてみた。

基本


基本型


 戻り値 (^名前) (引数)
(私が勝手に決めた)基本型はこれ。これを覚えないことには始まらない。とにかくこれだけは覚える。
戻り値と引数は一般的なCの関数の書き方と同じなので、名前の書き方だけ (^名前) になったと覚えるのがいい。
(例)BOOL (^completion)(BOOL canceled) = ...;
この基本型を元に各場面でどう使うかを見ていく。

変数宣言・typedef宣言


変数宣言なら
 戻り値 (^名前) (引数) = …;
typedef宣言なら
 typedef 戻り値 (^名前) (引数);
両方とも基本型がそのまま使える。

(例)typedef BOOL (^LKCompletion)(BOOL canceled);

プロパティ宣言


プロパティ宣言で使う時にも基本型がそのまま使える。
 @property (copy) 戻り値 (^名前) (引数)
Blocksをプロパティ宣言する場合は copy にしておくのがポイント。
(例)@property (copy) BOOL (^completion)(BOOL canceled);

メソッド引数


メソッド引数にした時に書き方が若干変わる。形が変わるのが Blocks構文を覚えにくくさせている理由の一つ。
(例)- (void)loadImageCompletion:(BOOL(^)(BOOL canceled))completion;
ただ落ち着いて見るとObjective-C標準のメソッド引数の書式に従って基本型を変形しているだけなので実は難しくない。

もともとメソッド引数は「(型)名前」というスタイルなので基本型をそれに合わせて変形ればいい。
戻り値 (^名前) (引数)  ※基本型
 ↓
 ↓ 名前を後ろへ切り出す ←メソッド引数名は最後にくるから
 ↓
戻り値 (^) (引数)名前
 ↓
 ↓ 引数の型となる範囲を( )で囲う ←メソッド標準の書き方
 ↓
(戻り値 (^) (引数)) 名前
基本型をメソッド引数の書き方に合わせたものだと覚えておくといい。


Blocks定義


 ^戻り値 (引数) {...}
やはりBlocks構文を覚えくくさせている原因の一つ。基本型と書き方が異なる。
(例)BOOL (^display)(BOOL animated) = ^BOOL(BOOL animated) {return YES;};
こちらはメソッド引数の時と異なり変形ルールが想定できない。仕方がないので基本型同様に暗記する。なおメソッド引数にBlocks定義を記述する場合、Xcodeならコーディング時にBlocksの書式を補完してくれる。この為記憶を頼りに記述するケースは実際には多くないかもしれない。

(例)[hoge loadImageCompletion:^BOOL(BOOL canceled) {
    :
   }];


まとめ


こうして見ていくとBlocksの書き方が覚えにくいのには理由があって、書き方のパターンが何種類もあるのが原因だとわかる。逆にそれらが整理できて把握できれば自分で書けるようになる。

最低限覚えるべきは次の2つ。

基本型

 戻り値 (^名前) (引数)

Blocks定義

 ^戻り値(引数) {...}
それ以外は基本型がそのまま使えるのと、メソッド引数の場合は基本型の応用なので変形ルールを知っておけば書式を無理に覚える必要は無い。またBlocks定義を書く時は、多くの場合 Xcodeが補完してくれるので覚えておく必要性は基本型に比べると実は少ない(人によるが)。なので、乱暴に言えば基本型だけ覚えればどうにかなる。

なお「基本型」など、この記事の分類は私が勝手に決めたものなのでご注意を(公式の呼び名ではない)。


応用


voidな引数と戻り値は省略できるケースがある。

引数省略


引数が voidの場合は省略できる。

BOOL (^execute)(void) = …;
 ↓
BOOL (^execute)( ) = …;
- (void)displayScreen:(void(^)(void))didAppear;
 ↓
- (void)displayScreen:(void (^)( ))didAppear;

戻り値省略


Blocks定義の時は戻り値が voidであれば省略できる。
(例)^void (void) { … }
    ↓
   ^(void) { … }
Blocks定義の場合、引数が voidならさらに引数のカッコ ()自体も省略できる。
(例)^{ … }
よく見かける ^{...} は実は ^void(void){...} が省略されたもの。

Blocks定義以外で戻り値voidは省略できないので注意。
○ @property (copy) void (^didAppear)(BOOL animated);
☓ @property (copy) (^didAppear)(BOOL animated);


参考・お役立ち情報


Block Cheat Sheet

Block Declaration Syntax List
Recursive Blocks(再帰ブロック)の書き方の紹介あり。

坂本氏担当の第5章にBlocksの説明あり。GCDと合わせてわかりやすい。


こちらも坂本氏の本。詳細な解説だがそれでいてわかりやすい。ARCの詳説もあるのでかなりおすすめ。私は紙で入手したが電子書籍版もあると思う。



- - - -
ツッコミあればお願いします。間違い等は訂正します。

NSTimer を Blocks で処理する [2]改良版

2010年8月7日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: NSTimer を Blocks で処理する

前回のコードを少し改良してみる。

改良版 Blocks対応NSTimer


前回は userInfo を持回す為に NSMutableDictionary を導入したり、Blocksの引数に userInfo を付けたりしていたが、考えてみると Blocksは定義されているそのレキシカルスコープ内の self やメンバ変数、auto変数が参照できるので userInfo は無くてもいい。そこで userInfo の無い改良版を作ってみた。

まず定義
typedef void (^TIMER_BLOCK__)(NSTimer*);

@interface NSTimer (Extension)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
 block:(TIMER_BLOCK__)block repeats:(BOOL)repeats;

@end

実装
+ (void)executeBlock__:(NSTimer*)timer
{
 if (![timer isValid]) {
  return;
  // do nothing
 }
 
 TIMER_BLOCK__ block = [timer userInfo];
 block(timer);
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
 block:(TIMER_BLOCK__)block repeats:(BOOL)repeats
{
 NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:seconds
     target:self
   selector:@selector(executeBlock__:)
   userInfo:[[block copy] autorelease]
    repeats:repeats];
 return timer;
 
}
userInfo の持回しが無くなったので、ものすごいシンプルになった。


サンプルによる検証


前回のサンプルを書き換えて上記メソッドを使うようにした。

※ソースコードは GitHubからどうぞ。
NSTimerBlocks at 2010-08-07.2 from xcatsan's iOS-Sample-Code - GitHub

タイマー利用部分はこんな感じ。
- (void)viewDidLoad {
    [super viewDidLoad];
 
 NSArray* array = [NSArray arrayWithObjects:@"DOG", @"CAT", @"MONKEY", nil];

 timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0
      block:^(NSTimer* timer) {
       self.counterLabel1.text =
     [NSString stringWithFormat:@"%d", counter1++];
       NSLog(@"%@", array);
      }
    repeats:YES];

 timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0
      block:^(NSTimer* timer) {
       self.counterLabel2.text =
     [NSString stringWithFormat:@"%d", counter2];
       counter2 += 10;
       NSLog(@"%@", array);
      }
    repeats:YES];

 timer3 = [NSTimer scheduledTimerWithTimeInterval:3.0
      block:^(NSTimer* timer) {
       self.counterLabel3.text =
     [NSString stringWithFormat:@"%d", counter3];
       counter3 += 100;
       NSLog(@"%@", array);
      }
    repeats:YES];
前回と比べると userInfo が無くなった。また auto変数 array への参照を Block内に記述してある。実行すると NSLog出力で array の内容が参照出来ていることがわかる。
NSTimerBlocks[4485:207] (
    DOG,
    CAT,
    MONKEY
)


block内でのauto変数参照について


これまで見てきた通り、block内ではその外側で定義されている auto変数への参照も可能である。blockがどのようにして auto変数を扱っているかデバッガで調べてみた。

+[NSTimer executeBlock__] が呼び出された時の引数 block の内容は次の通り。

block のメンバ変数に self と array が存在することがわかる。恐らくコンパイラが blockクラスを定義するときに参照している変数を、blockのメンバ変数として定義するのだと思われる。ただ以前の調査では、blockがスタック上にある場合はスコープを外れるとデバッガ上でこれらのメンバ変数が消えていたことが確認できているので、追加・除去は動的に行われるようだ。

NSTimer を Blocks で処理する

2010年8月6日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

調子に乗って今度は NSTimer に Blocks を導入してみた。

利用イメージはこんな感じ。
[NSTimer scheduledTimerWithTimeInterval:1.0
   block:^(NSTimer* timer, id userInfo) {
    self.counterLabel.text =
   [NSString stringWithFormat:@"%d", counter++];
   }
 userInfo:nil
 repeats:YES];


実装


NSTimer のカテゴリとして実装した。NSTimer 発火時に呼ばれるメソッドを受け取るインスタンスが必要になるが、今回は NSTimerクラスとした(つまりクラスメソッドを呼び出させる)。このあたりは少々違和感はあるが、一番簡単だと思われるのでそうした。

まずはヘッダから。
typedef void (^TIMER_BLOCK__)(NSTimer*, id);

@interface NSTimer (Extension)

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
 block:(TIMER_BLOCK__)block userInfo:(id)userInfo repeats:(BOOL)repeats;
@end
ブロックの定義は何箇所かで使うので typedef で定義してみた。

実装。
#define KEY_BLOCK  @"block"
#define KEY_USERINFO @"userInfo"

+ (void)executeBlock__:(NSTimer*)timer
{
 if (![timer isValid]) {
  return;
  // do nothing
 }

 NSDictionary* context = [timer userInfo];
 TIMER_BLOCK__ block = [context objectForKey:KEY_BLOCK];
 id userInfo = [context objectForKey:KEY_USERINFO];

 block(timer, userInfo);
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
 block:(TIMER_BLOCK__)block userInfo:(id)userInfo repeats:(BOOL)repeats
{
 NSMutableDictionary* context = [NSMutableDictionary dictionary];

 [context setObject:[[block copy] autorelease]
    forKey:KEY_BLOCK];
 if (userInfo) {
  [context setObject:userInfo forKey:KEY_USERINFO];
 }

 NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:seconds
    target:self
  selector:@selector(executeBlock__:)
  userInfo:context
  repeats:repeats];
 return timer;
}
ポイントは userInfo を NSDictionary へ入れ替え、そこへ渡ってきた blockを詰めていること。これを発火時(executeBlock__:)に取り出して実行している。block の引数として NSTimer の他、userInfo を渡しているが、これは NSDictionary への入れ替えを行うので -[NSTimer userInfo] が使えないため。

block は copy すると同時に autorelease をかけている。NSTimer のインスタンスが破棄されるタイミングで (NSMutableDictionary*)contextも開放されると思うので、autorelease をかけておけば同時に block も開放されると思われる(多分)。

利用側はこんな感じ。
[NSTimer scheduledTimerWithTimeInterval:1.0
   block:^(NSTimer* timer, id userInfo) {
    self.counterLabel.text =
   [NSString stringWithFormat:@"%d", counter++];
   }
 userInfo:nil
 repeats:YES];

サンプル


NSTimerを使って数字をカウントアップするサンプルを作ってみた。異なる周期を持つ3つのタイマー(1秒、2秒、3秒)を作り、画面上の数字をカウントアップする。

ちゃんと動いているようだ。

コードはこんな感じ。
timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0
   block:^(NSTimer* timer, id userInfo) {
    self.counterLabel1.text =
   [NSString stringWithFormat:@"%d", counter1++];
   }
 userInfo:nil
 repeats:YES];

timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0
   block:^(NSTimer* timer, id userInfo) {
    self.counterLabel2.text =
    [NSString stringWithFormat:@"%d", counter2];
    counter2 += 10;
   }
   userInfo:nil
 repeats:YES];

timer3 = [NSTimer scheduledTimerWithTimeInterval:3.0
   block:^(NSTimer* timer, id userInfo) {
    self.counterLabel3.text =
    [NSString stringWithFormat:@"%d", counter3];
    counter3 += 100;
   }
   userInfo:nil
 repeats:YES];

※ソースコードはGitHubからどうぞ。
NSTimerBlocks at 2010-08-06 from xcatsan's iOS-Sample-Code - GitHub

参考情報


自前で Blocks を実装した他の例など。
Cocoaの日々: 遅延実行を Blocksで記述する
-[NSObject performSelector:afterDelay:] に Blocksを導入してみた。
Cocoaの日々: UIActionSheet を Blocks で処理する
UIActionSheetDelegate を使わず Blocks を使ってボタン押下時の処理を書く。

- - - -
Blocks が使えると、たった一個とはいえメソッドを作らなくて楽。ましてや複数タイマーを使う場合も面倒が無くかなりいい。可読性もよくなる。Blocks便利だ。

(追記) Block内では、self の他、そのBlockのレキシカルスコープで定義された他の auto変数も参照することができる (*)。そう考えると userInfo が不要なので無くすことができる。こんな感じ。
timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0
     block:^(NSTimer* timer) {
      self.counterLabel1.text =
    [NSString stringWithFormat:@"%d", counter1++];
      NSLog(@"%@", array);
     }
   repeats:YES];

次回、検証してみる。


(*) 実行側で copyしてヒープへ移しておく必要はある。

遅延実行を Blocksで記述する

2010年8月3日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

数秒後に指定した Blocks を実行するメソッドを作ってみた。利用イメージはこんな感じ。

[self performBlock:^(void) {
  self.label.text = @"DONE-4"; }
 afterDelay:4];


performSelector:withObject:afterDelay:


NSObject には数秒後に指定したメッセージを投げるメソッドが用意されている。
NSObject Class Reference - performSelector:withObject:afterDelay:

これを利用して渡された Blocksを実行するメソッドを用意する。今回は NSObject のカテゴリとして実装してみた。

こんな感じ。
@interface NSObject (Extension)
- (void)performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay;
@end

- (void)executeBlock__:(void (^)(void))block
{
 block();
 [block release];
}

- (void)performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay
{
 [self performSelector:@selector(executeBlock__:)
      withObject:[block copy]
      afterDelay:delay];
}

performBlock:afterDelay: が呼ばれたら、performSelector:withObject:afterDelay: を使い別途用意したメソッド executeBlock__: を遅延実行する。これだけ。


サンプル


ソースコードは GitHub からどうぞ。
PerformSelectorAfterDelay at 2010-08-03 from xcatsan's iOS-Sample-Code - GitHub

UILabelを一つ置いてこれを数秒後とに書き換える。

コードはこんな感じ。
- (void)viewWillAppear:(BOOL)animated {

 [self performBlock:^(void) {self.label.text = @"DONE-1";}
   afterDelay:1];

 [self performBlock:^(void) {self.label.text = @"DONE-2";}
   afterDelay:2];

 [self performBlock:^(void) {self.label.text = @"DONE-3";}
   afterDelay:3];

 [self performBlock:^(void) {self.label.text = @"DONE-4";}
   afterDelay:4];
}
1秒ごとにメッセージを書き換えている。

実行すると1秒毎にメッセージが書き換わる。


補足


Blocks はデフォルトでスタックフレーム上に作られる。また Blocks内でメンバ変数が参照されている場合は self が retain され、なおかつ Blocksオブジェクトでの管理対象となる(self というメンバ変数が用意される。また消滅時に releaseされる)。

performBlock:afterDelay: が呼ばれた時の状態は次の通り。
block オブジェクトに selfというメンバ変数があるのがわかる。また block 自体は 0xbfffd528 といった高アドレスに配置されていることからスタックフレーム上にあることが推測できる。

今回は次のように copy を使い、performSelector:withObject:afterDealy: に渡している。
[self performSelector:@selector(executeBlock__:)
     withObject:[block copy]
     afterDelay:delay];
仮に copy を使わないとどうなるのか。次のようにして、executeBlock__ が呼ばれた時の変数を見てみよう。
[self performSelector:@selector(executeBlock__:)
     withObject:block
     afterDelay:delay];
self が無い。これは Blocksがレキシカルスコープを抜けて開放されてしまった為。performSelector:withObject:afterDelay: は非同期実行を行うため,呼び出し後に結果を待つこと無く次の処理へ進む。すると performBlock:afterDelay: を呼び出した側の viewWillAppear: へ制御が戻り、このメソッドも最後まで実行されて終了する。この時 Blocksがスコープを外れるために開放される。その結果、executeBlock__ が呼ばれた時には self が利用できなくなっている(実際には block自体も開放されているので使えない。今回は単純なサンプルなのでたまたまスタックフレームに残っていた blockが参照できたものと思われる)。

このままでは Blocks内で self が使えない。この場合には Blocksをスタックフレームからヒープへ移動させれば良い。その為に Block_copy() が用意されおり、Cocoaを使う場合には copy が利用できる。copyを使った場合の executeBlock__ 内での状態は次の通り。

self が存在する。また blockのアドレスも以前よりも低アドレスに配置されていることがわかる(なのでスタックフレームに配置されているものではないと推測できる)。


参考情報

Blocks Programming Topics: Introduction
Appleによる Blocksの解説
Programming with C Blocks on Apple Devices
丁寧に書かれていて非常に役に立った。Blocksが関数ポインタを利用していることがわかるとスタックとヒープを意識しなければならないことが理解できる。
C/Objective-C + Blocks でクロージャ - TrashSUITE
こちらも参考になった。

UIActionSheet を Blocks で処理する

2010年7月28日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

iOS4から UIView のアニメーション関連のメソッドで Blocksが利用できるようになった。最近よく利用しているがこれは非常に便利。一方、UIActionSheet で不便を感じていたので試しに Blocksが使える実装を書いてみた。

使い方はこんなイメージ:
ActionSheetBlocksExtension*  sheet = [[ActionSheetBlocksExtension alloc] 
            initWithTitle:@"Action sheet sample" 
            didClick:^(UIActionSheet* actionSheet, NSInteger buttonIndex) {
                NSLog(@"[1] index %d: %@", buttonIndex, actionSheet);
            }
            cancelButtonTitle:@"Cancel"
            destructiveButtonTitle:@"Destructive" 
            otherButtonTitles:@"Other-11", @"Other-12", @"Other-13", nil];


アーキテクチャ


UIActionSheet のサブクラスを作り、このクラスで UIActionSheetDelegate を実装する。できればカテゴリで行きたかったが、Delegate先になるのと Blockを保持する必要があったのでサブクラスとした。


実装


今回は ActionSheetBlocksExtension という UIActionSheet のサブクラスを作った。
@interface ActionSheetBlocksExtension : UIActionSheet <UIActionSheetDelegate> {

 void (^didClickBlock_)(UIActionSheet*, NSInteger);
}

- (id)initWithTitle:(NSString *)title
     didClick:(void (^)(UIActionSheet*, NSInteger))block
  cancelButtonTitle:(NSString *)cancelButtonTitle
destructiveButtonTitle:(NSString *)destructiveButtonTitle
  otherButtonTitles:(NSString *)firstOtherTitle,...;

@end

didClickBlock_ は引数で渡される Block を格納するためのメンバ変数。

実装:
#pragma mark -
#pragma mark Initialization & deallocation
- (id)initWithTitle:(NSString *)title
     didClick:(void (^)(UIActionSheet*, NSInteger))block
  cancelButtonTitle:(NSString *)cancelButtonTitle
destructiveButtonTitle:(NSString *)destructiveButtonTitle
 otherButtonTitles:(NSString *)firstOtherTitle,...
{
 self = [super initWithTitle:title
        delegate:self
     cancelButtonTitle:nil
   destructiveButtonTitle:nil
     otherButtonTitles:nil];


 if (self) {
  didClickBlock_ = [block retain];

  int index = 0;
  
  if (destructiveButtonTitle) {
   [self addButtonWithTitle:destructiveButtonTitle];
   self.destructiveButtonIndex = index;
   index++;
  }
  
  if (firstOtherTitle) {
   [self addButtonWithTitle:firstOtherTitle];
   index++;
 
   va_list args;
   va_start(args, firstOtherTitle);
   NSString* title;
   while (title = va_arg(args, NSString*)) {
    [self addButtonWithTitle:title];
    index++;
   }
   va_end(args);
  }
  
  [self addButtonWithTitle:cancelButtonTitle];
  self.cancelButtonIndex = index;
 }
 return self;
}

- (void) dealloc
{
 [didClickBlock_ release];
 [super dealloc];
}

#pragma mark -
#pragma mark UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
 didClickBlock_(actionSheet, buttonIndex);
}

特に難しいことはやっていない。初期化で渡された blockを didClickBlock_ へ格納しておき、ボタンが押された時に呼び出している。

UIActionSheet のサブクラスの初期化については前回取り上げたのでそちらを参照のこと。



サンプル


これを組み込んだサンプルを作ってみた。呼び出し側のコードはこんな感じ。
- (IBAction)openSheet1:(id)sender
{
 ActionSheetBlocksExtension*  sheet = [[ActionSheetBlocksExtension alloc] 
            initWithTitle:@"Action sheet sample" 
            didClick:^(UIActionSheet* actionSheet, NSInteger buttonIndex) {
             NSLog(@"[1] index %d: %@", buttonIndex, actionSheet);
            }
            cancelButtonTitle:@"Cancel"
            destructiveButtonTitle:@"Destructive" 
            otherButtonTitles:@"Other-11", @"Other-12", @"Other-13", nil];
    [sheet autorelease]; 
    [sheet showInView:self.view];
}
Blocks内の処理は、押されたボタンのindexとUIActionSheetのインスタンスをデバッグ出力している。

さて実行してみよう。テスト用に3つの UIActionSheet を作るようにしてみた。
UIActionSheetの表示

ボタンを押すと Blocksで定義したコードが呼び出されているのがわかる。
[67822:207] [1] index 4: <ActionSheetBlocksExtension: 0x5d14ab0;
 baseClass = UIActionSheet; frame = (0 124; 320 336); opaque = NO;
 layer = <CALayer: 0x5d0fa80>>


ソースコード


GitHubからどうぞ。
ActionSheetUsingBlocks at 2010-07-27 from xcatsan's iOS-Sample-Code - GitHub


参考情報


Blocks Programming Topics: Getting Started with Blocks
iPhone Dev Center 提供の Blocks解説



その他


Blocks は不慣れなので問題があるかもしれません。ツッコミがあれば是非コメントにどうぞ。

なお @marvelph @gnue のツイートが参考になりました。ありがとうございました。

人気の投稿(過去 30日間)