diff --git a/Examples/TreeView/TreeView.xcodeproj/project.pbxproj b/Examples/TreeView/TreeView.xcodeproj/project.pbxproj index 8753315..edd0b7e 100644 --- a/Examples/TreeView/TreeView.xcodeproj/project.pbxproj +++ b/Examples/TreeView/TreeView.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -11,7 +11,6 @@ 086D5379262C743A00E7B920 /* Queue-Optimized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086D5378262C743A00E7B920 /* Queue-Optimized.swift */; }; 086D537D262C7A5A00E7B920 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086D537C262C7A5A00E7B920 /* Utils.swift */; }; 089A27232607D85A00F907DA /* OutlineItemDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089A27222607D85A00F907DA /* OutlineItemDataSet.swift */; }; - 08FD01D526E01B6400902DD3 /* SwiftListTreeDataSource in Frameworks */ = {isa = PBXBuildFile; productRef = 08FD01D426E01B6400902DD3 /* SwiftListTreeDataSource */; }; C14ABAA32913568C5599EFDC /* Pods_TreeView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F0E7D30A446750D518E9158 /* Pods_TreeView.framework */; }; D81696802BF39C25009E72D6 /* SwiftListTreeDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D816967D2BF39C25009E72D6 /* SwiftListTreeDataStore.swift */; }; D81696812BF39C25009E72D6 /* SwiftUIListWithSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = D816967E2BF39C25009E72D6 /* SwiftUIListWithSearch.swift */; }; @@ -21,6 +20,8 @@ D833408E253842DD00AE6894 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D833408C253842DD00AE6894 /* Main.storyboard */; }; D8334090253842E000AE6894 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D833408F253842E000AE6894 /* Assets.xcassets */; }; D8334093253842E000AE6894 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D8334091253842E000AE6894 /* LaunchScreen.storyboard */; }; + D8AD1ADD2D84A79E006E2326 /* TableViewDragAndDropSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AD1ADC2D84A79E006E2326 /* TableViewDragAndDropSupport.swift */; }; + D8CFE6D72D848F8C001CE3C3 /* SwiftListTreeDataSource in Frameworks */ = {isa = PBXBuildFile; productRef = D8CFE6D62D848F8C001CE3C3 /* SwiftListTreeDataSource */; }; D8F4525C2539C8A500F10963 /* TreeViewController+RATreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F4525B2539C8A500F10963 /* TreeViewController+RATreeView.swift */; }; D8F452642539C93200F10963 /* OutlineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F452632539C93200F10963 /* OutlineItem.swift */; }; D8F4526C2539CCE500F10963 /* Cell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D8F4526B2539CCE500F10963 /* Cell.xib */; }; @@ -51,6 +52,7 @@ D833408F253842E000AE6894 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D8334092253842E000AE6894 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D8334094253842E000AE6894 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D8AD1ADC2D84A79E006E2326 /* TableViewDragAndDropSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDragAndDropSupport.swift; sourceTree = ""; }; D8F4525B2539C8A500F10963 /* TreeViewController+RATreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TreeViewController+RATreeView.swift"; sourceTree = ""; }; D8F452632539C93200F10963 /* OutlineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineItem.swift; sourceTree = ""; }; D8F4526B2539CCE500F10963 /* Cell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Cell.xib; sourceTree = ""; }; @@ -67,7 +69,7 @@ buildActionMask = 2147483647; files = ( C14ABAA32913568C5599EFDC /* Pods_TreeView.framework in Frameworks */, - 08FD01D526E01B6400902DD3 /* SwiftListTreeDataSource in Frameworks */, + D8CFE6D72D848F8C001CE3C3 /* SwiftListTreeDataSource in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -159,6 +161,7 @@ D816967F2BF39C25009E72D6 /* SwiftUI */, D833408A253842DD00AE6894 /* TableController.swift */, D8F452F6253A44CE00F10963 /* TableWithSearchController.swift */, + D8AD1ADC2D84A79E006E2326 /* TableViewDragAndDropSupport.swift */, ); path = SwiftListTreeDataSource; sourceTree = ""; @@ -198,7 +201,7 @@ ); name = TreeView; packageProductDependencies = ( - 08FD01D426E01B6400902DD3 /* SwiftListTreeDataSource */, + D8CFE6D62D848F8C001CE3C3 /* SwiftListTreeDataSource */, ); productName = test; productReference = D8334083253842DD00AE6894 /* TreeView.app */; @@ -228,7 +231,7 @@ ); mainGroup = D833407A253842DD00AE6894; packageReferences = ( - 08FD01D326E01B6400902DD3 /* XCRemoteSwiftPackageReference "SwiftListTreeDataSource" */, + D8CFE6D52D848F8C001CE3C3 /* XCLocalSwiftPackageReference "../../../SwiftListTreeDataSource" */, ); productRefGroup = D8334084253842DD00AE6894 /* Products */; projectDirPath = ""; @@ -311,6 +314,7 @@ D81696802BF39C25009E72D6 /* SwiftListTreeDataStore.swift in Sources */, D8F4526F2539CCEE00F10963 /* Cell.swift in Sources */, 089A27232607D85A00F907DA /* OutlineItemDataSet.swift in Sources */, + D8AD1ADD2D84A79E006E2326 /* TableViewDragAndDropSupport.swift in Sources */, D8334087253842DD00AE6894 /* AppDelegate.swift in Sources */, D8F452CA2539DDAE00F10963 /* OutlineViewController.swift in Sources */, D8334089253842DD00AE6894 /* SceneDelegate.swift in Sources */, diff --git a/Examples/TreeView/TreeView/Base.lproj/Main.storyboard b/Examples/TreeView/TreeView/Base.lproj/Main.storyboard index f3023b0..b815f14 100644 --- a/Examples/TreeView/TreeView/Base.lproj/Main.storyboard +++ b/Examples/TreeView/TreeView/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -122,12 +122,12 @@ - + - - + + @@ -234,11 +234,11 @@ - + - + @@ -286,10 +286,9 @@ - - + diff --git a/Examples/TreeView/TreeView/Common/Cell.swift b/Examples/TreeView/TreeView/Common/Cell.swift index 35ec430..3218aeb 100644 --- a/Examples/TreeView/TreeView/Common/Cell.swift +++ b/Examples/TreeView/TreeView/Common/Cell.swift @@ -16,11 +16,12 @@ class Cell: UITableViewCell { func configure(with item: ListTreeDataSource.TreeItemType, searchText: String? = nil) { let left = 11 + 10 * item.level - let mattrString = NSMutableAttributedString(string: item.value.title, attributes: [ .foregroundColor: UIColor.black ]) + let mattrString = NSMutableAttributedString(string: item.value.title, attributes: [ .foregroundColor: UIColor.label ]) if let searchText = searchText, !searchText.isEmpty { let range = (item.value.title.lowercased() as NSString).range(of: searchText.lowercased()) mattrString.addAttributes([ - .font: UIFont.systemFont(ofSize: 17, weight: .semibold) + .font: UIFont.systemFont(ofSize: 17, weight: .semibold), + .foregroundColor: UIColor.tintColor ], range: range) } diff --git a/Examples/TreeView/TreeView/Common/OutlineItem.swift b/Examples/TreeView/TreeView/Common/OutlineItem.swift index 7bc1533..42cc8c8 100644 --- a/Examples/TreeView/TreeView/Common/OutlineItem.swift +++ b/Examples/TreeView/TreeView/Common/OutlineItem.swift @@ -7,7 +7,7 @@ import Foundation -class OutlineItem: Hashable { +class OutlineItem: Identifiable, Hashable, Codable { let title: String var subitems: [OutlineItem] @@ -22,7 +22,8 @@ class OutlineItem: Hashable { static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool { return lhs.identifier == rhs.identifier } - private let identifier = UUID() + private var identifier = UUID() + var id: UUID { identifier } } extension OutlineItem: CustomStringConvertible { diff --git a/Examples/TreeView/TreeView/RATreeView/TreeViewController+RATreeView.swift b/Examples/TreeView/TreeView/RATreeView/TreeViewController+RATreeView.swift index 9cff254..1ef06fe 100644 --- a/Examples/TreeView/TreeView/RATreeView/TreeViewController+RATreeView.swift +++ b/Examples/TreeView/TreeView/RATreeView/TreeViewController+RATreeView.swift @@ -28,7 +28,7 @@ class TreeViewController: UIViewController, RATreeViewDelegate, RATreeViewDataSo override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + view.backgroundColor = .systemBackground setupTreeView() } diff --git a/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableController.swift b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableController.swift index 35faf8e..8da2309 100644 --- a/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableController.swift +++ b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableController.swift @@ -25,7 +25,8 @@ class TableController: UITableViewController { addItems(items, to: dataSource) return dataSource }() - + var dragAndDropController: TableViewDragAndDropSupport! + @available(iOS 13.0, *) private(set) lazy var diffableDataSource: UITableViewDiffableDataSource = { return self.createDiffableDataSource() @@ -40,11 +41,12 @@ class TableController: UITableViewController { setupTableView() setupDataSource() + configureDragAndDrop() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - reloadUI() + updateUI() } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -60,7 +62,8 @@ class TableController: UITableViewController { cell.lbl?.text = item.value.title cell.lblLeadingConstraint.constant = CGFloat(left) cell.disclosureImageView.isHidden = item.subitems.isEmpty - + cell.contentView.backgroundColor = .systemBackground + let transform = CGAffineTransform.init(rotationAngle: item.isExpanded ? CGFloat.pi/2.0 : 0) cell.disclosureImageView.transform = transform @@ -79,9 +82,30 @@ class TableController: UITableViewController { } } - self.reloadUI(animating: true) + self.updateUI(animating: true) } - + + // MARK: - Row Swipe to Delete + /// Provide a trailing swipe action for "Delete" + override func tableView(_ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) + -> UISwipeActionsConfiguration? { + + let node = listTreeDataSource.items[indexPath.row] + + let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, complete in + guard let self = self else { return } + + // Deleting a node also removes its entire subtree + self.listTreeDataSource.delete([node.value]) + self.listTreeDataSource.reload() + self.updateUI(animating: true) + + complete(true) + } + return UISwipeActionsConfiguration(actions: [deleteAction]) + } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } @@ -103,27 +127,38 @@ extension TableController { self.tableView.dataSource = self } } - func reloadUI(animating: Bool = true) { + + func updateUI(animating: Bool = true, reloadIds: [ListTreeDataSource.TreeItemType] = []) { if isOS13Available { - var diffableSnaphot = NSDiffableDataSourceSnapshot() + var diffableSnaphot = NSDiffableDataSourceSnapshot.TreeItemType>() diffableSnaphot.appendSections([.main]) diffableSnaphot.appendItems(listTreeDataSource.items, toSection: .main) + diffableSnaphot.reloadItems(reloadIds) self.diffableDataSource.apply(diffableSnaphot, animatingDifferences: animating) } else { self.tableView.reloadData() } } + + private func configureDragAndDrop() { + dragAndDropController = TableViewDragAndDropSupport(dataSource: self.listTreeDataSource, tableView: self.tableView, updateUI: { [weak self] animating, reloadIds in + self?.updateUI(animating:animating, reloadIds:reloadIds) + }) + tableView.dragInteractionEnabled = true + tableView.dragDelegate = dragAndDropController + tableView.dropDelegate = dragAndDropController + } } // MARK: - Actions fileprivate extension TableController { @IBAction func collapseAll(_ sender: UIBarButtonItem) { listTreeDataSource.collapseAll() - reloadUI(animating: false) // `false` to stay on the safe side for batch update in large data set + updateUI(animating: false) // `false` to stay on the safe side for batch update in large data set } @IBAction func expandAll(_ sender: UIBarButtonItem) { listTreeDataSource.expandAll() - reloadUI(animating: false) // `false` to stay on the safe side for batch update in large data set + updateUI(animating: false) // `false` to stay on the safe side for batch update in large data set } } diff --git a/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableViewDragAndDropSupport.swift b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableViewDragAndDropSupport.swift new file mode 100644 index 0000000..e184c10 --- /dev/null +++ b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableViewDragAndDropSupport.swift @@ -0,0 +1,206 @@ +// +// TableViewDragAndDropSupport.swift +// TreeView +// +// Created by Dzmitry Antonenka on 3/14/25. +// + +import UIKit +import SwiftListTreeDataSource + +class TableViewDragAndDropSupport: NSObject, UITableViewDragDelegate, UITableViewDropDelegate { + var dataSource: ListTreeDataSource + var tableView: UITableView + var updateUI: (_ animating: Bool, _ reloadIds: [ListTreeDataSource.TreeItemType]) -> Void + + var autoExpandTimer: Timer? + var hoveredNode: TreeItem? + var draggedNode: (TreeItem, Bool)? + + init(dataSource: ListTreeDataSource, tableView: UITableView, updateUI: @escaping (_ animating: Bool, _ reloadIds: [ListTreeDataSource.TreeItemType]) -> Void) { + self.tableView = tableView + self.dataSource = dataSource + self.updateUI = updateUI + } + + // MARK: - Drag Delegate + func tableView(_ tableView: UITableView, + itemsForBeginning session: UIDragSession, + at indexPath: IndexPath) -> [UIDragItem] { + // We'll attach the warehouse item as an NSItemProvider, or just an empty provider + let node = self.dataSource.items[indexPath.row] + + startDraggingNode(node) + + let dragItem = UIDragItem(itemProvider: NSItemProvider()) + dragItem.localObject = node // local reference to our node + return [dragItem] + } + + // MARK: - UITableViewDropDelegate + + func tableView(_ tableView: UITableView, + canHandle session: UIDropSession) -> Bool { + return session.localDragSession != nil + } + + /// **Live expand** logic happens here: we track the hovered node and start a timer if it’s collapsed. + func tableView(_ tableView: UITableView, + dropSessionDidUpdate session: UIDropSession, + withDestinationIndexPath destinationIndexPath: IndexPath?) + -> UITableViewDropProposal { + guard + let dragSession = session.localDragSession, + let sourceItem = dragSession.items.first, + let sourceNode = sourceItem.localObject as? TreeItem + else { + setHoveredNode(nil) + return UITableViewDropProposal(operation: .cancel) + } + + // If no valid index path, just allow a move to the end + guard let destinationIndexPath = destinationIndexPath, + destinationIndexPath.row < dataSource.items.count else { + resetAutoExpand() + setHoveredNode(nil) + return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + } + + let hoveredCandidate = dataSource.items[destinationIndexPath.row] + + // *** Here’s the key check: If hoveredCandidate is in sourceNode’s subtree => .forbidden + if allDescendants(of: sourceNode).contains(hoveredCandidate) { + setHoveredNode(nil) + return UITableViewDropProposal(operation: .forbidden) + } + + // If new hovered node => reset the auto-expand timer + if hoveredCandidate != hoveredNode { + setHoveredNode(hoveredCandidate) + resetAutoExpand() + + // If node is collapsed but has children => schedule expand + if !hoveredCandidate.isExpanded && !hoveredCandidate.subitems.isEmpty { + autoExpandTimer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: false) { [weak self] _ in + guard let self = self else { return } + self.dataSource.toggleExpand(item: hoveredCandidate) + self.updateUI(true, []) + } + } + } + + return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) + } + + /// Called when the user moves the drag away from the table or finishes the drop + func tableView(_ tableView: UITableView, dropSessionDidEnd session: any UIDropSession) { + resetAutoExpand() + setHoveredNode(nil) + finishDraggingNode() + } + + func tableView(_ tableView: UITableView, + performDropWith coordinator: UITableViewDropCoordinator) { + + guard coordinator.proposal.operation == .move, + let item = coordinator.items.first, + let sourceNode = item.dragItem.localObject as? TreeItem else { return } + + // destinationIndexPath might be nil if user drops outside any rows, so we handle that scenario too + let destIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: dataSource.items.count, section: 0) + + // We want to re-parent or reorder the sourceNode based on the drop location. + moveNode(sourceNode, to: destIndexPath) + + updateUI(true, [sourceNode]) + } + + /// Moves `node` so that it appears at the position indicated by `destinationIndexPath`. + /// We must figure out which parent is relevant at that path, etc. + private func moveNode(_ sourceNode: TreeItem, + to destinationIndexPath: IndexPath) { + + // If the user drops below all items, we can re-parent at the top-level + if destinationIndexPath.row >= dataSource.items.count { + // Move the item to top-level at the end + // We'll do an index = backingStore.count, but the library expects a numeric index, parent, etc. + dataSource.move(sourceNode.value, toIndex: dataSource.items.count, inParent: nil) + dataSource.reload() + return + } + + // Else, find the node that is currently at the destination + let destinationNode = dataSource.items[destinationIndexPath.row] + + // We could choose to place `sourceNode` at the same level as `destNode` or as a child. + // Let’s do: “drop on row” => re-parent to its parent if it has one, inserted just after it. + + if let parentNode = destinationNode.parent { + // Move under the same parent as destNode + // We'll find parent's subitems, figure out the index for insertion + let subitems = parentNode.subitems + if let insertIndex = subitems.firstIndex(of: destinationNode) { + dataSource.move(sourceNode.value, toIndex: insertIndex, inParent: parentNode.value) + } + } else { + // destNode is top-level; insert it at top-level + let topLevel = dataSource.items.filter({ $0.parent == nil }) + if let insertIndex = topLevel.firstIndex(of: destinationNode) { + dataSource.move(sourceNode.value, toIndex: insertIndex, inParent: nil) + } + } + + dataSource.reload() + } + + // MARK: - Helpers for auto-expand + private func resetAutoExpand() { + autoExpandTimer?.invalidate() + autoExpandTimer = nil + } + + private func startDraggingNode(_ node: TreeItem) { + draggedNode = (node, node.isExpanded) + + node.isExpanded = false + dataSource.reload() + + updateUI(true, [node]) + } + + // MARK: - Restore expansions once drag ends + private func finishDraggingNode() { + guard let (node, isExpanded) = draggedNode else { return } + draggedNode = nil + + node.isExpanded = isExpanded + dataSource.reload() + + updateUI(true, [node]) + } + + // MARK: - Hover Helpers + private func setHoveredNode(_ newNode: TreeItem?) { + // 1. Unhighlight the old node's row + if let oldNode = hoveredNode { + if let oldIndex = dataSource.items.firstIndex(of: oldNode) { + updateCell(at: IndexPath(row: oldIndex, section: 0), color: .systemBackground) + } + } + + // 2. Update hoveredNode + hoveredNode = newNode + + // 3. Highlight the new node's row + if let newNode = newNode { + if let newIndex = dataSource.items.firstIndex(of: newNode) { + updateCell(at: IndexPath(row: newIndex, section: 0), color: .quaternarySystemFill) + } + } + } + + private func updateCell(at indexPath: IndexPath, color: UIColor) { + guard let cell = tableView.cellForRow(at: indexPath) else { return } + cell.contentView.backgroundColor = color + } +} diff --git a/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableWithSearchController.swift b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableWithSearchController.swift index 34b5d86..1963871 100644 --- a/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableWithSearchController.swift +++ b/Examples/TreeView/TreeView/SwiftListTreeDataSource/TableWithSearchController.swift @@ -24,11 +24,12 @@ class TableWithSearchController: UIViewController { var displayMode: DisplayMode = .standard var debouncer = Debouncer() - + var dragAndDropController: TableViewDragAndDropSupport! + lazy var listTreeDataSource: FilterableListTreeDataSource = { - var dataSource = FilterableListTreeDataSource() - addItems(items, to: dataSource) - return dataSource + var listTreeDataSource = FilterableListTreeDataSource() + addItems(items, to: listTreeDataSource) + return listTreeDataSource }() @available(iOS 13.0, *) @@ -41,18 +42,20 @@ class TableWithSearchController: UIViewController { setupTableView() setupDataSource() setupBarButtonItems() + configureDragAndDrop() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - reloadUI() + updateUI() } - func reloadUI(animating: Bool = true) { + func updateUI(animating: Bool = true, reloadIds: [ListTreeDataSource.TreeItemType] = []) { if isOS13Available { var diffableSnaphot = NSDiffableDataSourceSnapshot.TreeItemType>() diffableSnaphot.appendSections([.main]) diffableSnaphot.appendItems(listTreeDataSource.items, toSection: .main) + diffableSnaphot.reloadItems(reloadIds) self.diffableDataSource.apply(diffableSnaphot, animatingDifferences: animating) } else { self.tableView.reloadData() @@ -63,17 +66,17 @@ class TableWithSearchController: UIViewController { if !searchText.isEmpty { self.searchBar.isLoading = true self.displayMode = .filtering(text: searchText) - + self.listTreeDataSource.filterItemsKeepingParents(by: { $0.title.lowercased().contains(searchText.lowercased()) }) { [weak self] in guard let self = self else { return } self.searchBar.isLoading = false - self.reloadUI(animating: false) + self.updateUI(animating: false) } } else { self.displayMode = .standard self.searchBar.isLoading = false self.listTreeDataSource.resetFiltering(collapsingAll: true) - self.reloadUI(animating: false) + self.updateUI(animating: false) } } } @@ -132,9 +135,30 @@ extension TableWithSearchController: UITableViewDataSource, UITableViewDelegate } } - self.reloadUI(animating: true) + self.updateUI(animating: true) } - + + // MARK: - Row Swipe to Delete + /// Provide a trailing swipe action for "Delete" + func tableView(_ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) + -> UISwipeActionsConfiguration? { + + let node = listTreeDataSource.items[indexPath.row] + + let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, complete in + guard let self = self else { return } + + // Deleting a node also removes its entire subtree + self.listTreeDataSource.delete([node.value]) + self.listTreeDataSource.reload() + self.updateUI(animating: true) + + complete(true) + } + return UISwipeActionsConfiguration(actions: [deleteAction]) + } + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } @@ -144,12 +168,35 @@ extension TableWithSearchController: UITableViewDataSource, UITableViewDelegate } } +@available(iOS 13, *) +fileprivate extension TableWithSearchController { + func createDiffableDataSource() -> UITableViewDiffableDataSource.TreeItemType> { + let listTreeDataSource = UITableViewDiffableDataSource.TreeItemType>( + tableView: tableView, + cellProvider: { tableView, indexPath, _ in + return self.tableView(self.tableView, cellForRowAt: indexPath) + } + ) + return listTreeDataSource + } +} + +// MARK: - Setup extension TableWithSearchController { func setupTableView() { tableView.register(UINib(nibName: "Cell", bundle: nil), forCellReuseIdentifier: "Cell") tableView.register(DetailTextCell.self, forCellReuseIdentifier: "TitleDetailCell") } + private func configureDragAndDrop() { + dragAndDropController = TableViewDragAndDropSupport(dataSource: self.listTreeDataSource, tableView: self.tableView, updateUI: { [weak self] animating, reloadIds in + self?.updateUI(animating:animating, reloadIds:reloadIds) + }) + tableView.dragInteractionEnabled = true + tableView.dragDelegate = dragAndDropController + tableView.dropDelegate = dragAndDropController + } + func setupBarButtonItems() { } @@ -161,16 +208,3 @@ extension TableWithSearchController { } } } - -@available(iOS 13, *) -fileprivate extension TableWithSearchController { - func createDiffableDataSource() -> UITableViewDiffableDataSource.TreeItemType> { - let dataSource = UITableViewDiffableDataSource.TreeItemType>( - tableView: tableView, - cellProvider: { tableView, indexPath, _ in - return self.tableView(self.tableView, cellForRowAt: indexPath) - } - ) - return dataSource - } -}