diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index 508e70955..184515d64 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -22,32 +22,59 @@ extension MasterFeedViewController: UITableViewDropDelegate { return UITableViewDropProposal(operation: .forbidden) } - guard let destIndexPath = correctDestinationIndexPath(session: session) else { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + guard let sourceNode = session.localDragSession?.items.first?.localObject as? Node, + let sourceWebFeed = sourceNode.representedObject as? WebFeed else { + return UITableViewDropProposal(operation: .forbidden) + } + + var successOperation = UIDropOperation.move + + if let destinationIndexPath = destinationIndexPath, + let sourceIndexPath = coordinator.indexPathFor(sourceNode), + destinationIndexPath.section != sourceIndexPath.section { + successOperation = .copy } - guard destIndexPath.section > 0 else { + guard let correctedIndexPath = correctDestinationIndexPath(session: session) else { + // We didn't hit the corrected indexPath, but this at least it gets the section right + guard let section = destinationIndexPath?.section, + let account = coordinator.nodeFor(section)?.representedObject as? Account, + !account.hasChildWebFeed(withURL: sourceWebFeed.url) else { + return UITableViewDropProposal(operation: .forbidden) + } + + return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath) + } + + guard correctedIndexPath.section > 0 else { return UITableViewDropProposal(operation: .forbidden) } - guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed, - let destAccount = destFeed.account else { + guard let correctDestNode = coordinator.nodeFor(correctedIndexPath), + let correctDestFeed = correctDestNode.representedObject as? Feed, + let correctDestAccount = correctDestFeed.account else { return UITableViewDropProposal(operation: .forbidden) } // Validate account specific behaviors... - if destAccount.behaviors.contains(.disallowFeedInMultipleFolders), - let sourceNode = session.localDragSession?.items.first?.localObject as? Node, - let sourceWebFeed = sourceNode.representedObject as? WebFeed, - sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) { + if correctDestAccount.behaviors.contains(.disallowFeedInMultipleFolders), + sourceWebFeed.account?.accountID != correctDestAccount.accountID && correctDestAccount.hasWebFeed(withURL: sourceWebFeed.url) { return UITableViewDropProposal(operation: .forbidden) } // Determine the correct drop proposal - if destFeed is Folder { - return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + if let correctFolder = correctDestFeed as? Folder { + if correctFolder.hasChildWebFeed(withURL: sourceWebFeed.url) { + return UITableViewDropProposal(operation: .forbidden) + } else { + return UITableViewDropProposal(operation: successOperation, intent: .insertIntoDestinationIndexPath) + } } else { - return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + if let parentContainer = correctDestNode.parent?.representedObject as? Container, !parentContainer.hasChildWebFeed(withURL: sourceWebFeed.url) { + return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath) + } else { + return UITableViewDropProposal(operation: .forbidden) + } } } @@ -55,13 +82,13 @@ extension MasterFeedViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { guard let dragItem = dropCoordinator.items.first?.dragItem, let dragNode = dragItem.localObject as? Node, - let source = dragNode.parent?.representedObject as? Container, - let destIndexPath = correctDestinationIndexPath(session: dropCoordinator.session) else { + let source = dragNode.parent?.representedObject as? Container else { return } // Based on the drop we have to determine a node to start looking for a parent container. let destNode: Node? = { + guard let destIndexPath = correctDestinationIndexPath(session: dropCoordinator.session) else { return nil } if coordinator.nodeFor(destIndexPath)?.representedObject is Folder { if dropCoordinator.proposal.intent == .insertAtDestinationIndexPath { @@ -70,15 +97,8 @@ extension MasterFeedViewController: UITableViewDropDelegate { return coordinator.nodeFor(destIndexPath) } } else { - if destIndexPath.row == 0 { - return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section)) - } else if destIndexPath.row > 0 { - return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section)) - } else { - return nil - } + return nil } - }() // Now we start looking for the parent container @@ -86,8 +106,11 @@ extension MasterFeedViewController: UITableViewDropDelegate { if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) { return container } else { + // We didn't hit the corrected indexPath, but this at least gets the section right + guard let section = dropCoordinator.destinationIndexPath?.section else { return nil } + // If we got here, we are trying to drop on an empty section header. Go and find the Account for this section - return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account + return coordinator.nodeFor(section)?.representedObject as? Account } }() @@ -96,7 +119,7 @@ extension MasterFeedViewController: UITableViewDropDelegate { if source.account == destination.account { moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) } else { - moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) + copyWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) } } @@ -130,7 +153,7 @@ private extension MasterFeedViewController { } } - func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { + func copyWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) { @@ -138,15 +161,7 @@ private extension MasterFeedViewController { destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in switch result { case .success: - sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.presentError(error) - } - } + BatchUpdate.shared.end() case .failure(let error): BatchUpdate.shared.end() self.presentError(error) @@ -159,15 +174,7 @@ private extension MasterFeedViewController { destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in switch result { case .success: - sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.presentError(error) - } - } + BatchUpdate.shared.end() case .failure(let error): BatchUpdate.shared.end() self.presentError(error) @@ -179,3 +186,11 @@ private extension MasterFeedViewController { } + +private extension Container { + + func hasChildWebFeed(withURL url: String) -> Bool { + return topLevelWebFeeds.contains(where: { $0.url == url }) + } + +} diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 2bffcfe4d..ba3c3a737 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -40,9 +40,11 @@ struct FeedNode: Hashable { var node: Node var feedID: FeedIdentifier - init(_ node: Node) { + init?(_ node: Node) { + guard let feed = node.representedObject as? Feed else { return nil } + self.node = node - self.feedID = (node.representedObject as! Feed).feedID! + self.feedID = feed.feedID! } func hash(into hasher: inout Hasher) { @@ -622,8 +624,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } func indexPathFor(_ node: Node) -> IndexPath? { + guard let feedNode = FeedNode(node) else { return nil } + for i in 0..