SplitViewで、複数のアプリを同時に表示するメリットの一つが、他アプリのデータをDrag and Dropでコピーできること。
UITextField, UITextAreaは、デフォルトでテキストのDnDに対応している様子。
今回対応中のレシピアプリでは、画像が登録できる機能があるので、他アプリから画像をDropで追加できるようにしてみた。
Drag and Dropの仕組みは、iOS 11以上のUITableView, UICollectionView で実装が可能。
Dropの実装
使用するのは、UITableViewDropDelegate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { | |
if session.canLoadObjects(ofClass: UIImage.self) { | |
return true | |
} else { | |
return false | |
} | |
} |
Dropの処理は、以下のように実装した。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { | |
if coordinator.items.count == 0 { return } | |
let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: editImages.images.count, section: 0) | |
let cellImageWidth = tableView.frame.size.width - tableView.layoutMargins.left - tableView.layoutMargins.right | |
for item in coordinator.items { | |
let itemProvider = item.dragItem.itemProvider | |
if !itemProvider.canLoadObject(ofClass: UIImage.self) { continue } | |
itemProvider.loadObject(ofClass: UIImage.self) {(object, error) in | |
guard let image = object as? UIImage else { return } | |
let cellHeight = image.size.height * (cellImageWidth / image.size.width) + 5 | |
let placeholder = UITableViewDropPlaceholder(insertionIndexPath: destinationIndexPath, reuseIdentifier: "Cell", rowHeight: cellHeight) | |
placeholder.cellUpdateHandler = { cell in | |
if let imageCell = cell as? SURImageTableViewCell { | |
imageCell.iconView.image = image | |
} | |
} | |
DispatchQueue.main.async { | |
let placeholderContext = coordinator.drop(item.dragItem, to: placeholder) | |
placeholderContext.commitInsertion { insertionIndexPath in | |
self.insertImage(image, indexPath: insertionIndexPath) | |
tableView.insertRows(at: [insertionIndexPath], with: .none) | |
placeholderContext.deletePlaceholder() | |
} | |
} | |
} | |
} | |
coordinator.session.progressIndicatorStyle = .none | |
} | |
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { | |
if session.localDragSession != nil { | |
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) | |
} else { | |
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath) | |
} | |
} |
Dragの実装
Dragを実装すると、外部アプリへデータをドラッグできるようになる。使用するのはUITableViewDragDelegate。itemsForBeginning で、受け渡したい情報を設定する。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { | |
let itemProvider = NSItemProvider(object: "sample name" as NSString) | |
return [UIDragItem(itemProvider: itemProvider)] | |
} |
DragDelegateとともに、tableView(_:canMoveRowAt:)、tableView(_:moveRowAt:to:) などを実装すると、テーブル内のセルの順番をDnDで操作することも可能になる。
以下は、同じSection内に限りDnDで行を入れ替えることができる実装例。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { | |
return true | |
} | |
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { | |
if sourceIndexPath.section != destinationIndexPath.section { return } | |
SURDataManager.shared.move(from: sourceIndexPath, to: destinationIndexPath) | |
} | |
func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { | |
if sourceIndexPath.section != proposedDestinationIndexPath.section { | |
var row:Int = 0 | |
if sourceIndexPath.section < proposedDestinationIndexPath.section { | |
row = tableView.numberOfRows(inSection: sourceIndexPath.section) - 1 | |
} | |
return IndexPath(row: row, section: sourceIndexPath.section) | |
} | |
return proposedDestinationIndexPath | |
} | |
//MARK:- UITableViewDragDelegate implements | |
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { | |
return [] | |
} |
外部アプリにデータを渡さないのであれば、itemsForBeginningの戻り値は設定なしでOK。
ただし、tableView(_:moveRowAt:to:) の処理中で、tableView.reloadSections で更新すると、次回から行移動ができなくなるバグ?があるみたいなので注意。tableView.reloadData() だと大丈夫のようだ。
iPadだと、登録がかなり楽になる! 素敵!
次回につづく。