iOSアプリでドラッグ&ドロップしてみる件について

| コメントをどうぞ

久しぶりにXcodeなぞ利用して、サンプルでも作ってみようかと思い立ったのですが、XCode5.1から普通にプロジェクト作成するとStoryBoardベースになるんですね。
俺のxibを返せ。(xibベースで作るにはEmpty Applicationから作成する必要があります)
DiceK Mikamiです。

そんな試行錯誤はさておき本日はiOSアプリでドラッグ&ドロップのサンプルを作ってみたので、そのメモなど。
ドラッグ&ドロップ自体は様々な場所で紹介されていますが、今回の目標は↓のようなものを考えてみました。

iosdd1_1

1. 環境

  • iOS: 7.1
  • Xcode: 5.1.1
  • Project Template: Single View Application

2. ドラッグオブジェクトの設置

次にドラッグするためのオブジェクトを用意します。
今回は最も基本的な型を示すためにただのViewを利用します。
Main.storyboardを開き、右ペインからViewオブジェクトを2つ最初から設置されているviewの上に設置します。

iosdd2_1

この状態では、まだドラッグはできません。
ドラッグ処理を実装していきます。

3. ドラッグ処理の実装

今回はドラッグ処理にPan Gesture Recognizerを利用します。(touchBegin、touchChangeを利用する方法もあります)
Main.storyboardを開き、右ペインからPan Gesture Recognizerを2で設置したViewオブジェクトに紐づけます。
Pan Gesture Recognizerをドラッグし、設置したViewにドロップすれば紐付けの最初の段階はOKです。

iosdd3_1

ジェスチャーの処理自体は、テンプレートで用意されているViewController.mに記述します。

ViewController.m

- (IBAction)dragGesture:(UIPanGestureRecognizer *)sender {
    // 移動量を取得
    CGPoint point = [sender translationInView:self.view];    

    // 移動量をドラッグしたViewの中心値に加える
    CGPoint movedPoint = CGPointMake(sender.view.center.x + point.x,
                                     sender.view.center.y + point.y);
    sender.view.center = movedPoint;

    // ドラッグで移動した距離を初期化する
    [sender setTranslation:CGPointZero inView:self.view];
}

これで紐付けが終わり、、、ではなくて、何と面倒なことにこの処理を先ほどと同様にViewに紐づける必要があります。

「さっき紐づけたじゃないか!」

となるところでしょうが、先ほどの紐づけは、あくまでも「ジェスチャーを利用する」と言う行為に紐づけただけであって、具体的な処理には紐づいていないのです。
残念ですね。

再度、Main.storyboardを開き、記述した処理の紐づけを行います。
StoryBoardの左ペインからPan Gesture Recognizerを左クリックします。
Sent ActionsのselectorをView Controllerにドラッグ&ドロップします。
すると、dragGesture:が表示されるので、それを選択します。
また、Pan Gesture RecognizerのOutletsにあるdelegateプロパティは、Viewに設定します。
これもドラッグ&ドロップでOKです。
* 今回はドラッグできるオブジェクトを2つ用意しましたので、同様の操作を2つ目のViewに対しても行ってください。

iosdd3_2

4. ドロップゾーンの設置

さて、ドロップできる場所も作ります。
目標では、ドロップできる場所とできない場所を作る予定ですので、まずはドロップ可能領域を用意します。
2で設置したのと同じように設置しましょう。
ドラッグオブジェクトが裏に回らないように、下図のように順番は入れかえておきます。

iosdd4_1

ドロップゾーンは、土台のどの位置にあるのかを知る必要が出てきますので、オブジェクトをコードに登録しておきます。
ViewController.mのインタフェースに以下のように記述します。

ViewController.m

@interface ViewController ()
    // ドロップゾーンのオブジェクト
    @property (weak, nonatomic)UIView *dropZone;
@end

記述しましたら、Main.storyboardを開き、ドロップゾーンのViewを左クリックし、Refarencing OutletsをViewControllerに紐づけます。
紐づける先は作成したdropZoneになります。

iosdd4_2

5. ドロップできない場所を決める。

今のままですと、どこにでもドロップできてしまいます。
ので、ドロップできる場所とできない場所をきちんと設定します。
この処理は、3で記述したViewController.mのジェスチャー処理のところに記述します。

ViewController.m

@interface ViewController ()
    // ドロップゾーンのオブジェクト
    @property (weak, nonatomic)IBOutlet UIView *dropZone;
    // オブジェクトの初期位置を対比しておくための変数を用意する。
    @property CGPoint initPosition;
@end

〜中略〜

- (IBAction)dragGesture:(UIPanGestureRecognizer *)sender {
    // stateプロパティはジェスチャーの状態を監視する
    if(sender.state == UIGestureRecognizerStateBegan){
        // ドラッグオブジェクトの初期値を覚えておく
        _initPosition = sender.view.center;

        // 事前にUse Auto Layoutは外しておくこと
        [self.view bringSubviewToFront:sender.view];
    }

    // 移動量を取得
    CGPoint point = [sender translationInView:self.view];

    // 移動量をドラッグしたViewの中心値に加える
    CGPoint movedPoint = CGPointMake(sender.view.center.x + point.x,
                                     sender.view.center.y + point.y);
    sender.view.center = movedPoint;

    // ドラッグで移動した距離を初期化する
    [sender setTranslation:CGPointZero inView:self.view];

    // ドラッグで移動した距離を初期化する
    [sender setTranslation:CGPointZero inView:self.view];

    if(sender.state == UIGestureRecognizerStateEnded
        || sender.state == UIGestureRecognizerStateFailed
        || sender.state == UIGestureRecognizerStateCancelled){

        // ドロップゾーンの位置と大きさ
        CGRect dropZoneRect = _dropZone.frame;

        // ドラッグするためにタップしている座標を取得
        CGPoint drapPoint = [sender locationInView:self.view];

        if(!CGRectContainsPoint(dropZoneRect, drapPoint)){
            // ドラッグオブジェクトがドロップゾーン以外にドロップされた場合、初期値にもどす。
            sender.view.center = _initPosition;
        }else{
            // dropZone内で整列する
            NSUInteger arrCount = [_dropObjArray count];
            if(arrCount<2 && ![_dropObjArray containsObject:sender.view]){
                switch (arrCount) {
                    case 0:{
                        sender.view.frame = CGRectMake(
                                             dropZoneRect.origin.x+5, 
                                             dropZoneRect.origin.y+12.5,
                                             sender.view.frame.size.width,
                                             sender.view.frame.size.height);
                        break;
                    }
                    case 1:{
                        sender.view.frame = CGRectMake(
                           (dropZoneRect.origin.x+dropZoneRect.size.width)
                                          -(sender.view.frame.size.width+5),
                           dropZoneRect.origin.y+12.5,
                           sender.view.frame.size.width,
                           sender.view.frame.size.height);
                        break;
                    }
                }
                [_dropObjArray addObject:sender.view];
            }else{
                // 2つのドラッグオブジェクトがすでにドロップポイントにある場合
                NSUInteger order = [_dropObjArray indexOfObject:sender.view];

                // Arrayの順番を入れ替える
                CGPoint dragObjCenter = sender.view.center;
                CGRect targetObjRect;
                switch (order) {
                    case 0:{
                        targetObjRect = [[_dropObjArray objectAtIndex:1] frame];
                        break;
                    }
                    case 1:{
                        targetObjRect = [[_dropObjArray objectAtIndex:0] frame];
                        break;
                    }
                }

                // ドラッグオブジェクトの中心点がもう一方のオブジェクトに重なったら入れ替え
                if(CGRectContainsPoint(targetObjRect, dragObjCenter)){
                    [_dropObjArray exchangeObjectAtIndex:0 withObjectAtIndex:1];
                }

                // Arrayの順番に合わせて整頓する
                for(int i=0;i<arrCount;i++){
                    UIView *view = [_dropObjArray objectAtIndex:i];
                    switch (i) {
                        case 0:{
                            view.frame = CGRectMake(dropZoneRect.origin.x+5,
                                                    dropZoneRect.origin.y+12.5,
                                                    view.frame.size.width,
                                                    view.frame.size.height);
                            break;
                        }
                        case 1:{
                            view.frame = CGRectMake(
                                   (dropZoneRect.origin.x+dropZoneRect.size.width)
                                                         -(view.frame.size.width+5),
                                   dropZoneRect.origin.y+12.5,
                                   view.frame.size.width,
                                   view.frame.size.height);
                            break;
                        }
                    }
                }
            }
        }
    }

6. おわりに

解説した分はまだまだドラッグとドロップの基本中の基本ですが、これをもとに改造を施せば大分良くなるかと思います。
特に、今回の例でよろしくない点として、

  • ドラッグオブジェクトが定数。(n個が想定されていない)
  • StoryBoardを利用してジェスチャーを設定しているため、個数分ジェスチャーを必要とする。
  • ドラッグオブジェクトの重なりが考慮されていない。

など色々考えられます。
ドロップゾーンにただのUIViewを利用しているのも良くないですね。(UICollectionViewやUITableViewを利用する方が実践的でしょう)
例に利用したソースコードをGitHubにアップしておきますので、参考までにどうぞ。

GitHub corestrike/ddtext

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>