Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/partial reloading #49

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ThunderTable.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -145,6 +146,7 @@
B1EC81011FDE86BF00C8EE72 /* SubtitleTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SubtitleTableViewCell.xib; sourceTree = "<group>"; };
B1EC81041FDE873700C8EE72 /* Value1TableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Value1TableViewCell.swift; sourceTree = "<group>"; };
B1EC81051FDE873700C8EE72 /* Value1TableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Value1TableViewCell.xib; sourceTree = "<group>"; };
B1F8047E20FDEC5200921AC9 /* TableViewController+Reload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableViewController+Reload.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
112 changes: 112 additions & 0 deletions ThunderTable/TableViewController+Reload.swift
Original file line number Diff line number Diff line change
@@ -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<T: Row & Equatable>(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<T: Row & Equatable>(row: T, with otherRow: Row, reloading additionalReloadIndexPaths: [IndexPath] = [], animation: UITableView.RowAnimation = .none) {
Copy link
Contributor

@BenShutt BenShutt Jul 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do fancy setters on subscripts here! But let's leave that for now 😄. Bit OTT

Copy link
Contributor

@BenShutt BenShutt Jul 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to reuse the array option below if we can, save code duplication - happy to leave ofc


guard let indexPath = indexPathFor(row: row) else { return }
guard let tableSection = data[indexPath.section] as? TableSection else { return }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is TableSection required? Or just Section

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TableSection currently... I didn't want to make changes to Section to require rows to be { get set }, but may be okay to do so now?


var rows = tableSection.rows
rows[indexPath.row] = otherRow
tableSection.rows = rows

withoutRedrawing {
data[indexPath.section] = tableSection
}

var indexPaths = [indexPath]
indexPaths.append(contentsOf: additionalReloadIndexPaths)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to leave, but user's could wrap additionalReloadIndexPaths logic into a tableView.beginUpdates() so it doesn't need to go via this method. But let's leave for now

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<T: Row & Equatable>(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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again do we need TableSection ?


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<T: Row & Equatable>(row: T, with animation: UITableView.RowAnimation = .none) {

guard let indexPath = indexPathFor(row: row) else { return }
tableView.reloadRows(at: [indexPath], with: animation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are using animation shouldn't we wrap in a tableView.beginUpdates - tableView.endUpdates? May be wrong there

}

/// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's rows.isEmpty then data.count-1 wouldn't be the correct section

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh yikes yep this needs more consideration!

}
}
12 changes: 12 additions & 0 deletions ThunderTable/TableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

@BenShutt BenShutt Jul 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

executeWithoutReloadingData ? Happy to leave

_isBlockingRedrawing = true
closure()
_isBlockingRedrawing = false
}

open var data: [Section] {
set {
_data = newValue
guard !_isBlockingRedrawing else { return }
tableView.reloadData()
}
get {
Expand Down