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値が小さい)。これをやるとなるとそれなりに手間がかかるので、ここだけは手を抜いた。押した時にここは見えないし。




[iOS] SCNetworkReachabilityGetFlags のブロックの件

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

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

以前、Cocoaの日々: ネットワーク接続状況を知る[2] SCNetworkReachabilityGetFlags はブロックする という記事を書いたがそのブロックの原因がわかった。


SCNetworkReachabilityCreateWithName


以前は、SCNetworkReachability のインスタンスを作るのにホスト名ベースの関数を使っていた。こんな感じ。
SCNetworkReachabilityCreateWithName(kCFAllocatorDefault,
   [@"xcatsan.com" UTF8String]);
この後、接続性をチェックする為に SCNetworkReachabilityGetFlags() を呼び出すとブロックするケースがあった、というのがその記事の内容だったのだが、その理由は DNSによる名前解決に時間がかかっていた為と思われる。

通常は 3G か Wifi がつながればインターネットへの接続ができて DNSによる名前解決が成功するのでこのブロック現象は起きない。ただモバイルルーターを使っていて、例えば
iPhone --◯--> Wifiルータ --×--> 3G
となっていた場合、iPhoneから見るとネット(Wifi)接続OKだが実際にはインターネットにつながっていない。地下鉄で利用しているとこんなケースがありうる。
この時に先ほどのホスト名ベースの関数 SCNetworkReachabilityCreateWithName() を使うと SCNetworkReachabilityGetFlags() を呼び出した時に、そのホスト名の名前解決を試みようとする。インターネットにはつながっていないので名前解決は当然失敗するのだが、Wifiネットワークには繋がっているので名前解決の処理を待ち続ける。その結果、タイムアウトが発生するまで SCNetworkReachabilityGetFlags() でブロックが起きる。

なお、そもそも Wifiルータへ接続できない場合は名前解決を試みないのでこの現象は起きない。
iPhone --×--> Wifiルータ --×--> 3G

また同じ記事で書いていた「初回のタイムラグ」はこの DNSの名前解決にかかる時間だと思われる。なお名前解決したからといってICMPなどを使って実際に到達可能性を調べているわけではない。


SCNetworkReachabilityCreateWithAddress


SCNetworkReachability のインスタンスを作る関数は他にも何種類かある。そのなかには IPアドレスを指定する関数もある。
SCNetworkReachability.h

SCNetworkReachabilityRef
SCNetworkReachabilityCreateWithAddress  (
      CFAllocatorRef   allocator,
      const struct sockaddr  *address
      )    __OSX_AVAILABLE_STARTING(__MAC_10_3,__IPHONE_2_0);
この関数を使うとDNS名前解決が発生しないので、それを原因とする SCNetworkReachabilityGetFlags() におけるブロッキングが発生しない(はず)。使い方はこんな感じ。
struct sockaddr_in sockaddr;
        bzero(&sockaddr, sizeof(sockaddr));
        sockaddr.sin_len = sizeof(sockaddr);
        sockaddr.sin_family = AF_INET;
        inet_aton("0.0.0.0", &sockaddr.sin_addr);        

        reachability_ =
            SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *) &sockaddr);
なおこの IPアドレスは接続性のチェックには使われていないようで適当なもの例えば 0.0.0.0 でも動作する。


DNS名前解決


DNSの利用について簡単な確認をやってみた。以下、結果。
a) iPhoneのWifi設定で DNS設定あり
  ホスト名ベース   => 接続 OK
  IPアドレスベース => 接続 OK

 b) iPhoneのWifi設定で DNS設定なし(設定項目を空)
  ホスト名ベース   => 接続 NG
  IPアドレスベース => 接続 OK

他の現象と合わせ、やっぱりホスト名ベースの場合は DNS名前解決を試みていると思われる。


NetworkReachability のまとめ


これまでの試行錯誤でわかった結果をまとめておく。
・デバイスの接続状態のみを判断する
・3G 接続できていれば、接続可能
・Wifi接続できていれば、接続可能(その場合、3Gより優先される)
・指定したホストに ping(ICMP)は飛ばしていない
・APIへホスト名を渡した場合、デバイスが接続可能なら名前解決を実行する
・名前解決に時間がかかると SCNetworkReachabilityGetFlags() はブロックする
 (名前解決のタイムアウトは実測値で 60秒程度)
・APIへIPアドレスを渡す場合、IPアドレスが実在しなくても接続チェックに影響を与えない。
  (例)0.0.0.0


ライブラリ


これらを受けて以前公開したライブラリを修正した。
[参照] Cocoaの日々: [iOS] ネットワーク接続状況取得ライブラリを公開

最新コードはこれ。
dev5tec/FBNetworkReachability - GitHub
以前は非同期(通知)ベースだったが、新バージョンでは同期、非同期の両方で使える。

以前のコードはここに残してある。
dev5tec/FBNetworkReachability at v1 - GitHub

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

2011年7月7日木曜日 | Published in | 7 コメント

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

(2011-08-22 追記)
矢印画像のアニメーションにひっかかりが発生する件は解決策が見つかった。下記を参照のこと。
Cocoaの日々: Blocks を使ったアニメーションのひっかかりを解消する



[前回] Cocoaの日々: [iOS] UITableView でセルをスワイプするとスライドするユーザインタフェースを実装

今回も Twitterアプリのまね。UITableView をプルダウンした時に出てくるあのユーザインターフェイスを実装してみた。

"pull down to refresh" と呼ばれているらしい。


サンプル


まずはサンプルの動作から。

ここでプルダウンすると上部にメッセージが現れる。
さらに引き下げるとメッセージが変わる。左の画像もクルッと回転して上向きになる。
ここで指を離すとしばらく止まる。サンプルでは単純に2秒のウェイトを入れてある。
2秒すぎると自動的に上へスクロールして最初の状態に戻る。


実装


表示ロジック


プルダウンすると現れるエリアの表示には UITableView のヘッダ(tableHeaderView)をつかう。Xib で UIView を一つ作り、これを UITableViewへ紐付けておく。
- (void)viewDidLoad
{
       :
    self.tableView.tableHeaderView = self.headerView;
}
ただこのままだと初期表示でこのヘッダが表示されてしまう。
そこで UIScrollView の contentInsets プロパティを調整してヘッダを見えなくする。contentInsets は Xib でもコードでも変更可能。
ヘッダビューの高さが 60ポイントなので cotentInsets.top = -60 としてやると表示エリアが下に 60ポイント下がり、初期状態ではヘッダ部分が隠れるようになる。
これが最初に見えている状態。指で下にドラッグするとヘッダ部分が現れるようになる。
この状態で指を離すとスクロールして最初の状態(ヘッダが隠れる状態)に戻る。一方、下へのドラッグ距離がある閾値(60ピクセル+α)を越えたところ指を離した時には、contentInsets.top = 0 を設定してやる。こうするとスクロールしてもヘッダ表示された状態でピタリと止まる。
処理が終わったら(サンプルでは2秒間のウェイトが入れてある) contentInsets.top = -60 へ戻してやる。すると最初のヘッダが隠れた表示に戻る。なお contentInsets も UIViewアニメーションの対象なので +[UIView animateWithDuration:aniamations:] を使うとヘッダが隠れるまでがアニメーションになる(逆に使わないと一瞬で切り替わり、やや不自然になる)。

コードではヘッダの表示制御用にメソッドを用意してある。こんな感じ。
- (void)_setHeaderViewHidden:(BOOL)hidden animated:(BOOL)animated
{
    CGFloat topOffset = 0.0;
    if (hidden) {
        topOffset = -self.headerView.frame.size.height;
    }
    if (animated) {
        [UIView animateWithDuration:0.2
                         animations:^{
                                self.tableView.contentInset = UIEdgeInsetsMake(topOffset, 0, 0, 0);
                         }];
    } else {
        self.tableView.contentInset = UIEdgeInsetsMake(topOffset, 0, 0, 0);        
    }
}
上記を適切なタイミングで呼んでやる。なおアニメーションを制御の対象にしているのは一番最初の初期表示時にはアニメーションを使わないため。


カスタムヘッダビュー


ヘッダ用にカスタムクラス HeaderView を用意した。
typedef enum {
    HeaderViewStateHidden = 0,
    HeaderViewStatePullingDown,
    HeaderViewStateOveredThreshold,
    HeaderViewStateStopping
} HeaderViewState;

@interface HeaderView : UIView {
    
}

@property (nonatomic, retain) IBOutlet UILabel* textLabel;
@property (nonatomic, retain) IBOutlet UILabel* detailTextLabel;
@property (nonatomic, retain) IBOutlet UIImageView* imageView; 
@property (nonatomic, retain) IBOutlet UIActivityIndicatorView* activityIndicatorView;

@property (nonatomic, assign) HeaderViewState state;

- (void)setUpdatedDate:(NSDate*)date;

@end
ラベルなどの表示項目の保持の他、矢印画像のアニメーション、UIActivityIndicatorView の表示制御をここで行わせる。またプルダウンの状態管理もここで行う。プルダウンの状態は4つ定義している。
typedef enum {
    HeaderViewStateHidden = 0,          // ヘッダが隠れた状態
    HeaderViewStatePullingDown,         // プルダウン状態1(ただし閾値は超えていない)
    HeaderViewStateOveredThreshold,     // プルダウン状態2(閾値を越えている)
    HeaderViewStateStopping             // プルダウン停止状態(処理中)
} HeaderViewState;
状態毎にラベルの表示内容、矢印画像の向き、UIActivityIndicatorView の表示・非表示を決める。state プロパティのセッターメソッドを自前で用意してこの処理を書いておく。
- (void)setState:(HeaderViewState)state
{
    switch (state) {
        case HeaderViewStateHidden:
            [self.activityIndicatorView stopAnimating];
            self.imageView.hidden = YES;
            break;

        case HeaderViewStatePullingDown:
            [self.activityIndicatorView stopAnimating];
            self.imageView.hidden = NO;
            self.textLabel.text = @"引き下げて...";
            if (state_ != HeaderViewStatePullingDown) {
                [self _animateImageForHeadingUp:NO];
            }
            break;
            
        case HeaderViewStateOveredThreshold:
            [self.activityIndicatorView stopAnimating];
            self.imageView.hidden = NO;
            self.textLabel.text = @"指をはなすと更新";
            if (state_ == HeaderViewStatePullingDown) {
                [self _animateImageForHeadingUp:YES];
            }
            break;

        case HeaderViewStateStopping:
            [self.activityIndicatorView startAnimating];
            self.textLabel.text = @"更新中...";
            self.imageView.hidden = YES;
            break;
    }

    state_ = state;
}
画像の回転は前回のブログで紹介した。
Cocoaの日々: [iOS] transform を使った画像のお手軽回転アニメーション

こんな感じ。
- (void)_animateImageForHeadingUp:(BOOL)headingUp
{
    CGFloat startAngle = headingUp ? 0 : M_PI + 0.00001;
    CGFloat endAngle = headingUp ? M_PI + 0.00001 : 0;

    self.imageView.transform = CGAffineTransformMakeRotation(startAngle);           
    [UIView animateWithDuration:0.1
         animations:^{
             self.imageView.transform =
                CGAffineTransformMakeRotation(endAngle);
         }];
}
headingUp によって上→下、下→上のアニメーションを切り替えている。


※画像入手元および画像作者
入手元:Arrow, Up icon | Icon Search Engine
作成者: Kyo Tux (ホームページ kyo-tux on deviantART)


プルダウンの検出


プルダウンの検出には UIScrollViewDelegate を使う。こんな感じ。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (self.headerView.state == HeaderViewStateStopping) {
        return;
    }

    CGFloat threshold = self.headerView.frame.size.height;

    if (PULLDOWN_MARGIN <= scrollView.contentOffset.y &&
        scrollView.contentOffset.y < threshold) {
        self.headerView.state = HeaderViewStatePullingDown;

    } else if (scrollView.contentOffset.y < PULLDOWN_MARGIN) {
        self.headerView.state = HeaderViewStateOveredThreshold;
        
    } else {
        self.headerView.state = HeaderViewStateHidden;
    }
}
図にすると多分わかりやすい。

ドラッグ状態(≒スクロール状態)が閾値を越えているかどうかによってヘッダのプルダウン状態を変えている。先ほど見たようにヘッダは state の値によってラベルの表示を変えたり、矢印画像のアニメーションを実行している。 contentOffset.y はヘッダの表示・非表示にかかわらずその上の部分が基準(0)となる。ここを基準に PULLDOWN_MARGIN よりも上まで表示するようにプルダウンした時に閾値を越えたと判断する( HeaderViewStateOveredThreshold)。また PULLDOWN_MARGIN からヘッダの高さまでの間の時はプルダウン状態(HeaderViewStatePullingDown)とみなし、それより下は非表示状態(HeaderViewStateHidden )

プルダウン中に指を離した時の処理はこう。
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (self.headerView.state == HeaderViewStateOveredThreshold) {
        self.headerView.state = HeaderViewStateStopping;
        [self _setHeaderViewHidden:NO animated:YES];
        
        [self performSelector:@selector(_taskFinished) withObject:nil afterDelay:2.0];
    }
}
scrollViewDidEndDragging:willDelegate: は UIScrollView を指でドラッグした後、指を離したときに呼び出される。このメソッドが呼び出された時(=指を離した時)にプルダウン状態が HeaderViewStateOveredThreshold ならヘッダを表示状態(位置固定)にしている。サンプルでは2秒のディレイを入れていて、これが終わったら再びヘッダを閉じる処理を入れている。
- (void)_taskFinished
{
    self.headerView.state = HeaderViewStateHidden;
    [self.headerView setUpdatedDate:[NSDate date]];
    [self _setHeaderViewHidden:YES animated:YES];
}
なお UIScrollViewDelegate でプルダウンを検出するアイディアは下記の本がヒントになった。


備考


矢印が上下に回転する処理は若干重め。当初アニメーション時間を 0.2秒にしていたが 3GS ではプルダウンしてアニメーションが起きる時に若干のひっかかりを感じた。今は 0.1秒にしていて多少ましになった。多分コマ数が減った分負荷が下がったのだと思う。この辺りはケースによってはチューニングの必要があるかもしれない。なお、お手本にした Twitterクライアントではこのプチフリーズは起きない。


(2011-08-22 追記)
矢印画像のアニメーションにひっかかりが発生する件は解決策が見つかった。下記を参照のこと。




ソースコード

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


参考情報


UITableView カスタマイズシリーズ


Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方 Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理 Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その3)ボタンの処理[改良版] Cocoaの日々: [iOS] UITableView でセルをスワイプするとスライドするユーザインタフェースを実装 今回のコードはこれらをベースに作っている。


関連情報


iphone - Pull down to refresh. - Stack Overflow
"Pull down to refresh" の発案者は Twitter公式クライアントの前身である Tweetie の開発者 Loren Brichter氏が考え出したものらしい。

このアイディアの利用について対価を求めるかの問いに対して Brichter氏は "absolutely not" と明確に否定している。
Twitter / @lorenb: @michellusthof absolutely ...
考え出したセンスもアイディアも素晴らしいし、利用に対する態度も素晴らしい。

[iOS] transform を使った画像のお手軽回転アニメーション

2011年7月6日水曜日 | Published in | 0 コメント

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

1枚の画像を回転させるアニメーションを実現したい。UIView の transform プロパティを使うと非常に簡単にできることがわかった。


サンプル
初期状態。ここで start を押すと
時計回りに回転が始まり

下を向いた状態で終わる。


実装

用意している画像はこの1枚だけ。


※画像入手元および画像作者
入手元:Arrow, Up icon | Icon Search Engine
作成者: Kyo Tux (ホームページ kyo-tux on deviantART)



これを UIImageView.image に設定し、transformプロパティを使って回転させる。こんな感じ。
- (IBAction)start:(id)sender {
    
    self.imageView.transform = CGAffineTransformMakeRotation(0);
    [UIView animateWithDuration:0.2
        animations:^{
            self.imageView.transform =
                CGAffineTransformMakeRotation(2*M_PI*180/360.0-0.000001);                         
        }];
}
最初に transform を CGAffineTransformMakeRotation(0)(角度 0)で初期化し、アニメーションブロックで transform を180度回転させた値を設定している。CGAffineTransformMakeRotation の引数の単位はラジアンなので 2π x 度/360 で角度変換する。最後の -0000001 は右側で回転させる為のおまじない。 これが無いと左側での回転となる。こうやってアニメーションブロックに入れておくと回転中の画像が自動的に作られてスムーズなアニメーションが行える。


その他


当初は NSTimer を使ってコンマ数秒毎に角度を変えて自前にアニメーションを行ってみたが、スムーズなアニメーションを作るには試行錯誤が必要でかなり面倒。
- (void)fire:(NSTimer*)timer
{
    angle_ += 30.0;
    self.imageView.transform = CGAffineTransformMakeRotation(2*M_PI*angle_/360.0);

    if (angle_ >= 180.0) {
        angle_ = 0;
        self.button.enabled = YES;
        [timer invalidate];
    }
}

- (IBAction)start:(id)sender {
    
    [NSTimer scheduledTimerWithTimeInterval:0.05
                                     target:self
                                   selector:@selector(fire:)
                                   userInfo:nil
                                    repeats:YES];
}
上記は 30度単位で回しているがカクカク感が多少ある。UIView のアニメーションを使うとこの当たりの調整が不要で簡単にアニメーションができるので非常に便利。画像も1種類用意しておけば使いまわせるのもいい。


ソースコード


GitHub からどうぞ。
RotateImageSample at 2011-07-06 from xcatsan/iOS-Sample-Code - GitHub


参考情報


CGAffineTransformMakeRotation
transform プロパティの使い方はここが参考になった。

iPhoneアプリ開発、その(64) トランスフォ〜ム|テン*シー*シー
CoreGrapshicsを操作する場合は CGContextRotateCTM などを使う。transformプロパティを使わずに自力で描画しているケースはこちらが参考になる。

[iOS] UITableView でセルをスワイプするとスライドするユーザインタフェースを実装

2011年6月29日水曜日 | Published in | 0 コメント

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

スワイプしてセルが横にスライドする動作を実装してみた。Twitterクライアントなどで実装されているあれ。


サンプル


セルを右方向にスワイプすると

スライドアニメーションが始まり下に隠れていたビューが姿を現す。

開ききった状態。

この後左にスワイプするか、他のセルをスワイプするとスライドが閉じる。


実装


ロジック

スワイプイベントは RootViewController で受け取り、セルの開け閉めを管理する。


ビューの配置


通常表示のビュー(BaseView)の下にスライド時に現れるビュー(SlideView)を重ねておく。普段は SlideView は隠れて見えない。
一時的に順番を入れ替えて内容を確認。SlideView はこんな感じ。




スワイプの処理


RootViewController にUIGestureRecognier を左右両方向について登録しておく。
- (void)viewDidLoad
{
    [super viewDidLoad];
          :   
    UISwipeGestureRecognizer* swipeGesture =
        [[UISwipeGestureRecognizer alloc]
         initWithTarget:self action:@selector(didSwipeCell:)];
    swipeGesture.direction = UISwipeGestureRecognizerDirectionRight;  
    [self.tableView addGestureRecognizer:swipeGesture];  
    [swipeGesture release];

    swipeGesture =
        [[UISwipeGestureRecognizer alloc]
            initWithTarget:self action:@selector(didSwipeCell:)];
    swipeGesture.direction = UISwipeGestureRecognizerDirectionLeft;  
    [self.tableView addGestureRecognizer:swipeGesture];  
    [swipeGesture release];
}
ハンドラはこう。
- (void)didSwipeCell:(UISwipeGestureRecognizer*)swipeRecognizer
{
    CGPoint loc = [swipeRecognizer locationInView:self.tableView];
    NSIndexPath* indexPath = [self.tableViewindexPathForRowAtPoint:loc];
    CustomCell* cell = (CustomCell*)[self.tableViewcellForRowAtIndexPath:indexPath];

    if ([openedIndexPath_ isEqual:indexPath]) {
        if (swipeRecognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
            // close cell
            [cell setSlideOpened:NO animated:YES];
            openedIndexPath_ = nil;
        }
    } else if (swipeRecognizer.direction == UISwipeGestureRecognizerDirectionRight) {
        if (openedIndexPath_) {
            // close previous opened cell
            NSArray* visibleIndexPaths = [self.tableView indexPathsForVisibleRows];
            if ([visibleIndexPaths containsObject:openedIndexPath_]) {
                CustomCell* openedCell =
                    (CustomCell*)[self.tableView cellForRowAtIndexPath:openedIndexPath_];
                [openedCell setSlideOpened:NO animated:YES];
            }
        }
        // open new cell
        [cell setSlideOpened:YES animated:YES];
        openedIndexPath_ = indexPath;
    }
         
}
アーキテクチャで説明した②③④、②’③’を実行する。開けるのは右方向のスワイプの時だけ、また閉める時は左スワイプの時だけ有効にしてある。スワイプの結果開いた状態のセルが存在する場合はその場所を openedIndexPath_ へ取っておく。


スライドアニメーション


カスタムセルにスライドアニメーション用のメソッドを用意する。
@interface CustomCell : UITableViewCell {
    
    BOOL slideOpened_;
}
   :
- (void)setSlideOpened:(BOOL)slideOpened animated:(BOOL)animated;
無駄な開け閉めを防ぐ為、現在の状態を slideOpened_ にとっておいてある。実装はこんな感じ。
- (void)setSlideOpened:(BOOL)slideOpened animated:(BOOL)animated
{
    if (slideOpened == slideOpened_) {
        return;
    }    
    slideOpened_ = slideOpened;
    
    if (animated) {
        if (slideOpened_) {
            // open slide
            [UIViewanimateWithDuration:0.2
                             animations:^{
                                 CGRect frame = self.baseView.frame;
                                 frame.origin.x += frame.size.width;
                                 self.baseView.frame = frame;
                             }];
            
        } else {
            // close slide
            [UIViewanimateWithDuration:0.1
                             animations:^{
                                 CGRect frame = self.baseView.frame;
                                 frame.origin.x = 0;
                                 self.baseView.frame = frame;
                             }];
        }
    } else {
        CGRect frame = self.baseView.frame;
        if (slideOpened_) {
            // open slide
             frame.origin.x += frame.size.width;
            
        } else {
            // close slide
             frame.origin.x = 0;
        }
        self.baseView.frame = frame;
        
    }

}
わざわざアニメーション有り・無しのケースを用意しているのは、セルを初期表示する場合などアニメーションが不要なケースがあるから。アニメーション処理自体は UIViewanimateWithDuration:animations: を使いビューの位置を変えているだけの単純なもの。開くときと閉じるときでかかる時間を変えてみた。


セルの表示


セルは再利用されるため、表示の度にモデルの状態をきちんと反映してやる必要がある。
RootViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
        :
    // Configure the cell.
        :
    if ([openedIndexPath_ isEqual:indexPath]) {
        [cell setSlideOpened:YES animated:NO];
    } else {
        [cell setSlideOpened:NO animated:NO];
    }

    return cell;
}
文字や画像の設定の他、スライドの開閉状態を反映しておく。その判断にスワイプジェスチャの処理で取っておいた openedIndexPath_ を使う。


選択状態の自前実装


UITableViewCell上でビューを2枚重ねている場合で選択した時には下のビュー上のコントロールが表示されてしまう。UITableViewCell は選択状態の時にその上に配置されているすべてのビューに対して -setHilight:YES のメッセージを送るようだ(さらにその場合は背景となっているビューよりも前に表示されるようだ)。
これを回避するには標準の選択状態を消した上で自前で描画する。
まず SelectionをNoneにする。
次にカスタムビュークラス BaseView を用意し、選択時の描画を行わせる。単色だと味気ないので選択状態の表示にグラデーションをかけてみた。
@interface BaseView : UIView {
}
@property (nonatomic, assign) BOOL selected;
@end


@implementation BaseView
@synthesize selected;
- (void)drawRect:(CGRect)rect
{
    // draw 
    if (selected) {
        
        CGContextRef context = UIGraphicsGetCurrentContext();    
        
        CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
        CGFloat components[] = { 0.9f, 0.9f, 0.9f, 0.9f,
                                 0.7f, 0.7f, 0.7f, 0.7f };

        
        size_t count = sizeof(components)/ (sizeof(CGFloat)* 4);

        
        CGContextAddRect(context, self.frame);
        
        CGRect frame = self.bounds;
        CGPoint startPoint = frame.origin;
        CGPoint endPoint = frame.origin;
        endPoint.y = frame.origin.y + frame.size.height;

        CGGradientRef gradientRef =
            CGGradientCreateWithColorComponents(colorSpaceRef, components, NULL, count);

        CGContextDrawLinearGradient(context,
                                    gradientRef,
                                    startPoint,
                                    endPoint,
                                    kCGGradientDrawsAfterEndLocation);

        
        
        CGGradientRelease(gradientRef);
        CGColorSpaceRelease(colorSpaceRef);
    }
}
@end
なおSelection を Noneにすると押している間のハイライト状態の表示が変わらなくなるのでこれも手当しておく。
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
    UIColor* selectedColor = [UIColor whiteColor];  // default color
    if (highlighted) {
        selectedColor = [UIColor lightGrayColor];
    }
    self.baseView.backgroundColor = selectedColor;
    [super setHighlighted:highlighted animated:animated];
}
これで選択状態はこうなった。


影を付ける


セルがスライドして開いている状態はそのままだとこんな感じ。
平面的で少々味気ない。少し立体感を持たせる為に影を落とすことにする。そこでカスタムビュー SlideView に影を描く。
@interface SlideView : UIView {
}
@end
    
@implementation SlideView


#define CUSTOMCELL_OBJECT_LENGTH    10.0
#define CUSTOMCELL_SHADOW_OFFSET    5.0
#define CUSTOMCELL_SHADOW_BLUR      5.0

- (void)drawRect:(CGRect)rect
{
    // draw edge shadow
    CGRect frame = self.bounds;
    frame.origin.x -= CUSTOMCELL_OBJECT_LENGTH;
    frame.origin.y -= CUSTOMCELL_OBJECT_LENGTH;
    frame.size.width += CUSTOMCELL_OBJECT_LENGTH;
    frame.size.height = CUSTOMCELL_OBJECT_LENGTH;

    CGContextRef context = UIGraphicsGetCurrentContext();    
    
    CGContextSetShadow(context,CGSizeMake(
        CUSTOMCELL_SHADOW_OFFSET, CUSTOMCELL_SHADOW_OFFSET),
            CUSTOMCELL_SHADOW_BLUR);

    [[UIColorwhiteColor] setFill];
    CGContextFillRect(context, frame);
    
}

@end
左上の描画枠外に矩形を描きその影の部分だけ表示するようにするといいあんばいとなる。


ソースコード


GitHub からどうぞ。
CustomCellSample at 2011-06-29b from xcatsan/iOS-Sample-Code - GitHub

その他


すべてのセルに対して開いた時に表示されるビュー(SlideView)を用意するのはメモリ消費の観点から無駄かもしれない。もし改良するとすれば、スワイプが行われた時に SlideView のインスタンスを作り、開く直前にセルへ貼り付ける方法が考えられる。また閉じた後はセルから取り除いて廃棄する。セルの表示情報が多く画面が重くなってしまった時にはこういった方法法は有効だと思われる。


参考情報


カスタムセルのシリーズ

Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方
Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理
Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その3)ボタンの処理[改良版]
今回のコードはこれらをベースに作っている。

グラデーション

Cocoaの日々: [iOS][Mac] Core Graphics - グラデーション
グラデーションの描き方は前回書いた。

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