diff --git a/LayoutKit.playground/Pages/CollectionView Animation.xcplaygroundpage/Contents.swift b/LayoutKit.playground/Pages/CollectionView Animation.xcplaygroundpage/Contents.swift
new file mode 100644
index 00000000..d997a0c4
--- /dev/null
+++ b/LayoutKit.playground/Pages/CollectionView Animation.xcplaygroundpage/Contents.swift
@@ -0,0 +1,224 @@
+//: [Previous](@previous)
+
+import Foundation
+import LayoutKit
+import UIKit
+import PlaygroundSupport
+
+class ContentCollectionViewFlowLayout : UICollectionViewFlowLayout {
+
+ private var animations: [Animation] = []
+
+ func add(animations: [Animation]) {
+ self.animations.append(contentsOf: animations)
+ }
+
+ // MARK: UICollectionViewFlowLayout
+
+ override func finalizeCollectionViewUpdates() {
+ super.finalizeCollectionViewUpdates()
+ self.animations.forEach { animation in
+ animation.apply()
+ }
+ self.animations = []
+ }
+}
+
+
+class MyViewController : UIViewController {
+
+ lazy var layouts: [[Layout]] = {
+ return [
+ [
+ self.itemLayout(text: "Section 0 item 0"),
+ self.itemLayout(text: "Section 0 item 1")
+ ],
+ [
+ self.itemLayout(text: "Section 1 item 0"),
+ self.itemLayout(text: "Section 1 item 1")
+ ]
+ ]
+ }()
+
+ let collectionViewLayout: ContentCollectionViewFlowLayout = {
+ let layout = ContentCollectionViewFlowLayout()
+ layout.sectionInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0)
+ return layout
+ }()
+
+ lazy var layoutAdapterCollectionView: LayoutAdapterCollectionView = {
+ let collectionView = LayoutAdapterCollectionView(frame: .zero, collectionViewLayout: self.collectionViewLayout)
+ collectionView.backgroundColor = .lightGray
+ collectionView.alwaysBounceVertical = true
+ return collectionView
+ }()
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+ self.layoutAdapterCollectionView.frame = self.view.bounds
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.view.backgroundColor = .white
+ self.view.addSubview(self.layoutAdapterCollectionView)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+
+ self.layoutAdapterCollectionView.layoutAdapter.reload(
+ width: self.layoutAdapterCollectionView.bounds.width,
+ synchronous: true,
+ layoutProvider: self.layoutAdapter,
+ completion: nil
+ )
+ }
+
+
+ private func layoutAdapter() -> [Section<[Layout]>] {
+ return [
+ Section<[Layout]>(header: self.headerLayout(title: "Reload item"), items: self.layouts[0]),
+ Section<[Layout]>(header: self.headerLayout(title: "Invalidate layout item"), items: self.layouts[1])
+ ]
+ }
+
+ private func headerLayout(title: String) -> Layout {
+ let labelLayout = LabelLayout(
+ text: title,
+ font: .boldSystemFont(ofSize: 24),
+ numberOfLines: 0,
+ alignment: .centerLeading,
+ viewReuseId: "headerlabel"
+ )
+
+ return InsetLayout(
+ insets: UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0),
+ sublayout: labelLayout
+ )
+ }
+
+ private func itemLayout(text: String, minHeight: CGFloat = 100, color: UIColor = .red) -> Layout {
+
+ let imageLayout = SizeLayout(
+ width: 80,
+ height: 80,
+ viewReuseId: "image",
+ config: { view in
+ view.backgroundColor = color
+ }
+ )
+
+ let labelLayout = LabelLayout(
+ text: text,
+ font: .systemFont(ofSize: 18),
+ numberOfLines: 0,
+ alignment: .centerLeading,
+ viewReuseId: "label"
+ )
+
+ let resizeButtonLayout = ButtonLayout(
+ type: .custom,
+ title: "Resize",
+ font: .systemFont(ofSize: 18),
+ contentEdgeInsets: UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8),
+ alignment: .centerTrailing,
+ viewReuseId: "button",
+ config: { [unowned self] button in
+ button.backgroundColor = .lightGray
+ button.addHandler(for: .touchUpInside, handler: { control in
+ self.updateCell(withSubview: button)
+ })
+ }
+ )
+
+ let stackLayout = StackLayout(
+ axis: .horizontal,
+ spacing: 10,
+ viewReuseId: "stackView",
+ sublayouts: [
+ imageLayout,
+ labelLayout,
+ resizeButtonLayout
+ ],
+ config: { view in
+ view.backgroundColor = .white
+ }
+ )
+
+ return SizeLayout(minHeight: minHeight, sublayout: stackLayout)
+ }
+
+ private func updateCell(withSubview subview: UIView) {
+ guard let cell = self.findIndexPathForCell(withSubview: subview) else { return }
+ guard let indexPath = self.layoutAdapterCollectionView.indexPath(for: cell) else { return }
+
+ let randomNum: UInt32 = arc4random_uniform(100) + 100
+ let colors: [UIColor] = [.blue, .green, .yellow]
+
+ self.layouts[indexPath.section][indexPath.item] = self.itemLayout(
+ text: "Section \(indexPath.section) item \(indexPath.item)",
+ minHeight: CGFloat(randomNum),
+ color: colors[Int(arc4random_uniform(UInt32(colors.count)))]
+ )
+
+ if indexPath.section == 0 {
+ self.reloadItem(at: indexPath)
+ }
+ else {
+ self.invalidateItem(at: indexPath)
+ }
+
+ }
+
+ private func reloadItem(at indexPath: IndexPath) {
+
+ let batchUpdates = BatchUpdates()
+ batchUpdates.reloadItems = [indexPath]
+
+ self.layoutAdapterCollectionView.layoutAdapter.reload(
+ width: self.layoutAdapterCollectionView.bounds.width,
+ synchronous: true,
+ batchUpdates: batchUpdates,
+ layoutProvider: self.layoutAdapter
+ )
+ }
+
+ private func invalidateItem(at indexPath: IndexPath) {
+ let items: [IndexPath] = [indexPath]
+
+ self.layoutAdapterCollectionView.layoutAdapter.reload(
+ items: items,
+ width: self.layoutAdapterCollectionView.bounds.width,
+ layoutProvider: self.layoutAdapter,
+ completion: { animations in
+ self.collectionViewLayout.add(animations: animations)
+ let invalidationContext = UICollectionViewFlowLayoutInvalidationContext()
+ invalidationContext.invalidateItems(at: items)
+
+ self.layoutAdapterCollectionView.performBatchUpdates({
+ self.layoutAdapterCollectionView.collectionViewLayout.invalidateLayout(with: invalidationContext)
+ })
+ }
+ )
+ }
+
+ private func findIndexPathForCell(withSubview view: UIView) -> UICollectionViewCell? {
+
+ if let cell = view as? UICollectionViewCell {
+ return cell
+ }
+
+ if let superview = view.superview {
+ return findIndexPathForCell(withSubview: superview)
+ }
+
+ return nil
+ }
+
+}
+
+
+PlaygroundPage.current.liveView = MyViewController()
+PlaygroundPage.current.needsIndefiniteExecution = true
diff --git a/LayoutKit.playground/contents.xcplayground b/LayoutKit.playground/contents.xcplayground
index 8fb4aed1..04d8fa14 100644
--- a/LayoutKit.playground/contents.xcplayground
+++ b/LayoutKit.playground/contents.xcplayground
@@ -12,5 +12,6 @@
+
\ No newline at end of file
diff --git a/Sources/Views/ReloadableView.swift b/Sources/Views/ReloadableView.swift
index f00c7a44..b3b579d7 100644
--- a/Sources/Views/ReloadableView.swift
+++ b/Sources/Views/ReloadableView.swift
@@ -42,6 +42,9 @@ public protocol ReloadableView: class {
of concurrent inserts/updates/deletes as UICollectionView documents in `performBatchUpdates`.
*/
func perform(batchUpdates: BatchUpdates, completion: (() -> Void)?)
+
+ // Returns contentView for either a UICollectionViewCell or UITableViewCell
+ func contentView(forIndexPath indexPath: IndexPath) -> UIView?
}
// MARK: - UICollectionView
@@ -93,6 +96,10 @@ extension UICollectionView: ReloadableView {
completion?()
})
}
+
+ open func contentView(forIndexPath indexPath: IndexPath) -> UIView? {
+ return self.cellForItem(at: indexPath)?.contentView
+ }
}
// MARK: - UITableView
@@ -144,4 +151,8 @@ extension UITableView: ReloadableView {
completion?()
}
+
+ open func contentView(forIndexPath indexPath: IndexPath) -> UIView? {
+ return self.cellForRow(at: indexPath)?.contentView
+ }
}
diff --git a/Sources/Views/ReloadableViewLayoutAdapter.swift b/Sources/Views/ReloadableViewLayoutAdapter.swift
index 97614cc0..a53cba17 100644
--- a/Sources/Views/ReloadableViewLayoutAdapter.swift
+++ b/Sources/Views/ReloadableViewLayoutAdapter.swift
@@ -173,6 +173,54 @@ open class ReloadableViewLayoutAdapter: NSObject, ReloadableViewUpdateManagerDel
currentArrangement = arrangement
reloadableView?.reloadDataSynchronously()
}
+
+ /**
+ Computes layouts without applying them on the view. Instead it returns a list of animations that
+ that can be can be used to smoothly resize e.g CollectionViewCells.
+
+ See Playground page CollectionView Animation in LayoutKit.playground for example
+ */
+ open func reload(
+ items: [IndexPath],
+ width: CGFloat? = nil,
+ height: CGFloat? = nil,
+ layoutProvider: @escaping (Void) -> T,
+ completion: @escaping ([Animation]) -> Void) where U.Iterator.Element == Layout, T.Iterator.Element == Section {
+
+ let start = CFAbsoluteTimeGetCurrent()
+ let operation = BlockOperation()
+
+ operation.addExecutionBlock { [weak self, weak operation] in
+ let arrangements: [Section<[LayoutArrangement]>] = layoutProvider().flatMap { sectionLayout in
+ if operation?.isCancelled ?? true {
+ return nil
+ }
+
+ return sectionLayout.map { (layout: Layout) -> LayoutArrangement in
+ return layout.arrangement(width: width, height: height)
+ }
+ }
+
+ let mainOperation = BlockOperation(block: {
+ let animations: [Animation] = items.flatMap({ indexPath in
+ guard let contentView = self?.reloadableView?.contentView(forIndexPath: indexPath) else { return nil }
+ let arrangement = arrangements[indexPath.section].items[indexPath.item]
+ return arrangement.prepareAnimation(for: contentView)
+ })
+ self?.currentArrangement = arrangements
+
+ let end = CFAbsoluteTimeGetCurrent()
+ self?.logger?("user: \((end-start).ms)")
+ completion(animations)
+ })
+
+ if let operation = operation, !operation.isCancelled {
+ OperationQueue.main.addOperation(mainOperation)
+ }
+ }
+
+ backgroundLayoutQueue.addOperation(operation)
+ }
}
/// A section in a `ReloadableView`.