diff --git a/README.md b/README.md index f2cfd87..238b7b2 100644 --- a/README.md +++ b/README.md @@ -83,23 +83,47 @@ However, if we decided to sort it so that deletions and higher indices are proce ### Table and Collection Views -```swift -// The following will automatically animate deletions, insertions, and moves: -tableView.animateRowChanges(oldData: old, newData: new) +The following will automatically animate deletions, insertions, and moves: -collectionView.animateItemChanges(oldData: old, newData: new) +```swift +tableView.animateRowChanges(oldData: old, newData: new) -// It can work with sections, too! +collectionView.animateItemChanges(oldData: old, newData: new, updateData: { self.dataSource = new }) +``` +It can work with sections, too! +```swift tableView.animateRowAndSectionChanges(oldData: old, newData: new) -collectionView.animateItemAndSectionChanges(oldData: old, newData: new) +collectionView.animateItemAndSectionChanges(oldData: old, newData: new, updateData: { self.dataSource = new }) +``` +You can also calculate `diff` separately and use it later: +```swift +// Generate the difference first +let diff = dataSource.diff(newDataSource) + +// This will apply changes to dataSource. +let dataSourceUpdate = { self.dataSource = newDataSource } + +// ... + +tableView.apply(diff) + +collectionView.apply(diff, updateData: dataSourceUpdate) ``` Please see the [included examples](/Examples/) for a working sample. +#### Note about `updateData` + +Since version `2.0.0` there is now an `updateData` closure which notifies you when it's an appropriate time to update `dataSource` of your `UICollectionView`. This addition refers to UICollectionView's [performbatchUpdates](https://developer.apple.com/documentation/uikit/uicollectionview/1618045-performbatchupdates): + +> If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call `performBatchUpdates(_:completion:)`. + +Thus, it is **recommended** to update your `dataSource` inside `updateData` closure to avoid potential crashes during animations. + ### Using Patch and Diff When you want to determine the steps to transform one collection into another (e.g. you want to animate your user interface according to changes in your model), you could do the following: diff --git a/Sources/Differ/Diff+UIKit.swift b/Sources/Differ/Diff+UIKit.swift index c4df580..d5c7c64 100644 --- a/Sources/Differ/Diff+UIKit.swift +++ b/Sources/Differ/Diff+UIKit.swift @@ -253,16 +253,17 @@ public extension UICollectionView { /// - oldData: Data which reflects the previous state of `UICollectionView` /// - newData: Data which reflects the current state of `UICollectionView` /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source /// - completion: Closure to be executed when the animation completes func animateItemChanges( oldData: T, newData: T, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Void)? = nil ) where T.Element: Equatable { let diff = oldData.extendedDiff(newData) - apply(diff, data: newData, updateData: updateData, completion: completion, indexPathTransform: indexPathTransform) + apply(diff, updateData: updateData, completion: completion, indexPathTransform: indexPathTransform) } /// Animates items which changed between oldData and newData. @@ -272,28 +273,28 @@ public extension UICollectionView { /// - newData: Data which reflects the current state of `UICollectionView` /// - isEqual: A function comparing two elements of `T` /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source /// - completion: Closure to be executed when the animation completes func animateItemChanges( oldData: T, newData: T, isEqual: EqualityChecker, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil ) { let diff = oldData.extendedDiff(newData, isEqual: isEqual) - apply(diff, data: newData, updateData: updateData, completion: completion, indexPathTransform: indexPathTransform) + apply(diff, updateData: updateData, completion: completion, indexPathTransform: indexPathTransform) } - func apply( + func apply( _ diff: ExtendedDiff, - data: T, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 } ) { performBatchUpdates({ - updateData?(data) + updateData() let update = BatchUpdate(diff: diff, indexPathTransform: indexPathTransform) self.deleteItems(at: update.deletions) self.insertItems(at: update.insertions) @@ -308,13 +309,14 @@ public extension UICollectionView { /// - newData: Data which reflects the current state of `UICollectionView` /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source /// - completion: Closure to be executed when the animation completes func animateItemAndSectionChanges( oldData: T, newData: T, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, sectionTransform: @escaping (Int) -> Int = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil ) where T.Element: Collection, @@ -322,7 +324,6 @@ public extension UICollectionView { T.Element.Element: Equatable { self.apply( oldData.nestedExtendedDiff(to: newData), - data: newData, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform, updateData: updateData, @@ -338,6 +339,7 @@ public extension UICollectionView { /// - isEqualElement: A function comparing two items (elements of `T.Element`) /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source /// - completion: Closure to be executed when the animation completes func animateItemAndSectionChanges( oldData: T, @@ -345,7 +347,7 @@ public extension UICollectionView { isEqualElement: NestedElementEqualityChecker, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, sectionTransform: @escaping (Int) -> Int = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil ) where T.Element: Collection, @@ -355,7 +357,6 @@ public extension UICollectionView { to: newData, isEqualElement: isEqualElement ), - data: newData, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform, updateData: updateData, @@ -371,6 +372,7 @@ public extension UICollectionView { /// - isEqualSection: A function comparing two sections (elements of `T`) /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source. /// - completion: Closure to be executed when the animation completes func animateItemAndSectionChanges( oldData: T, @@ -378,7 +380,7 @@ public extension UICollectionView { isEqualSection: EqualityChecker, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, sectionTransform: @escaping (Int) -> Int = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil ) where T.Element: Collection, @@ -388,7 +390,6 @@ public extension UICollectionView { to: newData, isEqualSection: isEqualSection ), - data: newData, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform, updateData: updateData, @@ -405,6 +406,7 @@ public extension UICollectionView { /// - isEqualElement: A function comparing two items (elements of `T.Element`) /// - indexPathTransform: Closure which transforms zero-based `IndexPath` to desired `IndexPath` /// - sectionTransform: Closure which transforms zero-based section(`Int`) into desired section(`Int`) + /// - updateData: Closure to be called immediately before performing updates, giving you a chance to correctly update data source /// - completion: Closure to be executed when the animation completes func animateItemAndSectionChanges( oldData: T, @@ -413,7 +415,7 @@ public extension UICollectionView { isEqualElement: NestedElementEqualityChecker, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, sectionTransform: @escaping (Int) -> Int = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Swift.Void)? = nil ) where T.Element: Collection { @@ -423,7 +425,6 @@ public extension UICollectionView { isEqualSection: isEqualSection, isEqualElement: isEqualElement ), - data: newData, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform, updateData: updateData, @@ -431,16 +432,15 @@ public extension UICollectionView { ) } - func apply( + func apply( _ diff: NestedExtendedDiff, - data: T, indexPathTransform: @escaping (IndexPath) -> IndexPath = { $0 }, sectionTransform: @escaping (Int) -> Int = { $0 }, - updateData: ((T) -> Void)? = nil, + updateData: () -> Void, completion: ((Bool) -> Void)? = nil ) { performBatchUpdates({ - updateData?(data) + updateData() let update = NestedBatchUpdate(diff: diff, indexPathTransform: indexPathTransform, sectionTransform: sectionTransform) self.insertSections(update.sectionInsertions) self.deleteSections(update.sectionDeletions)