Skip to content

Instantly share code, notes, and snippets.

@SergLam
Last active August 30, 2023 08:07
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SergLam/84c3180edbc6015c843358a59efba2cf to your computer and use it in GitHub Desktop.
Save SergLam/84c3180edbc6015c843358a59efba2cf to your computer and use it in GitHub Desktop.
UITableView - safe reload + section headers+footers reload without animation
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)
}
}
}
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