diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift index 26d5b7d4..631bf50f 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift @@ -54,9 +54,6 @@ import UIKit extension TabsTableViewCell: TabsDelegate { public func shouldSelectItem(_ indexPath: IndexPath, tabs: Tabs) -> Bool { guard indexPath.row != tabs.selectedIndex else { return false } - if let model = tabsListItemModel { - MVMCoreUIActionHandler.performActionUnstructured(with: RemoveMoleculesActionModel(indexPath.row < tabs.selectedIndex ? .right : .left), sourceModel: model, additionalData: nil, delegateObject: delegateObject) - } previousTabIndex = tabs.selectedIndex return true } @@ -68,7 +65,7 @@ extension TabsTableViewCell: TabsDelegate { if let action = model.tabs.tabs[index].action { MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model.tabs, additionalData: nil, delegateObject: delegateObject) } - MVMCoreUIActionHandler.performActionUnstructured(with: AddMoleculesActionModel(index < previousTabIndex ? .left : .right), sourceModel: model, additionalData: nil, delegateObject: delegateObject) + MVMCoreUIActionHandler.performActionUnstructured(with: SwapMoleculesActionModel(index < previousTabIndex ? .left : .right), sourceModel: model, additionalData: nil, delegateObject: delegateObject) } } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift index 8f4bfee0..f5491568 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeListProtocol.swift @@ -16,6 +16,9 @@ public protocol MoleculeListProtocol { /// Asks the delegate to remove molecules. func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) + + /// Asks the delegate to swap batches of molecules. + func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) } extension MoleculeListProtocol { diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 63d592ce..9e017d7a 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -375,6 +375,54 @@ extension MoleculeListTemplate: MoleculeListProtocol { self.view.layoutIfNeeded() } + public func swapMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], with replacements: [ListItemModelProtocol & MoleculeModelProtocol], at indexPath: IndexPath, animation: UITableView.RowAnimation?) { + + tableView.beginUpdates() + + var indexPaths: [IndexPath] = [] + for molecule in molecules { + guard let indexPath = getIndexPath(for: molecule) else { continue } + indexPaths.append(indexPath) + } + for (index, indexPath) in indexPaths.sorted().enumerated() { + let removeIndex = indexPath.row - index + moleculesInfo?.remove(at: removeIndex) + } + + var exitAnimation = UITableView.RowAnimation.automatic + switch animation { + case .left: exitAnimation = .right + case .right: exitAnimation = .left + case .top: exitAnimation = .bottom + case .bottom: exitAnimation = .top + default: break + } + + if indexPaths.count > 0 { + tableView?.deleteRows(at: indexPaths, with: exitAnimation) + } + + indexPaths = [] + for molecule in replacements { + if let info = self.createMoleculeInfo(with: molecule) { + self.tableView?.register(info.class, forCellReuseIdentifier: info.identifier) + let index = indexPath.row + indexPaths.count + self.moleculesInfo?.insert(info, at: index) + indexPaths.append(IndexPath(row: index, section: 0)) + } + } + + if let animation = animation, + indexPaths.count > 0 { + self.tableView?.insertRows(at: indexPaths, with: animation) + } + + tableView.endUpdates() + + self.updateViewConstraints() + self.view.layoutIfNeeded() + } + public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) diff --git a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift index 6e11e074..3ea65249 100644 --- a/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift +++ b/MVMCoreUI/Behaviors/AddRemoveMoleculeBehavior.swift @@ -87,25 +87,38 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo } public func canHandleAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?) -> Bool { - return model.actionType == AddMoleculesActionModel.identifier || model.actionType == RemoveMoleculesActionModel.identifier + return [AddMoleculesActionModel.identifier, RemoveMoleculesActionModel.identifier, SwapMoleculesActionModel.identifier].contains(model.actionType) } public func handleAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?) async throws { - if let model = model as? AddMoleculesActionModel, - let list = delegate?.moleculeListDelegate, - let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & AddMolecules), - let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(), - let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) { + switch model { + case let model as AddMoleculesActionModel: + guard let list = delegate?.moleculeListDelegate, + let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & AddMolecules), + let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(), + let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) else { break } await MainActor.run { list.addMolecules(moleculesToAdd.0, indexPath: indexPath, animation: model.animation) } - } else if let model = model as? RemoveMoleculesActionModel, - let list = delegate?.moleculeListDelegate, + case let model as RemoveMoleculesActionModel: + guard let list = delegate?.moleculeListDelegate, let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules), - let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove() { + let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove() else { break } await MainActor.run { list.removeMolecules(moleculesToRemove, animation: model.animation) } + case let model as SwapMoleculesActionModel: + guard let list = delegate?.moleculeListDelegate, + let sourceModel = MVMCoreUIActionHandler.getSourceModel(from: additionalData) as? (ListItemModelProtocol & MoleculeModelProtocol & RemoveMolecules & AddMolecules), + let moleculesToRemove = sourceModel.getRecursiveMoleculesToRemove(), + let moleculesToAdd = sourceModel.getRecursiveMoleculesToAdd(), + let indexPath = list.getAdjustedIndexPath(for: sourceModel, position: moleculesToAdd.1) + else { break } + await MainActor.run { + list.swapMolecules(moleculesToRemove, with: moleculesToAdd.0, at: indexPath, animation: model.animation) + } + default: + break } } } @@ -174,3 +187,30 @@ public class RemoveMoleculesActionModel: ActionModelProtocol { analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) } } + +public class SwapMoleculesActionModel: ActionModelProtocol { + public static var identifier: String = "swapMoleculesAction" + public var actionType: String = SwapMoleculesActionModel.identifier + public var animation: UITableView.RowAnimation = .automatic + public var extraParameters: JSONValueDictionary? + public var analyticsData: JSONValueDictionary? + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(_ animation: UITableView.RowAnimation, extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { + self.animation = animation + self.extraParameters = extraParameters + self.analyticsData = analyticsData + } + + public required init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + if let animation = try typeContainer.decodeIfPresent(UITableView.RowAnimation.self, forKey: .animation) { + self.animation = animation + } + extraParameters = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .extraParameters) + analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) + } +}