Blocks を使ったアニメーションのひっかかりを解消する

2011年8月22日月曜日 | Published in | 0 コメント

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

Cocoaの日々: [iOS] UITableView でプルダウンすると再読込するユーザインタフェースを実装

以前紹介した投稿でアニメーションにひっかかりを感じると書いた。
UITableView を下にドラッグした時に画面上部の矢印が下向き↓からクルッと回転して上向き↑になるアニメーションが起こる。これを iPhoneで実行してみるとカクっとひっかかるような感じがあった。コードはこんな感じ。
[UIView animateWithDuration:0.2
         animations:^{
             self.imageView.transform =
                CGAffineTransformMakeRotation(endAngle);
         }];
Blocks によるアニメーションを使っている。


beginAnimations


先日この件で Kyasu さんより情報提供があった。
kyasu says: 
2011年8月18日19:43

>プルダウンしてアニメーションが起きる時に若干のひっかかりを感じた。
この件ですが、Blocksを使わないでbeginAnimations,commitAnimationsã‚’
使うと動作がブロックされないようです。
?...試してみよう。
[UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.2];
    self.imageView.transform =
    CGAffineTransformMakeRotation(endAngle);
    [UIView commitAnimations];
するとひっかかりが無くなってスムーズにアニメーションが動作するようになった。おお、これはいい。

ただ、その一方で beginAnimations と blocks アニメーションで違いがある?という疑問が湧いてきた。


UIViewAnimationOptionAllowUserInteraction


違いはあった。
UIViewAnimationOptionAllowUserInteraction

普通の表示だけのアニメーションだと違いは無いのだが今回のようにユーザが操作(ドラッグ)している最中にアニメーションを動作させる場合、blocks アニメーションのデフォルト動作では今回のようにひっかかりが発生する。この場合は blocksアニメーションのオプションに UIViewAnimationOptionAllowUserInteraction を指定する必要がある。こんな感じ。
[UIView animateWithDuration:0.2
                          delay:0.0
                        options:UIViewAnimationOptionCurveLinear |
                                UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         self.imageView.transform =
                         CGAffineTransformMakeRotation(endAngle);
                         
                     }
                     completion:NULL
     ];
beginAnimations を使ったアニメーションではデフォルトでこのオプションと同等の動作になっているということなのだろう。


ソースコード


改良版は GitHub からどうぞ。
CustomCellSample at 2011-08-29 from xcatsan/iOS-Sample-Code - GitHub


- - - -
この件はずっと気になっていたので解消されてうれしい。
Kyasu さん、情報提供ありがとうございました。


OCUnit で Notification をテストする

2011年8月19日金曜日 | Published in | 0 コメント

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

通知(Notification)を配信(POST)するメソッドのテストコードを考える。

ポイントは次の2つ
(1) 意図したタイミングで通知が配信されたかどうか
(2) 配信された通知は意図したものだったか

たとえば addEntryWithInfo:tagName: というメソッドを呼び出すと LKQueueDidAddEntryNotification という通知が送られることをテストする場合を書いてみる。

テスト対象のメソッドの実装イメージはこんな感じ。
- (LKQueueEntry*)addEntryWithInfo:tagName:
{
     :
    [[NSNotificationCenter defaultCenter]
        postNotificationName:LKQueueDidAddEntryNotification
                     object:self];
}
処理の最後で LKQueueDidAddEntryNotification をポストしている。objectの引数に自身のインスタンスを渡している。これは通知を受け取った時に notification.object として参照できる。

次にこれをテストするコード。
- (void)testDidAddNotification
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(_didAdd:)
                                                 name:LKQueueDidAddEntryNotification
                                               object:self.queue];

    self.calledNotificationName = nil;
    [self.queue addEntryWithInfo:@"NOTIFY-TEST-1" tagName:nil];
    STAssertEqualObjects(self.calledNotificationName, LKQueueDidAddEntryNotification, nil); // (1)
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
自身を通知対象として _didAdd: メソッドを登録する。その後、addEntryWithInfo:tagName: を呼び出す。これで通知 LKQueueDidAddEntryNotificationが配信される。(1) はその結果の確認。_didAdd: が正しく呼ばれたら (NSString*)self.calledNotificationName に通知名が入っているのでそれを確認する。これでポイントで挙げた「(1) 意図したタイミングで通知が配信されたかどうか」がテストできる。_didAdd: が呼ばれていないと self.calledNotificationNa は nil となるので STAssertEqualsObjects() が失敗する。

次に _didAdd: の実装。
- (void)_didAdd:(NSNotification*)notification

{
    STAssertEqualObjects(notification.name, LKQueueDidAddEntryNotification, nil);
    STAssertEquals(notification.object, self.queue, nil);
    self.calledNotificationName = LKQueueDidAddEntryNotification;
}
ここではポイントで挙げた「(2) 配信された通知は意図したものだったか」を確認している。もし違う通知が届いていたら最初の STAssertEqualObjects() が失敗する。

なお通知は NSRunLoop によって管理されている。この為、通知が配信(POST)された後、通知が届くにはランループが一巡してからになる。ということはテストコードで addEntryWithInfo:tagName: を呼び出した直後の self.calledNotificationName のテストは失敗するはず(_didAdd: が呼ばれていないから)。この為、一旦ランループを一巡させる為に下記を挿入する必要がある。
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
が、実際には不要だった。動作順番を見ていると testDidAddNotification内で addEntryWithInfo:tagName呼び出し -> post LKQueueDidAddEntryNotification -> _didAdd: -> testDidAddNotificationへ復帰、という動きだった。理由は不明だが OCUnit が通知のテストを考慮してそんな作りにしているのだろうか..


関連情報



iphone - OCUnit testing NSNotification delivery - Stack Overflow
addObserver: に NSMutableArray を渡している。POST時にここへ通知が追加されるようなことが書いてあるが初耳。リファレンスには書いてないようだが。。

Xcode4.1 デバッグ時のアセンブリ表示からの脱出

2011年8月12日金曜日 | Published in | 2 コメント

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

ある日突然この現象が起きた。
デバッガを起動するとObjective-Cのソースコードの代わりにアセンブリコードが表示されるようになった。設定をいじった覚えは無いのだが。。

ネットで調べてみるとどうもそういうモードがあるらしい。
Xcodeのデバッガは:なぜ私だけにアセンブラを示し、それを何ですか?


この "Show Disassembly When Debugging" がそのスイッチらしい。これをオフにすると

治った。

一時はデバッグがまともにできないしどうしようかとも思った。やれやれ。

参考まで。



CALayer を使ってビューの内側に影を落とす

2011年8月8日月曜日 | Published in | 0 コメント

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

ビューの上の縁に影を落としたい。こんな感じ。

簡単に出来る方法はないか。

CALayer


CALayer を使うと簡単にビューに影を落とすことができる。
CALayer* layer = self.imageView1.layer;
    layer.shadowOffset = CGSizeMake(2.5, 2.5);
    layer.shadowColor = [[UIColor blackColor] CGColor];
    layer.shadowOpacity = 0.5;


ただこの方法はビューの外側に影を落とせても、ビューの内部には影を落とせない。
どうするか。

CALayer のプロパティを眺めていていると shadowPath に気がついた。このプロパティには CGPathRef を渡すことができる。
@property CGPathRef shadowPath;
もしかしてこれを使って任意の場所や形で影が落とせないか。

試しにこんな矩形のパスを作って渡してみた。

CALayer* subLayer = imageView.layer;
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(-10.0, -10.0, subLayer.bounds.size.width+10.0, 10.0)];
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = [path CGPath];
すると影が落ちた(わかりやすいように画像を縮小してある)。

でも画像の下側だ。影はコンテンツの下に来るものだから当たり前といえば当たり前。コンテンツの上に影をかぶせるにはどうしたらいいか。

サブレイヤーを追加してそこへ影を落としてはどうか?やってみよう。
CALayer* subLayer = [CALayer layer];
    subLayer.frame = imageView.bounds;
    [imageView.layer addSublayer:subLayer];
    subLayer.masksToBounds = YES;
    UIBezierPath* path = [UIBezierPath bezierPathWithRect:
            CGRectMake(-10.0, -10.0, subLayer.bounds.size.width+10.0, 10.0)];
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = [path CGPath];

出た。いい感じだ。

どんなビューでも CALayer がサポートされているので例えば MKMapView でも簡単に影を落とせる。


バリエーション


上だけでなく左にも影を落としてみた。ついでに角も丸くした。


逆L字型の図形を左上に用意してその影を落とせばいい。

逆L字型図形は CGMutablePath を使って地道に描く。
- (void)_addDropShadowToView2:(UIView*)toView
{
    CALayer* subLayer = [CALayer layer];
    subLayer.frame = toView.bounds;
    [toView.layer addSublayer:subLayer];
    subLayer.masksToBounds = YES;

    CGSize size = subLayer.bounds.size;
    CGFloat x = -10.0;
    CGFloat y = -10.0;
    CGMutablePathRef pathRef = CGPathCreateMutable();
    CGPathMoveToPoint(pathRef, NULL, x, y);
    x += size.width + 10.0;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y += 10.0;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    x -= size.width;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y += size.height;
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    x -= 5.0;   // (*)10
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    y -= size.height;   // (*)size.height+10
    CGPathAddLineToPoint(pathRef, NULL, x, y);
    CGPathCloseSubpath(pathRef);
   
    subLayer.shadowOffset = CGSizeMake(2.5, 2.5);
    subLayer.shadowColor = [[UIColor blackColor] CGColor];
    subLayer.shadowOpacity = 0.5;
    subLayer.shadowPath = pathRef;
   
    CGPathRelease(pathRef);
   
}
実は逆L字型は少し歪んだ形をしている。(*)のついている2行はコメントに記載した値が正しいのだが、これを使うと下図の様にコンテンツ全体に薄い影がかかってしまった。

パスはきちんと閉じていると思うのだが、どうも思った形で影が落ちないようだ。上記値は試行錯誤で見つけた値。逆L字型が正確な形ではないが、見た目は意図通りの影が落ちているのでこれでよしとする。原因を知っている方(バグを見つけた方)がいたら是非教えて下さい。


ソースコード


GitHub からどうぞ。
LayerShadowSample at 2011-08-08 from xcatsan/iOS-Sample-Code - GitHub


参考情報


Fun shadow effects using custom CALayer shadowPaths | iOS/Web Developer's Life in Beta
shadowPath プロパティを使った様々な形の影の落とし方。参考になった。


Invitation to CoreAnimation - NIT-Universe
サブレイヤーを使うアイディアはここからヒントを得た。

関連情報


Cocoaの日々: Bezelボタンを作る[03]矩形の内側に影を落とす
以前紹介したビューの内側へ影を落とす方法。マスクを作ったりと結構面倒。今回のCALayerを使う方が簡単。

数字キーボードビュー(ライブラリ)を公開

2011年8月1日月曜日 | Published in | 4 コメント

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

数字キーボードもどきを作ってみました。

キートップをタップすると標準キーボードのようにポップアップします。
CoreGraphicsの関数を使って全部自力で描いてます。

lakesoft/LKNumberPad - GitHub



インストール


1. GitHubからプロジェクトをダウンロードした後 Xcode 4 で開く。
2. その中から LKNumberPadView.h と LKNumberPadView.m を自分のプロジェクトへコピーして追加する。


使い方


Xib を使う場合は次の通り。
1. 表示したいビューを開き、UIView を貼りつけた後、クラスを LKNumberPadView に変更する。
推奨サイズは 横 320 x 縦 54。
2. 背景色(Background)プロパティを設定する。
これだけで表示できるようになる。

次にボタンがタップされた時の動作を記述する。
3. アウトレット delegate を設定する。※例では File's Owner に設定している
4. デリゲート LKNumberPadViewDelegate のメソッドを実装する
@protocol LKNumberPadViewDelegate 

@optional
- (void)didTouchNumberPadView:(LKNumberPadView*)numberPadView touchedString:(NSString*)string;
- (void)didTouchNumberPadView:(LKNumberPadView*)numberPadView withSequentialString:(NSString*)string;

@end

- didTouchNumberPadView:touchedString:
タップされる毎に呼び出され、押された数字が文字列として1文字だけ渡される。

- didTouchNumberPadView:withSequentialString:
タップされる毎に呼び出され、押された数字が文字列として渡される。一定期間に連続してボタンがタップされた場合は連続した数字が文字列として渡される。例えば1を押した直後にすぐに4を押すと @"14" が渡ってくる。


カスタマイズ


背景色などをプロパティで設定することができる。

BOOL startWithZero
YES だと0始まりになる(一番左が0で、一番右が9)。デフォルトは NOで標準のキーボードと同じ1始まり(一番左が1で、一番右が0)。

UIColor* keyboardColor
キーボードのキートップの背景色。
デフォルトは [UIColor colorWithWhite:0.95 alpha:1.0]。

UIColor* textColor
キーボードのキートップの文字色。
デフォルトは [UIColor blackColor]。

UIColor* disabledKeyboardColor
ディゼーブル時のキーボードのキートップの背景色。
デフォルトは [UIColor colorWithWhite:0.85 alpha:1.0]。

UIColor* disabledTextColor
ディゼーブル時のキーボードのキートップの文字色
デフォルトは [UIColor lightGrayColor]。

NSTimeInterval sequenceInterval
キー入力間隔(秒)。次のキーがこの間隔内にタップされた場合、連続したキー入力とみなす。
デフォルトは 0.75秒。

NSSet* enabledSet
キートップの有効・無効を指定する。有効にしたい数字(注意:インデックスではない)ã‚’ NSNumber の値として入れておく。nil の場合は全て有効になる。
view.enabledSet = [NSSet setWithObjects:
                                     [NSNumber numberWithInt:1],
                                     [NSNumber numberWithInt:2], nil];


ソース解説


キートップは UIBezierPath などを使い描画している。
- (void)- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();   
    CGRect frame;
    CGColorRef shadowColorRef;
    UIColor* drawColor;

    // (1) draw keyboard
    frame = CGRectMake(0, 0,
                       self.bounds.size.width / LKNUMBERPADVIEW_KEYBOARD_NUM,
                       self.bounds.size.height);
    
   :
}

キーが押された時のポップアップ表示には CALayer を使っている。この描画を行う為に CALayer のサブクラス LKNumberPadLayer ã‚’ LKNumberPadView.m 内で定義している。
enum {
    LKNumberPadViewImageLeft = 0,
    LKNumberPadViewImageInner,
    LKNumberPadViewImageRight,
    LKNumberPadViewImageMax
};

@interface LKNumberPadLayer : CALayer {
    CGImageRef keytopImages_[LKNumberPadViewImageMax];
}
@property (nonatomic, copy) NSString* character;
@property (nonatomic, assign) int imageKind;

- (CGImageRef)createKeytopImageWithKind:(int)kind;

@end
ポップアップは左はじ、右はじ、それ以外の3種類の表示が必要になる。あらかじめ3種類の画像を描画しておき、配列 keytopImages_[] に取っておく。

ポップアップの描画は CoreImage関数の CGPathAddArc() ã‚„ CGPathAddCurveToPoint() を使ってベタに書いている。
- (CGImageRef)createKeytopImageWithKind:(int)kind
{
    CGMutablePathRef path = CGPathCreateMutable();
   
    CGPoint p = CGPointMake(LKNUMBERPADVIEW_PADDING_X, LKNUMBERPADVIEW_PADDING_Y);
    CGPoint p1 = CGPointZero;
    CGPoint p2 = CGPointZero;
   
    p.x += LKNUMBERPADVIEW_PAN_UPPER_RADIUS;
    CGPathMoveToPoint(path, NULL, p.x, p.y);
   
    p.x += LKNUMBERPADVIEW_PAN_UPPDER_WIDTH;
    CGPathAddLineToPoint(path, NULL, p.x, p.y);
   
    p.y += LKNUMBERPADVIEW_PAN_UPPER_RADIUS;
    CGPathAddArc(path, NULL,
                 p.x, p.y,
                 LKNUMBERPADVIEW_PAN_UPPER_RADIUS,
                 3.0*M_PI/2.0,
                 4.0*M_PI/2.0,
                 false);
          :

頂点のイメージ



キーがタップされたら適切なポップアップ画像を選び CALayer 上に表示する。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSUInteger index = [self _indexWithEvent:event];
    [CATransaction begin];
    [CATransaction setValue:(id)kCFBooleanTrue
                     forKey:kCATransactionDisableActions];
    [self _updateWithIndex:index];
    [CATransaction commit];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSUInteger index = [self _indexWithEvent:event];
    if (index != self.touchedIndex) {
        [self _updateWithIndex:index];
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.touchedIndex = -1;
    self.numberPadLayer.opacity = 0.0;
    [self setNeedsDisplay];
}
- (void)_updateWithIndex:(NSUInteger)index
{
    self.touchedIndex = index;
    NSUInteger number = [self _numberWithIndex:index];
    self.touchedString = [NSString stringWithFormat:@"%d", number];
          :    
    [self setNeedsDisplay];
    [self.numberPadLayer setNeedsDisplay];    // ポップアップ表示
          :
}


ポップアップの描画は -[CALayer drawInContext:] 内で描画する。あらかじめ描いておいた画像を表示し、その上に数字を描画する。
- (void)drawInContext:(CGContextRef)context
{
    CGColorRef shadowColorRef = [[UIColor colorWithWhite:0.1 alpha:1.0] CGColor];
    CGContextSetShadowWithColor(context,
                                CGSizeMake(LKNUMBERPADVIEW_PAN_SHADOW_OFFSET_X,
                                           LKNUMBERPADVIEW_PAN_SHADOW_OFFSET_Y),
                                LKNUMBERPADVIEW_PAN_SHADOW_BLUR,
                                shadowColorRef
                                );

    CGImageRef imageRef = keytopImages_[self.imageKind];
    CGRect imageFrame = CGRectMake(0, 0,
                                   CGImageGetWidth(imageRef),
                                   CGImageGetHeight(imageRef));
    CGContextDrawImage(context, imageFrame, imageRef);

    // draw text
    CGContextSelectFont(context, "Helvetica Bold", 44, kCGEncodingMacRoman);
    CGContextSetTextDrawingMode(context, kCGTextFill);
    CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
    CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1.0, -1.0));
   
    shadowColorRef = [[UIColor whiteColor] CGColor];
    CGContextSetShadowWithColor(context,
                                CGSizeMake(0.0,
                                           1.0),
                                1.0,
                                shadowColorRef
                                );
 
    CGContextShowTextAtPoint(context, 28, 55,
                             [self.character UTF8String],
                             [self.character length]);
   
}



ライセンス


MIT ライセンスです。商用・非商用を問わず利用可能。カスタマイズしての再配布も自由。連絡も不要(でもくれるとうれしい)。


関連情報


なし



備考


・アプリ審査の実績なし。もし使って通った方がいたら是非教えて下さい。
・ポップアップ表示の下の影の部分だけは本物同様にできなかった(本物は影のblur値が小さい)。これをやるとなるとそれなりに手間がかかるので、ここだけは手を抜いた。押した時にここは見えないし。




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