iOSのドラッグ＆ドロップ（一歩先のドラッグ編）

「[基本編](//qiita.com/hironytic/items/a8ffdf37cfa768d92ced)」ではドラッグおよびドロップの最低限の実装のみを行いました。
しかし、ドラッグに関してもっと細かく制御を行うことができます。

ここではUIDragInteractionDelegateで定義された他のいくつかのメソッドについて、これを実装することで何ができるのか、どのように実装するのかを解説します。

※実装例として登場するコードは、基本編の続きになっています。基本編のコードも参考にしてください。

## プレビューの制御

デフォルトでは長押しして浮き上がるプレビューは、 UIDragInteractionを追加した対象のビュー全体になります。

ビューそのものがドラッグ対象である場合はそれで問題ないのですが、ビュー内の一部がドラッグ対象である場合には直感的な動作とは言えません。
このプレビュー `dragInteraction(_:previewForLifting:session:)` を実装することによりカスタマイズできます。

また、プレビューをドラッグしている状態から、ドロップできないところで指を離すとドラッグはキャンセルされます。
このとき、デフォルトではドラッグ中のプレビューがその場で少しずつ大きくなりながら徐々に消えるアニメーションが行われます。
この挙動は `dragInteraction(_:previewForCancelling:withDefault:)` を実装することで変更できます。
例えば、プレビューのターゲットを変更することで、ドラッグ元の位置へ戻っていくようなアニメーションが可能です。

before
![preview1.gif](https://qiita-image-store.s3.amazonaws.com/0/33397/0e9c7a39-157b-ee6e-e089-bc72e9f67e43.gif)

after
  ![preview2.gif](https://qiita-image-store.s3.amazonaws.com/0/33397/e12bdb2b-e583-d741-9f54-55bce373d82b.gif)


### dragInteraction(_:previewForLifting:session:)

このメソッドで返すUITargetedDragPreviewオブジェクトは、次の3つから作成します。

- `view: UIView` …… プレビューになるビュー
- `parameters: UIDragPreviewParameters` …… 背景色、プレビューの形状
- `target: UIDragPreviewTarget` …… プレビューの出現位置を、特定のビューとそのビュー内の座標で指定したもの

この中で最低限必要なのは1つ目のプレビューとなるUIViewです。
このビューがすでにビュー階層に存在するビューであれば、他の2つは省略できます。

#### 実装例1

ドラッグ対象のラベルをプレビューにする実装の例

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     previewForLifting item: UIDragItem,
                     session: UIDragSession) -> UITargetedDragPreview? {
    // ドラッグ対象のラベル
    // dragInteraction(_:itemsForBeginning:)で返すUIDragItemのlocalObjectに
    // あらかじめ紐付けておいたラベルです。
    guard let label = item.localObject as? UILabel else { return nil }
    return UITargetedDragPreview(view: label)
}
```

ビュー階層には存在しないビューを指定する場合は、3つ目のUIDragPreviewTargetの指定が必須になります。

#### 実装例2

プレビュー用にラベルを新規作成する実装の例

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     previewForLifting item: UIDragItem,
                     session: UIDragSession) -> UITargetedDragPreview? {
    // ドラッグ対象のラベル
    // dragInteraction(_:itemsForBeginning:)で返すUIDragItemのlocalObjectに
    // あらかじめ紐付けておいたラベルです。
    guard let label = item.localObject as? UILabel else { return nil }

    let previewView = UILabel()
    previewView.text = "🚚" + (label.text ?? "")
    previewView.font = UIFont.systemFont(ofSize: 42)
    previewView.sizeToFit()
    
    let target = UIDragPreviewTarget(container: draggableView, center: label.center)

    return UITargetedDragPreview(view: previewView,
                                  parameters: UIDragPreviewParameters(),
                                  target: target)
}
```

### dragInteraction(_:previewForCancelling:withDefault:)

このメソッドで返すのは先ほど説明したdragInteraction(_:previewForLifting:session:)と同じUITargetedDragPreviewです。
ただし、今回は第3引数にデフォルトのUITargetedDragPreviewが渡されます。
このデフォルトのプレビューをそのまま返すと、ドラッグ対象のビューの中心部へプレビューが戻っていきます。
ここでデフォルトプレビューの `retargetedPreview(with:)` を使ってターゲット（UIDragPreviewTarget）だけ変更したものを返すことも可能です。
もちろん、全く新しいUITargetedDragPreviewを返しても構いませんし、
nilを返せば標準の挙動（その場で少しずつ大きくなりながら徐々に消える）になります。

#### 実装例

この例ではキャンセル時にドラッグ元のラベルへ戻っていくようにアニメーションするプレビューの挙動になります。

- `dragInteraction(_:previewForCancelling:withDefault:)`

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     previewForCancelling item: UIDragItem,
                     withDefault defaultPreview: UITargetedDragPreview) -> UITargetedDragPreview? {
    guard let label = item.localObject as? UILabel else { return defaultPreview }
    let target = UIDragPreviewTarget(container: label.superview!, center: label.center)
    return defaultPreview.retargetedPreview(with: target)
}
```





## 浮き上がるときに何かする

長押ししてドラッグを始めるときに、例えばドラッグ元のアイテムを淡色表示にするなど、プレビューが浮き上がったときに何かを行いたい場合があります。
そのためには、次のメソッドを実装しましょう。

- `dragInteraction(_:willAnimateLiftWith:session:)`
- `dragInteraction(_:item:willAnimateCancelWith:)`
- `dragInteraction(_:session:didEndWith:)`

![animateLiftWith.gif](https://qiita-image-store.s3.amazonaws.com/0/33397/3d76e724-19d7-c127-007f-5bd497f0368f.gif)

### dragInteraction(_:willAnimateLiftWith:session:)

名前が表すとおり、このメソッドがプレビューの浮き上がるアニメーションの直前に呼ばれるものです。
第2引数に渡されるUIDragAnimatingオブジェクトを使って、浮き上がるタイミングで一緒に行うアニメーションを設定できます。
また浮き上がったタイミングを検出して何か行うことができます。

#### 実装例

浮き上がったときにドラッグ元のラベルを半透明にする例です。

```swift

func dragInteraction(_ interaction: UIDragInteraction,
                     willAnimateLiftWith animator: UIDragAnimating,
                     session: UIDragSession) {
    animator.addCompletion { (pos) in
        if pos == .end {
            // session.itemsにはドラッグ中のアイテムが全部入っています。
            // このタイミングなら、後述のドラッグ中のアイテムに追加されたときにも
            // 追加されたものがsession.itemsに入っています。
            for item in session.items {
                if let label = item.localObject as? UILabel {
                    label.alpha = 0.5
                }
            }
        }
    }
}
```


### dragInteraction(_:item:willAnimateCancelWith:)

こちらはドラッグがキャンセルされたときのアニメーションに合わせて行う処理を記述します。dragInteraction(_:willAnimateLiftWith:session:)とは異なり、セッションではなくアイテムごとに呼ばれます（なぜでしょうね？）。

#### 実装例

ドラッグ元のラベルの透明度を元に戻す例です。

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     item: UIDragItem,
                     willAnimateCancelWith animator: UIDragAnimating) {
    if let label = item.localObject as? UILabel {
        animator.addAnimations {
            label.alpha = 1
        }
    }
}
```

### dragInteraction(_:session:didEndWith:)

このメソッドはドラッグのセッションが終了されたときに呼ばれます。つまり、ドラッグがキャンセルされたときも、ドロップされたときにも呼ばれます。ここで元の状態に戻しておくのが確実でいいでしょう。

#### 実装例

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     session: UIDragSession,
                     didEndWith operation: UIDropOperation) {
    for item in session.items {
        if let label = item.localObject as? UILabel {
            label.alpha = 1
        }
    }
}

```



## ドラッグ中のアイテムに追加する

ドラッグ開始時にdragInteraction(_:itemsForBeginning:)で複数のUIDragItemを返すことで複数のアイテムをドラッグすることができますが、一度ドラッグを開始したあとでも、ドラッグしているのとは別の指でアイテムをタップすることで現在のドラッグにそれを追加させることができます。

これを実現するには、 `dragInteraction(_:itemsForAddingTo:withTouchAt:)` を実装します。

![itemsForAddingTo2.gif](https://qiita-image-store.s3.amazonaws.com/0/33397/6db0d06c-ec86-6cfa-af0c-d1d4cc71b955.gif)

### dragInteraction(_:itemsForAddingTo:withTouchAt:)

タップされた場所が対象のビューの座標系で渡されます。このメソッドで行うことはdragInteraction(_:itemsForBeginning:)とほとんど変わりません。
ただし、すでにドラッグ中のアイテムであっても追加されてしまうので、それを好ましくないと考える場合は、チェックして弾くのがいいでしょう。
また、ドラッグ中のセッションのアイテムをチェックして、追加するかどうかを決めることもできます。

#### 実装例

タップされた位置にあるラベルの文字列をドラッグに追加する例です。
ここではタップしたラベルがすでにドラッグ中なら追加しないようにしています。
また、文字列として扱えないものをドラッグしている場合にも追加しないようにしています。

```swift
func dragInteraction(_ interaction: UIDragInteraction,
                     itemsForAddingTo session: UIDragSession,
                     withTouchAt point: CGPoint) -> [UIDragItem] {
    // タップされた位置にあるラベルの文字列をドラッグします。
    if let hitView = draggableView.hitTest(point, with: nil) {
        if let label = hitView as? UILabel {
            // すでにドラッグ中のものは追加しません。
            if session.items.contains(where:{ ($0.localObject as? UILabel) == label }) {
                return []
            }

            // 文字列として扱えないものをドラッグしているところには
            // 追加しません。
            if session.items.contains(where: { !$0.itemProvider.canLoadObject(ofClass: NSString.self) }) {
                return []
            }

            let text = (label.text ?? "") as NSString
            let dragItem = UIDragItem(itemProvider: NSItemProvider(object: text))
            dragItem.localObject = label  // ドラッグ対象を紐付けておく
            return [dragItem]
        }
    }

	  // タップ位置にラベルがなければドラッグ可能ではありません。
    return []
```

## See Also
- [iOSのドラッグ＆ドロップ（基本編）](//qiita.com/hironytic/items/a8ffdf37cfa768d92ced)
- [iOSのドラッグ＆ドロップ（一歩先のドロップ編）](//qiita.com/hironytic/items/8dfbfabe1c71947ddb45)