Last active
December 9, 2024 18:34
-
-
Save SergLam/84c3180edbc6015c843358a59efba2cf to your computer and use it in GitHub Desktop.
UITableView - safe reload + section headers+footers reload without animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
extension UITableView { | |
func isCellVisible(section: Int, row: Int) -> Bool { | |
guard let indexes = self.indexPathsForVisibleRows else { | |
return false | |
} | |
return indexes.contains{ $0.section == section && $0.row == row } | |
} | |
func refreshHeaderTitle(inSection section: Int) { | |
UIView.setAnimationsEnabled(false) | |
beginUpdates() | |
let headerView = self.headerView(forSection: section) | |
headerView?.textLabel?.text = self.dataSource?.tableView?(self, titleForHeaderInSection: section)?.uppercased() | |
headerView?.sizeToFit() | |
endUpdates() | |
UIView.setAnimationsEnabled(true) | |
} | |
func refreshFooterTitle(inSection section: Int) { | |
UIView.setAnimationsEnabled(false) | |
beginUpdates() | |
let footerView = self.footerView(forSection: section) | |
footerView?.textLabel?.text = self.dataSource?.tableView?(self, titleForFooterInSection: section) | |
footerView?.sizeToFit() | |
endUpdates() | |
UIView.setAnimationsEnabled(true) | |
} | |
func refreshFooter(inSection section: Int, isHidden: Bool) { | |
UIView.setAnimationsEnabled(false) | |
beginUpdates() | |
let footerView = self.footerView(forSection: section) | |
footerView?.sizeToFit() | |
footerView?.isHidden = isHidden | |
endUpdates() | |
UIView.setAnimationsEnabled(true) | |
} | |
func refreshAllHeaderAndFooterTitles() { | |
for section in 0..<self.numberOfSections { | |
refreshHeaderTitle(inSection: section) | |
refreshFooterTitle(inSection: section) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
typealias VoidClosure = () -> Void | |
typealias TableViewDiffs = (deletions: [Int: UITableView.RowAnimation], | |
insertions: [Int: UITableView.RowAnimation], | |
modifications: [Int: UITableView.RowAnimation], | |
moves: [Int: Int]) | |
extension UITableView { | |
/** | |
Prevent app crash because of table view number of rows and table view datasource inconsistency | |
Example: Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: bla bla bla | |
IMPORTANT: I have tested this code only for single-section table view | |
*/ | |
func refreshDataSafely(in section: Int, | |
for dataSource: [Any], | |
animation: UITableView.RowAnimation, | |
diffs: TableViewDiffs, | |
completion: VoidClosure?) { | |
executeOnMain { [weak self] in | |
guard let self = self else { return } | |
// NOTE: - Check if requested section exist - prevent crash | |
let sectionsCount = self.numberOfSections | |
guard sectionsCount - 1 >= section else { | |
self.reloadData() | |
return | |
} | |
let rowsCount = self.numberOfRows(inSection: section) | |
let maxReloadIndex = diffs.modifications.keys.max() ?? 0 | |
let countCondition = rowsCount - diffs.deletions.count + diffs.insertions.count == dataSource.count | |
let reloadCondition = maxReloadIndex <= dataSource.count - 1 | |
var conflictingIndexes: [Int] = [] | |
diffs.deletions.forEach { deletion in | |
diffs.insertions.forEach { insertion in | |
if deletion == insertion { | |
conflictingIndexes.append(deletion.key) | |
} | |
} | |
} | |
guard countCondition && reloadCondition && conflictingIndexes.isEmpty else { | |
self.reloadSections(IndexSet(integersIn: section...section), with: animation) | |
completion?() | |
return | |
} | |
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', | |
// reason: 'attempt to delete and reload the same index path | |
var deleteAndReloadSameRow: Bool = false | |
for delete in diffs.deletions { | |
for mod in diffs.modifications where mod == delete { | |
deleteAndReloadSameRow = true | |
} | |
} | |
guard deleteAndReloadSameRow == false else { | |
self.reloadSections(IndexSet(integersIn: section...section), with: animation) | |
completion?() | |
return | |
} | |
// Terminating app due to uncaught exception 'NSInternalInconsistencyException', | |
// reason: 'attempt to delete and reload the same index path | |
// https://habr.com/ru/post/350230/ | |
self.applyDeletionsAndInsetrions(in: section, diffs.deletions, diffs.insertions) { | |
self.applyModificationsAndMoves(in: section, diffs.modifications, diffs.moves) { | |
completion?() | |
} | |
} | |
} | |
} | |
private func applyDeletionsAndInsetrions(in section: Int, | |
_ deletions: [Int: UITableView.RowAnimation], | |
_ insertions: [Int: UITableView.RowAnimation], | |
completion: (() -> Void)?) { | |
CATransaction.begin() | |
CATransaction.setCompletionBlock({ | |
completion?() | |
}) | |
self.beginUpdates() | |
for delete in deletions { | |
self.deleteRows(at: [IndexPath(row: delete.key, section: section)], with: delete.value) | |
} | |
for insert in insertions { | |
self.insertRows(at: [IndexPath(row: insert.key, section: section)], with: insert.value) | |
} | |
self.endUpdates() | |
CATransaction.commit() | |
} | |
private func applyModificationsAndMoves(in section: Int, | |
_ modifications: [Int: UITableView.RowAnimation], | |
_ moves: [Int: Int], | |
completion: (() -> Void)?) { | |
CATransaction.begin() | |
CATransaction.setCompletionBlock({ | |
completion?() | |
}) | |
UIView.performWithoutAnimation { | |
self.beginUpdates() | |
for mod in modifications.filter({ $0.value == .none }) { | |
self.reloadRows(at: [IndexPath(row: mod.key, section: section)], with: mod.value) | |
} | |
self.endUpdates() | |
} | |
self.beginUpdates() | |
for mod in modifications.filter({ $0.value != .none }) { | |
self.reloadRows(at: [IndexPath(row: mod.key, section: section)], with: mod.value) | |
} | |
self.endUpdates() | |
UIView.performWithoutAnimation { | |
self.beginUpdates() | |
for move in moves { | |
let fromIndex: IndexPath = IndexPath(row: move.key, section: section) | |
let toIndex: IndexPath = IndexPath(row: move.value, section: section) | |
self.moveRow(at: fromIndex, to: toIndex) | |
} | |
self.endUpdates() | |
} | |
UIView.performWithoutAnimation { | |
self.beginUpdates() | |
for move in moves { | |
let toIndex: IndexPath = IndexPath(row: move.value, section: section) | |
self.reloadRows(at: [toIndex], with: .none) | |
} | |
self.endUpdates() | |
} | |
CATransaction.commit() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment