From 5a16019b09350b9d3982ab5d5905ebfd88f5c256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20H=C3=BChne?= Date: Wed, 27 Mar 2019 17:21:37 +0100 Subject: [PATCH] Inter-App drag and drop, Toolbar actions for drag and drop (#318) * #250 implements drag & drop files from outside into the app * - implemented dragging outside the app to other apps - implemented drop to UIToolbarButtonItem * added delegate method to support dragging multiple items outside * - completed drag and drop on toolbar button items - fixed multiple items drop * Fixed items for ActionContext from Multiselection * - removed dead code - fixed code review issues * - locally available files could not used for drag and drop (internally) - prevent dropping folders --- ownCloud.xcodeproj/project.pbxproj | 4 + .../Client/ClientQueryViewController.swift | 243 +++++++++++++----- .../folder.imageset/Contents.json | 16 ++ .../folder.imageset/folder.pdf | Bin 0 -> 1310 bytes .../trash.imageset/Contents.json | 16 ++ .../trash.imageset/trash-25.pdf | Bin 0 -> 1601 bytes .../UIBarButtonItem+Extension.swift | 26 ++ .../UIKit Extensions/UIButton+Extension.swift | 39 +++ 8 files changed, 278 insertions(+), 66 deletions(-) create mode 100644 ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json create mode 100644 ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf create mode 100644 ownCloud/Resources/Assets.xcassets/trash.imageset/Contents.json create mode 100644 ownCloud/Resources/Assets.xcassets/trash.imageset/trash-25.pdf create mode 100644 ownCloud/UIKit Extensions/UIButton+Extension.swift diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index ad6b4c14e..affb51162 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; @@ -473,6 +474,7 @@ 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; @@ -861,6 +863,7 @@ 239F1314205A69240029F186 /* UIKit Extensions */ = { isa = PBXGroup; children = ( + 39104E0A223991C8002FC02F /* UIButton+Extension.swift */, 239F1318205A693A0029F186 /* UIColor+Extension.swift */, 2347446920761BB700859C93 /* String+Extension.swift */, DCE974BB207EACA60069FC2B /* UIImage+Extension.swift */, @@ -1932,6 +1935,7 @@ DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */, 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */, 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */, + 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */, DC422450207CB2500006A2A6 /* NSObject+ThemeApplication.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index 2ba76fdba..573da4a91 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -24,7 +24,7 @@ import Photos typealias ClientActionVieDidAppearHandler = () -> Void typealias ClientActionCompletionHandler = (_ actionPerformed: Bool) -> Void -class ClientQueryViewController: UITableViewController, Themeable { +class ClientQueryViewController: UITableViewController, Themeable, UIDropInteractionDelegate { weak var core : OCCore? var query : OCQuery @@ -49,7 +49,6 @@ class ClientQueryViewController: UITableViewController, Themeable { var queryRefreshControl: UIRefreshControl? let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - var deleteMultipleBarButtonItem: UIBarButtonItem? var moveMultipleBarButtonItem: UIBarButtonItem? @@ -158,12 +157,10 @@ class ClientQueryViewController: UITableViewController, Themeable { self.navigationItem.rightBarButtonItems = [selectBarButton!, uploadBarButton!] // Create bar button items for the toolbar - deleteMultipleBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(actOnMultipleItems)) - deleteMultipleBarButtonItem?.actionIdentifier = DeleteAction.identifier + deleteMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named:"trash"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: DeleteAction.identifier!) deleteMultipleBarButtonItem?.isEnabled = false - moveMultipleBarButtonItem = UIBarButtonItem(barButtonSystemItem: .organize, target: self, action: #selector(actOnMultipleItems)) - moveMultipleBarButtonItem?.actionIdentifier = MoveAction.identifier + moveMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named:"folder"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: MoveAction.identifier!) moveMultipleBarButtonItem?.isEnabled = false self.addThemableBackgroundView() @@ -346,6 +343,11 @@ class ClientQueryViewController: UITableViewController, Themeable { } func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { + for item in session.items { + if item.localObject == nil, item.itemProvider.hasItemConformingToTypeIdentifier("public.folder") { + return false + } + } return true } @@ -369,23 +371,6 @@ class ClientQueryViewController: UITableViewController, Themeable { return configuration } - func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { - let item: OCItem = itemAtIndexPath(indexPath) - - guard item.type != .collection else { - return [] - } - - guard let data = item.serializedData() else { - return [] - } - - let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) - let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item - return [dragItem] - } - func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { if session.localDragSession != nil { @@ -399,10 +384,73 @@ class ClientQueryViewController: UITableViewController, Themeable { return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) } } else { - return UITableViewDropProposal(operation: .forbidden) + return UITableViewDropProposal(operation: .copy) + } + } + + func updateToolbarItemsForDropping(_ items: [OCItem]) { + guard let tabBarController = self.tabBarController as? ClientRootViewController else { return } + guard let toolbarItems = tabBarController.toolbar?.items else { return } + + if let core = self.core { + // Remove duplicates + let uniqueItems = Array(Set(items)) + // Get possible associated actions + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .toolbar) + let actionContext = ActionContext(viewController: self, core: core, items: uniqueItems, location: actionsLocation) + self.actions = Action.sortedApplicableActions(for: actionContext) + + // Enable / disable tool-bar items depending on action availability + for item in toolbarItems { + if self.actions?.contains(where: {type(of:$0).identifier == item.actionIdentifier}) ?? false { + item.isEnabled = true + } else { + item.isEnabled = false + } + } + } + + } + + func tableView(_: UITableView, dragSessionDidEnd: UIDragSession) { + removeToolbar() + self.actions = nil + } + + // MARK: - UIBarButtonItem Drop Delegate + + func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { + return true + } + + func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { + return UIDropProposal(operation: .copy) + } + + func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { + guard let button = interaction.view as? UIButton, let identifier = button.actionIdentifier else { return } + + if let action = self.actions?.first(where: {type(of:$0).identifier == identifier}) { + // Configure progress handler + action.progressHandler = { [weak self] progress in + self?.progressSummarizer?.startTracking(progress: progress) + } + + action.completionHandler = { [weak self] _ in + } + + // Execute the action + action.willRun() + action.run() } } + func dragInteraction(_ interaction: UIDragInteraction, + session: UIDragSession, + didEndWith operation: UIDropOperation) { + removeToolbar() + } + // MARK: - Message var messageView : UIView? var messageContainerView : UIView? @@ -669,8 +717,7 @@ class ClientQueryViewController: UITableViewController, Themeable { removeToolbar() } - @objc func actOnMultipleItems(_ sender: UIBarButtonItem) { - + @objc func actOnMultipleItems(_ sender: UIButton) { // Find associated action if let action = self.actions?.first(where: {type(of:$0).identifier == sender.actionIdentifier}) { // Configure progress handler @@ -916,47 +963,54 @@ extension ClientQueryViewController: UITableViewDropDelegate { guard let core = self.core else { return } for item in coordinator.items { - - var destinationItem: OCItem - - guard let item = item.dragItem.localObject as? OCItem, let itemName = item.name else { - return - } - - if coordinator.proposal.intent == .insertIntoDestinationIndexPath { - - guard let destinationIP = coordinator.destinationIndexPath else { + if item.dragItem.localObject != nil { + var destinationItem: OCItem + + guard let item = item.dragItem.localObject as? OCItem, let itemName = item.name else { return } - - guard items.count >= destinationIP.row else { - return + + if coordinator.proposal.intent == .insertIntoDestinationIndexPath { + + guard let destinationIndexPath = coordinator.destinationIndexPath else { + return + } + + guard items.count >= destinationIndexPath.row else { + return + } + + let rootItem = items[destinationIndexPath.row] + + guard rootItem.type == .collection else { + return + } + + destinationItem = rootItem + + } else { + + guard let rootItem = self.query.rootItem, item.parentFileID != rootItem.fileID else { + return + } + + destinationItem = rootItem + } - - let rootItem = items[destinationIP.row] - - guard rootItem.type == .collection else { - return + + if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in + if error != nil { + Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") + } + }) { + self.progressSummarizer?.startTracking(progress: progress) } - - destinationItem = rootItem - } else { - - guard let rootItem = self.query.rootItem, item.parentFileID != rootItem.fileID else { - return + guard let UTI = item.dragItem.itemProvider.registeredTypeIdentifiers.last else { return } + item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: UTI) { (url, _ error) in + guard let url = url else { return } + self.upload(itemURL: url, name: url.lastPathComponent) } - - destinationItem = rootItem - - } - - if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in - if error != nil { - Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") - } - }) { - self.progressSummarizer?.startTracking(progress: progress) } } } @@ -965,18 +1019,75 @@ extension ClientQueryViewController: UITableViewDropDelegate { extension ClientQueryViewController: UITableViewDragDelegate { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + self.populateToolbar(with: [moveMultipleBarButtonItem!, flexibleSpaceBarButton, deleteMultipleBarButtonItem!]) + + var selectedItems = [OCItem]() + // Add Items from Multiselection too + if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { + if selectedIndexPaths.count > 0 { + for indexPath in selectedIndexPaths { + selectedItems.append(itemAtIndexPath(indexPath)) + } + } + } + for dragItem in session.items { + guard let item = dragItem.localObject as? OCItem else { continue } + selectedItems.append(item) + } + let item: OCItem = itemAtIndexPath(indexPath) + selectedItems.append(item) + updateToolbarItemsForDropping(selectedItems) - guard let data = item.serializedData() else { - return [] + guard let dragItem = itemForDragging(item: item) else { return [] } + return [dragItem] + } + + func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { + var selectedItems = [OCItem]() + for dragItem in session.items { + guard let item = dragItem.localObject as? OCItem else { continue } + selectedItems.append(item) } - let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) - let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + let item: OCItem = itemAtIndexPath(indexPath) + selectedItems.append(item) + updateToolbarItemsForDropping(selectedItems) + + guard let dragItem = itemForDragging(item: item) else { return [] } return [dragItem] } + func itemForDragging(item : OCItem) -> UIDragItem? { + if let core = self.core { + switch item.type { + case .collection: + guard let data = item.serializedData() else { return nil } + let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = item + return dragItem + case .file: + guard let rawUti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, item.mimeType as! CFString, nil)?.takeRetainedValue() else { return nil } + + if let fileData = NSData(contentsOf: core.localURL(for: item)) { + let itemProvider = NSItemProvider(item: fileData, typeIdentifier: rawUti as! String) + itemProvider.suggestedName = item.name + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = item + return dragItem + } else { + guard let data = item.serializedData() else { return nil } + let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = item + return dragItem + } + } + } + + return nil + } } // MARK: - UIDocumentPickerDelegate diff --git a/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json new file mode 100644 index 000000000..7ca136c06 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "folder.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf b/ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b94f4b65f57e2f4cdeed6bf92b4050c6d9714080 GIT binary patch literal 1310 zcmY!laBlN#)pr=rNS=QCHmLL868OJZ4ebyu%~So7U; zQT}NW<|56#Ck$qW+OH^ozbU-*+*r%{{{H1;zR$5X zVv$jY?4n(`1XCUupGw$nJe_;i=O=SRCQK^ceeThn-nZHh%G7SD_@vxCf2X$P-Z`pi)*WB7S_bb~zP1VVJ^LK+;O~6^UP^HWA7wsnt1c%Lb`68fWn{{bRqOh1{ zO5)tl8zc7JKfd!p%c-JGSr;qqIu$ZaZpzM`w!Ea;LaY7B>l-Fk7I~X3FH2l3ZIu7^ zulRRZ+-dO}>&_hFO3h1w#vPCaigi#_gJ@u6n;08|3Q(}I z0SN$sexd=8qoD8Xs9>m|pPUFJf$0J$0VIJ4YK{@y90mQ5%7Rn{{eZ;uR0UN9L$J94 ziAAY-B|xpP^ z0E@z084R?yC^4_N0Ay2gB}~{;p`@rZ6~=Q0s&Yv!%S=uUa(4m=LVThhlvr27D4JfBSdfvKT&$O0l&+}>w#XTn2TL;Z^ISj?qv2v@ zWMF7%U~Fh=U}|7wV4-VZs%~JQiDaialARDoJ13ST=H#b?O#?+tF*GU=5m-@_n#N_I zV8LYo2MT7UrpBfUX$mkg3m^|HtB?m5Gc&Zr5HmFax&Q=_)EO9J>NN$%Il4Mape{5q zLjyx#D1rdUypp2C%$(FBa48m?S(OTOGAJ^F^7Bg+Kpuyr7|*=4d<9V0gCn!JB(bOj R>_bBXGYc+NRabvEE&$C%vkd?M literal 0 HcmV?d00001 diff --git a/ownCloud/Resources/Assets.xcassets/trash.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/trash.imageset/Contents.json new file mode 100644 index 000000000..b5f36d39b --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/trash.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "trash-25.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template", + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/trash.imageset/trash-25.pdf b/ownCloud/Resources/Assets.xcassets/trash.imageset/trash-25.pdf new file mode 100644 index 0000000000000000000000000000000000000000..178832e685227f4c00aeacc701efc5f1689d1b01 GIT binary patch literal 1601 zcmY!laBlRGtVov0#D+x^I+ISxx48ubpV&X~M{|GC|Y z-~NAY2c?-jQ!D=2kXirll4XhLpIRjwt@!o*UyMa(|N8p(*PpF1_v_WQW?nyV<+y5P z(Q%KWnLp+qe^u=>b7kCmU(@2f_r%k)j1>&0iJUs*bnxlJXxi~7WLnRMIOZ!W)fvw%@|bG2A~DGQjmhLVYqzhO{CWG{?YW&S#i6oW zN%O+$zkFZ19p+e{3)Op7HmT`){gJndtADt!vD_!R^hz(sf<>Cw%liLDSuWcl$q^Oy zx4mzL18;DVw#Y~Mph=yNd!lw7o}gAKV%fHMTJW-K>Al-3Cx)#z2~3LgQkmoTkXNuO zp69rEfe7D`uqZ+HhR^R+*0cB~iMq8r>?^zTPCR4MokZhfJKjt1Z`&AgJ<-@&jXd5zlu^4_n3M*`!H*FOKoCUvuEewhO&MtV`Rw7bt#O!20z2 zy)^cofEU@iIZq6EMAtufylPR)YK|jYVqL{vF1nxcIdt;3x38x?-uLh6V#)3s#dnW= zik#2ZdpM%u`l(*md#;H(mx8R%Rjrm!m(5Qv{k(I9RQrz`jU1Ebo!fM_efk{rlzUs2 z*?i*@mRk5hGy82>S^f9E&F7BaOtjCveRJdOGjpEmY`%MLQ|Y&D*S_Xt&-eRx{8s); z?_bQaJBrJ>Qu9)v$s5Q5WeiYm0MWqwU~Xgrk_YjO;n@b9QCurZ+=EMinMA?H1|$Fk z`iTZWj)K0kqk^G=esUs^1ZEwe1ds$Gs5wS(a}@MLDhpB-^aB#pQx#Md48i6GBo?LS zl>oKEa-?r+N@k){euY9bP{=^R$P|QP!D^lJ^GbkvSO zf{^?Laxtt}LHMf}q*vcNGo={levsFL;BGGkc@xP1Gb957it QLqlUzOD. + * + */ + +import Foundation +import UIKit +import ownCloudSDK + +public extension UIButton { + private struct AssociatedKeys { + static var actionKey = "actionKey" + } + + public var actionIdentifier: OCExtensionIdentifier? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.actionKey) as? OCExtensionIdentifier + } + + set { + if newValue != nil { + objc_setAssociatedObject(self, &AssociatedKeys.actionKey, newValue!, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) + } + } + } +}