[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 - グラデーション
グラデーションの描き方は前回書いた。

Responses

Leave a Response

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