diff --git a/ThunderTable.xcodeproj/project.pbxproj b/ThunderTable.xcodeproj/project.pbxproj index 1c96f20..656cc62 100644 --- a/ThunderTable.xcodeproj/project.pbxproj +++ b/ThunderTable.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ B1EC81031FDE86BF00C8EE72 /* SubtitleTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1EC81011FDE86BF00C8EE72 /* SubtitleTableViewCell.xib */; }; B1EC81061FDE873700C8EE72 /* Value1TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1EC81041FDE873700C8EE72 /* Value1TableViewCell.swift */; }; B1EC81071FDE873700C8EE72 /* Value1TableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1EC81051FDE873700C8EE72 /* Value1TableViewCell.xib */; }; + B1F8047F20FDEC5200921AC9 /* TableViewController+Reload.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F8047E20FDEC5200921AC9 /* TableViewController+Reload.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -145,6 +146,7 @@ B1EC81011FDE86BF00C8EE72 /* SubtitleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SubtitleTableViewCell.xib; sourceTree = ""; }; B1EC81041FDE873700C8EE72 /* Value1TableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Value1TableViewCell.swift; sourceTree = ""; }; B1EC81051FDE873700C8EE72 /* Value1TableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Value1TableViewCell.xib; sourceTree = ""; }; + B1F8047E20FDEC5200921AC9 /* TableViewController+Reload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableViewController+Reload.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -255,6 +257,7 @@ B17BAA361D89639100844421 /* Info.plist */, B1EC80FB1FDE85C000C8EE72 /* Cells */, B17BAA551D8963BB00844421 /* TableViewController.swift */, + B1F8047E20FDEC5200921AC9 /* TableViewController+Reload.swift */, B185A58B2398129D00B87BC7 /* TableViewController+Collection.swift */, B17BAA571D89643800844421 /* TableSection.swift */, B17BAA591D89645000844421 /* TableRow.swift */, @@ -510,6 +513,7 @@ B17BAA581D89643800844421 /* TableSection.swift in Sources */, B1784D831D8C3A60007358EA /* InputSliderRow.swift in Sources */, B1C2C56C1FFCEE3100D968C5 /* InputDatePickerRow.swift in Sources */, + B1F8047F20FDEC5200921AC9 /* TableViewController+Reload.swift in Sources */, B1EC81021FDE86BF00C8EE72 /* SubtitleTableViewCell.swift in Sources */, B1B679611D89807A00B66FD8 /* TableViewCell.swift in Sources */, B1784D861D8C3A8A007358EA /* InputSliderViewCell.swift in Sources */, diff --git a/ThunderTable/TableViewController+Reload.swift b/ThunderTable/TableViewController+Reload.swift new file mode 100644 index 0000000..7266041 --- /dev/null +++ b/ThunderTable/TableViewController+Reload.swift @@ -0,0 +1,112 @@ +// +// TableViewController+Reload.swift +// ThunderTable +// +// Created by Simon Mitchell on 17/07/2018. +// Copyright © 2018 3SidedCube. All rights reserved. +// + +import UIKit + +extension TableViewController { + + /// Finds the index path for a particular row. + /// + /// - Note: The row in question must conform to Equatable for this to succeed. + /// + /// - Parameter row: The row to find the index path for. + /// - Returns: The index path the row is positioned at if it is in `data`. + public func indexPathFor(row: T) -> IndexPath? { + + for (index, section) in data.enumerated() { + + guard let matchingRowIndex = section.rows.firstIndex(where: { + guard let matchableRow = $0 as? T else { return false } + return matchableRow == row + }) else { + continue + } + + return IndexPath(row: matchingRowIndex, section: index) + } + + return nil + } + + /// Replaces a row in `data` with another row, whilst optionally reloading other index paths. + /// + /// - Note: This relies on the containing section of the row being of class `TableSection` as apposed to any object conforming to `Section` + /// + /// - Parameters: + /// - row: The row that should be replaced. + /// - otherRow: The row that is replacing the original row. + /// - additionalReloadIndexPaths: Additional index paths that should be reloaded at the same time. + /// - animation: The animation to use when reloading the rows + public func replace(row: T, with otherRow: Row, reloading additionalReloadIndexPaths: [IndexPath] = [], animation: UITableView.RowAnimation = .none) { + + guard let indexPath = indexPathFor(row: row) else { return } + guard let tableSection = data[indexPath.section] as? TableSection else { return } + + var rows = tableSection.rows + rows[indexPath.row] = otherRow + tableSection.rows = rows + + withoutRedrawing { + data[indexPath.section] = tableSection + } + + var indexPaths = [indexPath] + indexPaths.append(contentsOf: additionalReloadIndexPaths) + tableView.reloadRows(at: indexPaths, with: animation) + } + + /// Replaces multiple rows with replacement rows. + /// + /// - Note: This relies on the containing section of each row being of class `TableSection` as apposed to any object conforming to `Section`. + /// + /// - Parameters: + /// - rows: The rows to replace. + /// - otherRows: The rows they should be replaced by. + /// - animation: The row animation to use when replacing + public func replace(rows: [T], with otherRows: [Row], animation: UITableView.RowAnimation) { + + guard rows.count == otherRows.count else { return } + + let replacement: [(Int, IndexPath)] = rows.enumerated().compactMap({ + guard let indexPath = indexPathFor(row: $0.element) else { return nil } + return ($0.offset, indexPath) + }) + + replacement.forEach { (index, indexPath) in + + guard let tableSection = data[indexPath.section] as? TableSection else { return } + + var rows = tableSection.rows + rows[indexPath.row] = otherRows[index] + tableSection.rows = rows + + withoutRedrawing { + data[indexPath.section] = tableSection + } + } + + tableView.reloadRows(at: replacement.map({ $0.1 }), with: animation) + } + + /// Reloads the cell at the indexPath for a given row. + /// + /// - Parameters: + /// - row: The row to reload the cell for. + /// - animation: The animation to use when redrawing + public func redraw(row: T, with animation: UITableView.RowAnimation = .none) { + + guard let indexPath = indexPathFor(row: row) else { return } + tableView.reloadRows(at: [indexPath], with: animation) + } + + /// The last available indexPath in the tableView + public var lastIndexPath: IndexPath? { + guard let lastSection = data.last(where: { !$0.rows.isEmpty }) else { return nil } + return IndexPath(row: lastSection.rows.count - 1, section: data.count - 1) + } +} diff --git a/ThunderTable/TableViewController.swift b/ThunderTable/TableViewController.swift index 15fcf8f..8d77ec8 100644 --- a/ThunderTable/TableViewController.swift +++ b/ThunderTable/TableViewController.swift @@ -111,9 +111,21 @@ open class TableViewController: UITableViewController, UIContentSizeCategoryAdju private var _data: [Section] = [] + private var _isBlockingRedrawing: Bool = false + + /// A function which allows for mutation of `data` without causing the tableView to reload. + /// + /// - Parameter closure: Code to be run without reloading the table view. + public func withoutRedrawing(_ closure: () -> Void) { + _isBlockingRedrawing = true + closure() + _isBlockingRedrawing = false + } + open var data: [Section] { set { _data = newValue + guard !_isBlockingRedrawing else { return } tableView.reloadData() } get {