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

UIAppearance で色や画像を変える

2011年10月31日月曜日 | Published in | 2 コメント

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

UIAppearanceプロトコル


iOS5 から UIAppearance プロトコルが導入された。このプロトコルを使うと UINavigationBar や UISwitch のインスタンスの色を変更することができる。変更の仕方はこんな感じ。
[[UISwitch appearance] setOnTintColor:[UIColor greenColor]];
UIAppearance が有用なのは、上記の通りインスタンスではなくクラスに対して色設定ができること。つまりこれを使うと画面上で使われているすべての UISwitch の色を変えることができる。

例えば緑基調の画面があったとして、UIAppearance プロトコルを使うと別の色(例では赤)に変更することができる。
上記の例は1画面だけだが、アプリで使われるすべての画面で色が変わる。

上記例の色変更の箇所のコードはこんな感じ。
UIColor* color = nil;
    if (index == 0) {
        color = [UIColor colorWithRed:0 green:0.5 blue:0 alpha:1.0];
    } else {
        color = [UIColor colorWithRed:0.5 green:0 blue:0 alpha:1.0];
    }
    [[UINavigationBar appearance] setTintColor:color];
    [[UISegmentedControl appearance] setTintColor:color];
    [[UIProgressView appearance] setProgressTintColor:color];
    [[UISlider appearance] setMinimumTrackTintColor:color];
    [[UISwitch appearance] setOnTintColor:color];
    [[UIActivityIndicatorView appearance] setColor:color];
    [[UITabBar appearance] setTintColor:color];

UIAppearanceを使うとコントロール自体の基本色を実行中に切り替える事ができる。


所属ビューの限定


そのビューが所属している親ビューを条件に指定することができる。例えば UINavigationBar に配置されている UIBarButtonIem だけ色を変えたい場合は appearanceWhenContainedIn: を使ってこう書く。
[[UIBarButtonItem appearanceWhenContainedIn:
    [UINavigationBar class], nil] setTintColor:color];
親クラスは複数指定することができる。

appearance と appearanceWhenContainedIn: を比べた例。
[[UIBarButtonItem appearance] setTintColor:color];
この場合、UINaigationBar と UIToolBar 上の UIBarButtonItem の色が共に変更されている。

これを UINavigatorBar 限定にしてやるとこうなる。
[[UIBarButtonItem appearanceWhenContainedIn:
    [UINavigationBar class], nil] setTintColor:color];



サブビューへの適用


UINavigationBar や UIToolBar の色を変えるとそのサブビューである UIButtonBarItem の色も変更される。

[[UINavigationBar appearance] setTintColor:color];

ボタンを別の色に変えたい場合は明示的に指定してやる。
[[UINavigationBar appearance] setTintColor:color];
[[UIBarButtonItem appearance] setTintColor:blueColor];


UILabel


UILablel の文字色を変えると UITextField や UIButton の文字色も変わる。

これは UIButton 等が文字表示の為に UILabel をサブビューとして持っている為(例えば UIButton の場合、UIButtonLabel::UILabel を使っている)。UIButton の他にも UITextField が UISearchBar で使われていたりと他のコントロールを内包しているケースが多々ある。この為、UILabel や UItextField など基本的なパーツを使う場合は注意が必要。


ビュー階層内の優先順位


サブビューの設定が優先される。例えば UINavigationBar と UIBarButtonItem の両方で tintColorを設定した場合、UIBarButtonItem はそれ自身に設定した色のが優先される。
[[UINavigationBar appearance] setTintColor:color];
[[UIBarButtonItem appearance] setTintColor:blueColor];
呼び出し順は関係ないので逆にしてもこれは変わらない。
[[UIBarButtonItem appearance] setTintColor:blueColor];
[[UINavigationBar appearance] setTintColor:color];


画像の切替


UIAppearance による切替は色に限らず他の属性にも適用できる。例えば背景画像の切替をやって見る。
if (index == 0) {
        backImage = [UIImage imageNamed:@"back1.png"];
    } else {
        backImage = [UIImage imageNamed:@"back2.png"];
    }

    [[UIButton appearance] setBackgroundImage:backImage forState:UIControlStateNormal];

ボタンを画像で用意している場合や、ビューにテクスチャを貼っている場合など、あらかじめ複数種類の画像セットを用意しておけば簡単に切替ができる。


UIView


UIAppearanceプロトコル適用の宣言は UIView で行われている。
UIKIT_CLASS_AVAILABLE(2_0) @interface UIView : UIResponder
  <NSCoding, UIAppearance, UIAppearanceContainer> {
  :
この為、UIView の appearance に対して設定を行うと画面に表示されるすべてのビューに対して変更が適用される。例えば
[[Uiview apperance] setbackgroundcolor:[UIColor whitecolor]];
とすると、こうなる。


実際のデザインよりはデバッグ目的での利用が多そう。


備考


色を設定した後の再描画方法がわからなかった。-[UIView setNeedsDisplay] を performSelector:@selector(setNeedsDisplay)object:delay: を使ってみたがうまくいかなかった。この理由によりサンプルでは下のタブバーの色だけが切り替わらない。※もしご存知の方がいたら是非教えて下さい。


サンプル


検証に使ったサンプルソースはこちら。
UIAppearanceSample at 2011-10-31 from xcatsan/iOS5-Sample - 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 - グラデーション
グラデーションの描き方は前回書いた。

[iOS] Responder Chain と UIViewController

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

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

@novi_ さんの指摘をきっかけに UIViewController が UIResponder のサブクラスで Responder Chain に参加していることを知った(今更だが。。)。

Responder Chain


Responder Chain とは UIResponderオブジェクトの連なり(リンク)のことで、イベント処理の順番を決定するのに使われる。イベントが発生すると、システムは Redponder Chain の順番にしたがって UIResponderオブジェクトを評価する。もし最初の UIResponderオブジェクトでイベント処理が行われない場合は Responder Chain の次の UIResponderオブジェクト へと移り再び評価を行う。イベント処理を行う UIResponderオブジェクトが見つかるまで Responder Chain 上のリンクを辿って評価を続けていく。一旦どこかの UIResponderオブジェクトがイベントを処理するとイベントの伝搬はそこで終わる。Responder Chain 上のどの UIResponderオブジェクトも処理を行わない場合はやがては最終的に UIApplication へ到達し、そこでも処理されない場合はそのイベントは処理されず無視される。

例えばボタンがタップされた場合、タップイベントを最初に処理するのは当該ボタンだが、そこでイベント処理が行われない場合、そのボタンが配置されている superview が評価される。もし superview にイベント処理コードが存在すれば、そこで処理が実行されてイベントの伝搬は終わる。もしイベント処理コードが superview上に無ければ、その次の UIWindow, UIApplication と Responder Chain が次々と辿られる。
なお明示的に Responder Chain を利用する場合はイベント登録時のターゲットに nil を渡す。
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.button addTarget:nil 
                    action:@selector(touchMe:)
          forControlEvents:UIControlEventTouchUpInside];
}
これを nilターゲットと呼ぶ(動作も含めてたしか「nilターゲットアクション」と呼んだりもした)。この仕組は Mac OS X 由来で昔からある(Next由来か)。


Responder Chain と UIViewController


UIViewController は紐づいている UIView が参加している Responder Chain に割り込んで参加している。
UIResponder のサブクラスなのはこの Responder Chain に参加させる為だと思われる。nilターゲットをうまく使うことで紐づいている UIView 上のイベントをすべて UIViewController で集約的に処理することが可能になる。IBActionを宣言してXib上で接続する手間もない(そっちはそっちで明示的にターゲット・アクションの関係がわかりやすいくなるので良い面もあるが)。


※上の図は下記ページより借用
Event Handling Guide for iOS: Event Types and Delivery


サンプル


Responder Chain の確認と nilターゲットアクションが確認できるサンプルプログラムを作ってみた。
"display" ボタンを押すと、そのボタンが参加している Responder Chain をログに出力する。
- (void)_displayResponderChain:(UIResponder*)responder
{
    if (responder != nil) {
        NSLog(@"%@", responder);
        [self _displayResponderChain:[responder nextResponder]];
    }
}
- (IBAction)display:(id)sender {
    [self _displayResponderChain:sender];
    
}
再帰的に Responder Chain を辿って表示させる。結果はこんな感じ。
<UIRoundedRectButton: 0x4e13: 0x4e13c70>
 <UIView: 0x4e12f40;>
 <ResponderChainStudyViewController>
 <UIWindow: 0x4b44b30>
 <UIApplication: 0x4e012b0>
イベントの発生源であるボタンを起点に→superview→UIViewController→UIWindow→UIApplication と連なっているのがわかる。UIViewController は UIView と UIWindow の間に入り込んでいる。

次に nilターゲットアクション。
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.button2 addTarget:nil 
                 action:@selector(touchMe:)
       forControlEvents:UIControlEventTouchUpInside];
}
としておき、UIViewController にイベントハンドラを書いておく。
- (void)touchMe:(id)sender
{
    NSLog(@"%s|%@", __PRETTY_FUNCTION__, sender);
    
}
すると "touch me !"ボタンが押されたときにそのイベントハンドラが呼び出され、Responder Chain が辿られていることがわかる。


ソースコード


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


参考情報


Event Handling Guide for iOS: Event Types and Delivery

Responder Chain の解説ならびに UIViewController との関係が説明されている。

[iOS] UINib を使ったカスタム UITableViewCell の作り方(その3)ボタンの処理[改良版]

| Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理

前回のコードで何番目のセルがタップされたかどうかを判断するのに View階層上で特定のクラスに所属するインスタンスを検索する処理をしていた。
- (IBAction)didTouchDoitButton:(id)sender
{
    id cell = sender;
    while (![cell isKindOfClass:[CustomCell class]]) {
        cell = [cell superview];
    }
    NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
    NSLog(@"%@", indexPath);
}

その後、UITableView を見ていると特定のポイント(CGPoint)から何番目のセルを指しているかどうかを取得するメソッドがあるのに気がついた。
UITableView Class Reference - indexPathForRowAtPoint:
- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point
これを使うとクラスの検索は不要でもっと汎用的に書ける。こんな感じ。
- (IBAction)didTouchDoitButton:(id)sender event:(UIEvent*)event
{
    UITouch* touch = [[event allTouches] anyObject];
    CGPoint p = [touch locationInView:self.tableView];
    NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:p];
    NSLog(@"%@", indexPath);
}
こっちの方が良さそうだ。

なおボタンのイベントハンドラは引数が0〜2個のいずれかを取るメソッドとして実装が可能で、必要に応じて使い分けられる。
-(IBAction)didTouch;
-(IBAction)didTouch:(id)sender;
-(IBAction)didTouch:(id)sender (UIEvent*)event;
今回はタッチ位置を取得する必要があったので引数に UIEvent を含む方を使った。

[iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理

2011年6月25日土曜日 | Published in | 0 コメント

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

[参考] Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方

カスタムセルにボタンを貼りつけた時の処理パターンの紹介。

処理パターン


ボタンのタップイベントをどこで受け取るべきか。カスタムセルで受け取るのも手だが単なるビューなのでモデルとのつながりが無い。モデルとの接点があるのは ViewController なのでここでイベントを受け取るのが順当だろう。


実装


まず ViewController に IBAction を追加する。
@interface RootViewController : UITableViewController {
    CGFloat cellHeight_;
}
// cell events
- (IBAction)didTouchDoitButton:(id)sender;
@end

続いて Xib へボタンを追加する。

ここで Xib の File's Owner に RootViewController を指定する。

そしてタップアクションのターゲットとして先ほど用意した IBAction へ接続する。


次にカスタムセルの生成コードに手を入れる。前回は -[UINib instantiateWithOwner:options:] の owner引数に nil を渡していた。これを self(=RootViewController)とする。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CustomCell* cell = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:CUSTOM_CELL_NIB];
    if (cell == nil) {
        UINib* nib = [UINib nibWithNibName:CUSTOM_CELL_NIB bundle:nil];
//      NSArray* array = [nib instantiateWithOwner:self options:nil];   // 前回
        NSArray* array = [nib instantiateWithOwner:nil  options:nil];   // 今回
          :
    }
       :
こうすることで Xib がインスタンス化された時に File's Owner が RootViewController のインスタンスとなり、ボタンがタップされた時に所定のメソッドが呼び出されるようになる。

最後にボタンタップ時の処理。
- (IBAction)didTouchDoitButton:(id)sender
{
    id cell = sender;
    while (![cell isKindOfClass:[CustomCell class]]) {
        cell = [cell superview];
    }
    NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
    NSLog(@"%@", indexPath);
}
タップされたセルがどのモデルと紐づくかを知るには NSIndexPath が必要になるので -[UITableView indexPathForCell:] を使う。ただ、ボタンタップ時に呼び出されるこのメソッドの引数 sender には UIButton のインスタンスが入っているだけなので、ここからCustomCell のインスタンスを手に入れる必要がある。UIButton はカスタムセルの子ビュー(sub view) なので、逆に UIButton のビュー階層を上に辿っていって CustomCellクラスのインスタンスを探せば良い。

(2011-06-27追記)クラス検索は使わずに、タッチ位置から NSIndexPathを取得する方法が見つかった。そちらの方が汎用性があり実用的なので参照されたい。
Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その3)ボタンの処理[改良版]

ソースコード


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


参考情報

なし

[iOS] UINib を使ったカスタム UITableViewCell の作り方

2011年5月22日日曜日 | Published in | 2 コメント

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

2011-06-09 追記 UITableViewCell の Identifier 設定を忘れてたので追記しました。

UINib を使うと簡単に Nib で定義した UITableViewCell が使える。
今回のサンプル:


[関連] Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理


実装


まず CustomCell.xib を作る。

ここで表示したいセルのデザインを行う。UITableViewCell のクラスに "CustomCell" を指定しておく。

(2011-06-09 追記)
Identifier には "CustomCell" を設定しておく。この文字列は自由に付けられるがソースコード上の該当箇所と一致させておく。設定しないとセルの再利用が行われない。


次に CustomCell クラスの実装(*.m/*.h)を作成する。こんな感じ。
CustomCell.h
@interface CustomCell : UITableViewCell {
    
}
@property (nonatomic, retain) IBOutlet UILabel* nameLabel;
@property (nonatomic, retain) IBOutlet UILabel* dateLabel;
@property (nonatomic, retain) IBOutlet UILabel* descLabel;
@property (nonatomic, retain) IBOutlet UIImageView* imageView;

@end

CustomCell.m
@implementation CustomCell
@synthesize nameLabel;
@synthesize dateLabel;
@synthesize descLabel;
@synthesize imageView;

- (void)dealloc
{
    self.nameLabel = nil;
    self.dateLabel = nil;
    self.descLabel = nil;
    self.imageView = nil;
    [super dealloc];
}

@end
今回は特別な実装はやらないので単にプロパティ値のプレースホルダとして働く。

再び xib ファイルへ戻り、アウトレットの接続を行う。

セルの準備ができたら今度は UITableView の Datasource/Delegate の実装。

RootViewController.m
#define CUSTOM_CELL_NIB @"CustomCell"

- (UITableViewCell *)tableView:(UITableView *)tableView
   cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CustomCell* cell = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:CUSTOM_CELL_NIB];
    if (cell == nil) {
        UINib* nib = [UINib nibWithNibName:CUSTOM_CELL_NIB bundle:nil];
        NSArray* array = [nib instantiateWithOwner:nil options:nil];
        cell = [array objectAtIndex:0];
    }

    // Configure the cell.
    int r = rand();
    cell.nameLabel.text = [NSString stringWithFormat:@"NAME-%d", r];
    cell.dateLabel.text = [[NSDate dateWithTimeIntervalSince1970:r] description];
    cell.descLabel.text = @"Permission is hereby granted, free of charge, to any
 person obtaining a copy of this software and associated documentation files ....";
    cell.imageView.image = [UIImage imageNamed:
       [NSString stringWithFormat:@"image%02ds.jpg", (r%8)+1]];
    NSLog(@"%@", [NSString stringWithFormat:@"image%02ds.jpg", (r%8)+1]);
    return cell;
}
ポイントは UINib を使っているところ。
UINib* nib = [UINib nibWithNibName:CUSTOM_CELL_NIB bundle:nil];
        NSArray* array = [nib instantiateWithOwner:nil options:nil];
        cell = [array objectAtIndex:0];
nibwithNibName:bundle: で xib ファイルを読み込み、instantiateWithOwner:options: でインスタンス化する。この時、owner は使わないので指定しない(nil)。今回の用途ではこれでも問題ない。CustomCell クラスのインスタンスを得るのには決め打ちで objectAtIndex:0 としている。もし xib に複数のトップレベルオブジェクトが存在する場合は区別する必要がある。なお UINib は xib ファイルをキャッシュするので繰り返し使っても読み込み自体は大きなオーバーヘッドにならない。以前のカスタムセル作成には UIViewController を使った方法があったが、それに比べると余計なインスタンス(UIViewController)を作らず、キャッシュも効くのでこちらのほうが軽量だと思われる。

なお CustomCell の高さは通常よりも大きくしてあるのでデリゲートを実装して置く必要がある。
- (void)viewDidLoad
{
    self.title = @"Custom Cell Sample";
    
    // calc height of custom cell
    UINib* nib = [UINib nibWithNibName:CUSTOM_CELL_NIB bundle:nil];
    NSArray* array = [nib instantiateWithOwner:nil options:nil];
    CustomCell* cell = [array objectAtIndex:0];
    cellHeight_ = cell.frame.size.height;
    [super viewDidLoad];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return cellHeight_;
}
今回は viewDidLoad でセルの高さをメンバ変数へ取っておく方法をとった。あらかじめセルの高さが固定であればソースコード上に直接数値を指定して返しても良い。


ソースコード


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


関連情報


カスタムセルシリーズ

Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その2)ボタンの処理

Cocoaの日々: [iOS] UINib を使ったカスタム UITableViewCell の作り方(その3)ボタンの処理[改良版]

スライド実装

Twitter公式アプリなどで実装されているセルのスライドアニメーションの実装など。
Cocoaの日々: [iOS] UITableView でセルをスワイプするとスライドするユーザインタフェースを実装

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