diff --git a/Sources/Extensions/UIKitExtension.swift b/Sources/Extensions/UIKitExtension.swift index 206ac7c..bcf6a8c 100644 --- a/Sources/Extensions/UIKitExtension.swift +++ b/Sources/Extensions/UIKitExtension.swift @@ -2,6 +2,16 @@ import UIKit public extension UITableView { + /// Enum describing various scenarios, that might have happened while Table View was being updated + enum CommitCompletion { + /// Table View was updated without animation + case nonAnimated + /// Table View animation did not complete, `performBatchUpdates` ended with `finished = false` + case interrupted + /// Table View animation was completed. + case completed + } + /// Applies multiple animated updates in stages using `StagedChangeset`. /// /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. @@ -13,27 +23,115 @@ public extension UITableView { /// - animation: An option to animate the updates. /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - changesetCompleted: Completion handler that is executed after each animted or non-animated changeset commit. + /// - completedChangeset: The completed changeset. + /// - commitKind: Enum describing how was update handled. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of UITableView. + @available(iOS 11.0, tvOS 11.0, *) func reload( using stagedChangeset: StagedChangeset, with animation: @autoclosure () -> RowAnimation, interrupt: ((Changeset) -> Bool)? = nil, + changesetCompleted: @escaping (_ completedChangeset: Changeset, _ commitKind: CommitCompletion) -> Void, setData: (C) -> Void ) { - reload( + _reload( + using: stagedChangeset, + deleteSectionsAnimation: animation, + insertSectionsAnimation: animation, + reloadSectionsAnimation: animation, + deleteRowsAnimation: animation, + insertRowsAnimation: animation, + reloadRowsAnimation: animation, + interrupt: interrupt, + changesetCompleted: changesetCompleted, + setData: setData + ) + } + + /// Applies multiple animated updates in stages using `StagedChangeset`. + /// + /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. + /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it. + /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages. + /// + /// - Parameters: + /// - stagedChangeset: A staged set of changes. + /// - deleteSectionsAnimation: An option to animate the section deletion. + /// - insertSectionsAnimation: An option to animate the section insertion. + /// - reloadSectionsAnimation: An option to animate the section reload. + /// - deleteRowsAnimation: An option to animate the row deletion. + /// - insertRowsAnimation: An option to animate the row insertion. + /// - reloadRowsAnimation: An option to animate the row reload. + /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated + /// updates should be stopped and performed reloadData. Default is nil. + /// - changesetCompleted: Completion handler that is executed after each animted or non-animated changeset commit. + /// - completedChangeset: The completed changeset. + /// - commitKind: Enum describing how was update handled. + /// - setData: A closure that takes the collection as a parameter. + /// The collection should be set to data-source of UITableView. + @available(iOS 11.0, tvOS 11.0, *) + // swiftlint:disable:next function_parameter_count + func reload( + using stagedChangeset: StagedChangeset, + deleteSectionsAnimation: @autoclosure () -> RowAnimation, + insertSectionsAnimation: @autoclosure () -> RowAnimation, + reloadSectionsAnimation: @autoclosure () -> RowAnimation, + deleteRowsAnimation: @autoclosure () -> RowAnimation, + insertRowsAnimation: @autoclosure () -> RowAnimation, + reloadRowsAnimation: @autoclosure () -> RowAnimation, + interrupt: ((Changeset) -> Bool)? = nil, + changesetCompleted: @escaping (_ completedChangeset: Changeset, _ commitKind: CommitCompletion) -> Void, + setData: (C) -> Void + ) { + _reload( using: stagedChangeset, - deleteSectionsAnimation: animation(), - insertSectionsAnimation: animation(), - reloadSectionsAnimation: animation(), - deleteRowsAnimation: animation(), - insertRowsAnimation: animation(), - reloadRowsAnimation: animation(), + deleteSectionsAnimation: deleteSectionsAnimation, + insertSectionsAnimation: insertSectionsAnimation, + reloadSectionsAnimation: reloadSectionsAnimation, + deleteRowsAnimation: deleteRowsAnimation, + insertRowsAnimation: insertRowsAnimation, + reloadRowsAnimation: reloadRowsAnimation, interrupt: interrupt, + changesetCompleted: nil, setData: setData ) } + /// Applies multiple animated updates in stages using `StagedChangeset`. + /// + /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. + /// Assumes that `StagedChangeset` has a minimum staged changesets to avoid it. + /// The data of the data-source needs to be updated synchronously before `performBatchUpdates` in every stages. + /// + /// - Parameters: + /// - stagedChangeset: A staged set of changes. + /// - animation: An option to animate the updates. + /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated + /// updates should be stopped and performed reloadData. Default is nil. + /// - setData: A closure that takes the collection as a parameter. + /// The collection should be set to data-source of UITableView. + func reload( + using stagedChangeset: StagedChangeset, + with animation: @autoclosure () -> RowAnimation, + interrupt: ((Changeset) -> Bool)? = nil, + setData: (C) -> Void + ) { + _reload( + using: stagedChangeset, + deleteSectionsAnimation: animation, + insertSectionsAnimation: animation, + reloadSectionsAnimation: animation, + deleteRowsAnimation: animation, + insertRowsAnimation: animation, + reloadRowsAnimation: animation, + interrupt: interrupt, + changesetCompleted: nil, + setData: setData + ) + } + /// Applies multiple animated updates in stages using `StagedChangeset`. /// /// - Note: There are combination of changes that crash when applied simultaneously in `performBatchUpdates`. @@ -52,7 +150,7 @@ public extension UITableView { /// updates should be stopped and performed reloadData. Default is nil. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of UITableView. - func reload( + func _reload( using stagedChangeset: StagedChangeset, deleteSectionsAnimation: @autoclosure () -> RowAnimation, insertSectionsAnimation: @autoclosure () -> RowAnimation, @@ -63,58 +161,94 @@ public extension UITableView { interrupt: ((Changeset) -> Bool)? = nil, setData: (C) -> Void ) { - if case .none = window, let data = stagedChangeset.last?.data { - setData(data) - return reloadData() + _reload( + using: stagedChangeset, + deleteSectionsAnimation: deleteSectionsAnimation, + insertSectionsAnimation: insertSectionsAnimation, + reloadSectionsAnimation: reloadSectionsAnimation, + deleteRowsAnimation: deleteRowsAnimation, + insertRowsAnimation: insertRowsAnimation, + reloadRowsAnimation: reloadRowsAnimation, + interrupt: interrupt, + changesetCompleted: nil, + setData: setData + ) + } + + // swiftlint:disable:next function_parameter_count + private func _reload( + using stagedChangeset: StagedChangeset, + deleteSectionsAnimation: () -> RowAnimation, + insertSectionsAnimation: () -> RowAnimation, + reloadSectionsAnimation: () -> RowAnimation, + deleteRowsAnimation: () -> RowAnimation, + insertRowsAnimation: () -> RowAnimation, + reloadRowsAnimation: () -> RowAnimation, + interrupt: ((Changeset) -> Bool)? = nil, + changesetCompleted: ((Changeset, CommitCompletion) -> Void)?, + setData: (C) -> Void + ) { + if case .none = window, let lastChangeset = stagedChangeset.last { + setData(lastChangeset.data) + reloadData() + changesetCompleted?(lastChangeset, .nonAnimated) + return } for changeset in stagedChangeset { - if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { - setData(data) - return reloadData() + if let interrupt = interrupt, interrupt(changeset), let lastChangeset = stagedChangeset.last { + setData(lastChangeset.data) + reloadData() + changesetCompleted?(lastChangeset, .nonAnimated) + return } - _performBatchUpdates { - setData(changeset.data) + _performBatch( + updates: { + setData(changeset.data) - if !changeset.sectionDeleted.isEmpty { - deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) - } + if !changeset.sectionDeleted.isEmpty { + deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) + } - if !changeset.sectionInserted.isEmpty { - insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation()) - } + if !changeset.sectionInserted.isEmpty { + insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation()) + } - if !changeset.sectionUpdated.isEmpty { - reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation()) - } + if !changeset.sectionUpdated.isEmpty { + reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation()) + } - for (source, target) in changeset.sectionMoved { - moveSection(source, toSection: target) - } + for (source, target) in changeset.sectionMoved { + moveSection(source, toSection: target) + } - if !changeset.elementDeleted.isEmpty { - deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation()) - } + if !changeset.elementDeleted.isEmpty { + deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation()) + } - if !changeset.elementInserted.isEmpty { - insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation()) - } + if !changeset.elementInserted.isEmpty { + insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation()) + } - if !changeset.elementUpdated.isEmpty { - reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation()) - } + if !changeset.elementUpdated.isEmpty { + reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation()) + } - for (source, target) in changeset.elementMoved { - moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) + for (source, target) in changeset.elementMoved { + moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) + } + }, + completion: changesetCompleted.flatMap { completion -> ((Bool) -> Void) in + return { finished in completion(changeset, finished ? .completed : .interrupted) } } - } + ) } } - private func _performBatchUpdates(_ updates: () -> Void) { + private func _performBatch(updates: () -> Void, completion: ((Bool) -> Void)?) { if #available(iOS 11.0, tvOS 11.0, *) { - performBatchUpdates(updates) + performBatchUpdates(updates, completion: completion) } else { beginUpdates()