diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 0f1f6dde2..e19fa382a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -90,6 +90,9 @@ 515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; + 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; }; + 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; + 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; }; 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; @@ -212,7 +215,6 @@ 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; - 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; 51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; }; @@ -1266,6 +1268,9 @@ 515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = ""; }; 515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = ""; }; 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = ""; }; + 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = ""; }; + 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; + 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = ""; }; 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = ""; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; @@ -1326,7 +1331,6 @@ 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = ""; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; - 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; @@ -1875,7 +1879,9 @@ isa = PBXGroup; children = ( 51C45264226508F600C03939 /* MasterFeedViewController.swift */, - 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */, + 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */, + 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */, + 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */, 51CE1C0A23622006005548FC /* RefreshProgressView.swift */, 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */, 51C45260226508F600C03939 /* Cell */, @@ -3976,6 +3982,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, + 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, 5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */, @@ -4001,6 +4008,7 @@ 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, 5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */, 518651DA235621840078E021 /* ImageTransition.swift in Sources */, + 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, @@ -4012,12 +4020,12 @@ 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, - 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */, FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, + 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */, 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */, 513228FC233037630033D4ED /* Reachability.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */, diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift index b2353fe0f..9d6511354 100644 --- a/iOS/MasterFeed/MasterFeedDataSource.swift +++ b/iOS/MasterFeed/MasterFeedDataSource.swift @@ -14,12 +14,10 @@ import Account class MasterFeedDataSource: UITableViewDiffableDataSource { private var coordinator: SceneCoordinator! - private var errorHandler: ((Error) -> ())! - init(coordinator: SceneCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { + init(coordinator: SceneCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource.CellProvider) { super.init(tableView: tableView, cellProvider: cellProvider) self.coordinator = coordinator - self.errorHandler = errorHandler self.defaultRowAnimation = .middle } @@ -30,140 +28,4 @@ class MasterFeedDataSource: UITableViewDiffableDataSource { return true } - override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - guard let node = itemIdentifier(for: indexPath) else { - return false - } - return node.representedObject is WebFeed - } - - override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - - guard let sourceNode = itemIdentifier(for: sourceIndexPath), let webFeed = sourceNode.representedObject as? WebFeed else { - return - } - - // Based on the drop we have to determine a node to start looking for a parent container. - let destNode: Node = { - if destinationIndexPath.row == 0 { - return coordinator.rootNode.childAtIndex(destinationIndexPath.section)! - } else { - let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0 - let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section) - return itemIdentifier(for: adjustedDestIndexPath)! - } - }() - - // Now we start looking for the parent container - let destParentNode: Node? = { - if destNode.representedObject is Container { - return destNode - } else { - if destNode.parent?.representedObject is Container { - return destNode.parent! - } else { - return nil - } - } - }() - - // Move the Web Feed - guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else { - return - } - - if sameAccount(sourceNode, destParentNode!) { - moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) - } else { - moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) - } - - } - - private func sameAccount(_ node: Node, _ parentNode: Node) -> Bool { - if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) { - if accountID == parentAccountID { - return true - } - } - return false - } - - private func nodeAccount(_ node: Node) -> Account? { - if let account = node.representedObject as? Account { - return account - } else if let folder = node.representedObject as? Folder { - return folder.account - } else if let webFeed = node.representedObject as? WebFeed { - return webFeed.account - } else { - return nil - } - - } - - private func nodeAccountID(_ node: Node) -> String? { - return nodeAccount(node)?.accountID - } - - func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { - BatchUpdate.shared.start() - sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in - BatchUpdate.shared.end() - switch result { - case .success: - break - case .failure(let error): - self.errorHandler(error) - } - } - } - - func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { - - if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) { - - BatchUpdate.shared.start() - 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.errorHandler(error) - } - } - case .failure(let error): - BatchUpdate.shared.end() - self.errorHandler(error) - } - } - - } else { - - BatchUpdate.shared.start() - destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: 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.errorHandler(error) - } - } - case .failure(let error): - BatchUpdate.shared.end() - self.errorHandler(error) - } - } - - } - } - } diff --git a/iOS/MasterFeed/MasterFeedViewController+Drag.swift b/iOS/MasterFeed/MasterFeedViewController+Drag.swift new file mode 100644 index 000000000..333b89814 --- /dev/null +++ b/iOS/MasterFeed/MasterFeedViewController+Drag.swift @@ -0,0 +1,33 @@ +// +// MasterFeedViewController+Drag.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 11/20/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit +import MobileCoreServices +import Account + +extension MasterFeedViewController: UITableViewDragDelegate { + + func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + guard let node = dataSource.itemIdentifier(for: indexPath), let webFeed = node.representedObject as? WebFeed else { + return [UIDragItem]() + } + + let data = webFeed.url.data(using: .utf8) + let itemProvider = NSItemProvider() + + itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .all) { completion in + completion(data, nil) + return nil + } + + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = node + return [dragItem] + } + +} diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f31a16525..cc3ade3d5 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -17,7 +17,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { private var refreshProgressView: RefreshProgressView? private var addNewItemButton: UIBarButtonItem! - private lazy var dataSource = makeDataSource() + lazy var dataSource = makeDataSource() var undoableCommands = [UndoableCommand]() weak var coordinator: SceneCoordinator! @@ -38,8 +38,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { navigationController?.navigationBar.prefersLargeTitles = true } - navigationItem.rightBarButtonItem = editButtonItem - // Set the bar button item so that it doesn't show on the timeline view navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) @@ -51,6 +49,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.dataSource = dataSource + tableView.dragDelegate = self + tableView.dropDelegate = self + tableView.dragInteractionEnabled = true resetEstimatedRowHeight() tableView.separatorStyle = .none @@ -630,7 +631,7 @@ private extension MasterFeedViewController { } func makeDataSource() -> UITableViewDiffableDataSource { - return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in + return MasterFeedDataSource(coordinator: coordinator, tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell self?.configure(cell, node) return cell