ドラッグ&ドロップの概要
iOS 11からOSレベルでドラッグ&ドロップがサポートされるようになりました。
ユーザー視点では、ドラッグ&ドロップは次のように動作します。
- ドラッグ可能なものを長押しするとドラッグプレビューが浮き上がってくる。
- 浮き上がったあと、指を付けたまま移動すると、プレビューが指について移動する(ドラッグ)。
- ドラッグしている状態で、別の指で他のドラッグ可能なものをタップすると、それをドラッグに追加できる。
- ドロップできる位置に持っていき、指を離してドロップ(またはドロップできないところで指を離してキャンセル)
なお、iPadではアプリをまたがってドラッグ可能ですが、iPhoneではドラッグできるのは同じアプリ内のみです。
ドラッグ側の最低限の実装
UIDragInteractionDelegateに準拠したクラスのオブジェクトを作成し、これをドラッグを実現したいビューに紐付けます。
let dragDelegate: UIDragInteractionDelegate = ...
let dragInteraction = UIDragInteraction(delegate: dragDelegate)
dragInteraction.isEnabled = true // iPhoneの場合はデフォルトがfalseになっている
view.addInteraction(dragInteraction)
(↓2018-02-15 追記)
iPhoneでは、UIDragInteraction
の isEnabled
がデフォルトではfalseになっているのでtrueにする必要があります。
※デフォルトは UIDragInteraction.isEnabledByDefault
になっていて、iPhoneだとこの値がfalseになっているというのが、より正しい表現かもしれません。
ドラッグを行うためには、UIDragInteractionDelegateに準拠したクラスで、 dragInteraction(_:itemsForBeginning:)
を実装します。
dragInteraction(_:itemsForBeginning:)
このメソッドはドラッグのジェスチャー(長押し)を検出したときに呼ばれます。第2引数のUIDragSessionのメソッドからはドラッグが行われた点を知ることができます。その点に対してドラッグ可能なものがあれば、それを表すUIDragItemを配列に入れて返します。ドラッグ可能なものがなければ空の配列を返します。
実装例1
ビューがドラッグされたら単純に固定文字列をデータとしてドラッグする実装の例です。
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
let text = "うぇーいww" as NSString
return [UIDragItem(itemProvider: NSItemProvider(object: text))]
}
実装例2
ビュー内のドラッグ位置にラベルがあれば、そのラベルの文字列をデータとしてドラッグする実装の例です。
UIDragItemの localObject
にはドラッグを開始したアプリ内でのみ利用可能な情報をセットしておくことができます。
ここではドラッグ対象のラベルを紐付けています。 最低限の実装には必要ありませんが、一歩進んだことをやろうとすると必要になるでしょう。(これは「一歩先のドラッグ編」で利用します。)
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
// ドラッグされた位置を取得します
let point = session.location(in: draggableView)
// ドラッグされた位置にラベルがあれば、そのラベルの文字列をドラッグします。
// Note: UILabelをhitTestで見つけるためには、ラベルのuserInteractionEnabledを
// trueにしておく必要があります。
if let hitView = draggableView.hitTest(point, with: nil) {
if let label = hitView as? UILabel {
let text = (label.text ?? "") as NSString
let dragItem = UIDragItem(itemProvider: NSItemProvider(object: text))
dragItem.localObject = label // ドラッグ対象を紐付けておく
return [dragItem]
}
}
// ドラッグ位置にラベルがなければドラッグ可能ではありません。
return []
}
ドロップ側の最低限の実装
ドラッグ時と同様に、UIDropInteractionDelegateに準拠したクラスのオブジェクトを作成し、これをドロップを実現するビューに紐付けます。
let dropDelegate: UIDropInteractionDelegate = ...
let dropInteraction = UIDropInteraction(delegate: dropDelegate)
view.addInteraction(dropInteraction)
UIDropInteractionDelegateに準拠したクラスでは、ドロップを実現するために、少なくとも次の2つのメソッドの実装が必要です。
dropInteraction(_:sessionDidUpdate:)
dropInteraction(_:performDrop:)
dropInteraction(_:sessionDidUpdate:)
このメソッドはドラッグ中のアイテムがビューの領域に入ったり、ビューの領域内で動いたりすると呼ばれます。また、ドラッグ中のアイテムが増えたときにも呼ばれます。このメソッドでは、ドラッグ中のアイテムにドロップ可能なデータが含まれているかどうかを調べたり、ドラッグ中の位置が、ドロップできるところかどうかを判定したりします。そして、その結果に応じてUIDropProposalオブジェクトを返します。
UIDropProposalオブジェクトは operation
プロパティを持っていて、これによりドロップ可能かどうかが決まります。
UIDropOperation | ドロップ可否 | 説明 |
---|---|---|
.cancel |
不可 | ユーザーが指を離しても何もドロップされないことを意味します。 |
.forbidden |
不可 | キャンセルと同じです。 通常はドロップを受け付ける操作ですが、一時的にドロップできないとか、この位置にはドロップできないというような場合に使います。 |
.copy |
可 | ユーザーが指を離すとドラッグ元のコピーがドロップされることを意味します。 |
.move |
可 | ユーザーが指を離すとドラッグ元が移動されることを意味します。 |
実装例
func dropInteraction(_ interaction: UIDropInteraction,
sessionDidUpdate session: UIDropSession) -> UIDropProposal {
// ドロップできない場所では .forbidden を返します。
let point = session.location(in: droppableView)
guard forbiddenView != droppableView.hitTest(point, with: nil) else {
return UIDropProposal(operation: .forbidden)
}
// ドラッグ中のアイテムが文字列を含んでいる場合はドロップできます。
if session.canLoadObjects(ofClass: NSString.self) {
return UIDropProposal(operation: .copy)
} else {
return UIDropProposal(operation: .cancel)
}
}
dropInteraction(_:performDrop:)
このメソッドは、ユーザーが指を離して実際にアイテムがドロップされたときに呼び出されます。ここでドロップされたアイテムのデータを実際にロードします。実のところ、このメソッドのタイミングでしか、ドロップされたアイテムをロードすることはできません。
ここで注意が必要なのは、アイテムのロードは非同期に行われるという点です。例えばドラッグ元のデータはクラウド上に存在することもあります。データの転送に時間がかかる場合があるので、ロードは同期的には行われません。ロードが完了するかエラーになると渡した関数がコールバックされます。このコールバックはサブスレッドから呼ばれることがあります。結果をUIに反映するときは、さらにメインスレッドにスイッチさせる必要が出てきます。
実装例
func dropInteraction(_ interaction: UIDropInteraction,
performDrop session: UIDropSession) {
for item in session.items {
// 文字列をロードできないアイテムはスキップします
if item.itemProvider.canLoadObject(ofClass: NSString.self) {
item.itemProvider.loadObject(ofClass: NSString.self) { (object, error) in
// アイテムのロードは非同期に行われます
// ロードが終わるとここにやってきます
if let string = object as? NSString {
// UIへの反映はメインスレッドで行います
DispatchQueue.main.async {
self.infoLabel.text = String(format: "Dropped string - %@", string)
}
}
}
}
}
}