diff --git a/Sources/Extensions/AppKitExtension.swift b/Sources/Extensions/AppKitExtension.swift index 0570448..bb4df19 100644 --- a/Sources/Extensions/AppKitExtension.swift +++ b/Sources/Extensions/AppKitExtension.swift @@ -11,8 +11,10 @@ public extension NSTableView { /// - 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 + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of NSTableView. @@ -20,6 +22,7 @@ public extension NSTableView { using stagedChangeset: StagedChangeset, with animation: @autoclosure () -> NSTableView.AnimationOptions, interrupt: ((Changeset) -> Bool)? = nil, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { reload( @@ -28,6 +31,7 @@ public extension NSTableView { insertRowsAnimation: animation(), reloadRowsAnimation: animation(), interrupt: interrupt, + completion: completion, setData: setData ) } @@ -43,8 +47,10 @@ public extension NSTableView { /// - 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 + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of NSTableView. func reload( @@ -53,17 +59,22 @@ public extension NSTableView { insertRowsAnimation: @autoclosure () -> NSTableView.AnimationOptions, reloadRowsAnimation: @autoclosure () -> NSTableView.AnimationOptions, interrupt: ((Changeset) -> Bool)? = nil, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { if case .none = window, let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } for changeset in stagedChangeset { if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } beginUpdates() @@ -95,6 +106,8 @@ public extension NSTableView { endUpdates() } + + completion?(true) } } @@ -108,28 +121,59 @@ public extension NSCollectionView { /// /// - Parameters: /// - stagedChangeset: A staged set of changes. - /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of NSCollectionView. func reload( using stagedChangeset: StagedChangeset, interrupt: ((Changeset) -> Bool)? = nil, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { if case .none = window, let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } + let dispatchGroup: DispatchGroup? = completion != nil + ? DispatchGroup() + : nil + let completionHandler: ((Bool) -> Void)? = completion != nil + ? { _ in dispatchGroup!.leave() } + : nil + for changeset in stagedChangeset { if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } animator().performBatchUpdates({ setData(changeset.data) + dispatchGroup?.enter() + + if !changeset.sectionDeleted.isEmpty { + deleteSections(IndexSet(changeset.sectionDeleted)) + } + + if !changeset.sectionInserted.isEmpty { + insertSections(IndexSet(changeset.sectionInserted)) + } + + if !changeset.sectionUpdated.isEmpty { + reloadSections(IndexSet(changeset.sectionUpdated)) + } + + for (source, target) in changeset.sectionMoved { + moveSection(source, toSection: target) + } if !changeset.elementDeleted.isEmpty { deleteItems(at: Set(changeset.elementDeleted.map { IndexPath(item: $0.element, section: $0.section) })) @@ -146,7 +190,10 @@ public extension NSCollectionView { for (source, target) in changeset.elementMoved { moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section)) } - }) + }, completionHandler: completionHandler) + } + dispatchGroup?.notify(queue: .main) { + completion!(true) } } } diff --git a/Sources/Extensions/UIKitExtension.swift b/Sources/Extensions/UIKitExtension.swift index 206ac7c..c3d6ae6 100644 --- a/Sources/Extensions/UIKitExtension.swift +++ b/Sources/Extensions/UIKitExtension.swift @@ -11,14 +11,17 @@ public extension UITableView { /// - 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 + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - 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, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { reload( @@ -30,6 +33,7 @@ public extension UITableView { insertRowsAnimation: animation(), reloadRowsAnimation: animation(), interrupt: interrupt, + completion: completion, setData: setData ) } @@ -48,8 +52,10 @@ public extension UITableView { /// - 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 + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of UITableView. func reload( @@ -61,21 +67,34 @@ public extension UITableView { insertRowsAnimation: @autoclosure () -> RowAnimation, reloadRowsAnimation: @autoclosure () -> RowAnimation, interrupt: ((Changeset) -> Bool)? = nil, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { if case .none = window, let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } + let dispatchGroup: DispatchGroup? = completion != nil + ? DispatchGroup() + : nil + let completionHandler: ((Bool) -> Void)? = completion != nil + ? { _ in dispatchGroup!.leave() } + : nil + for changeset in stagedChangeset { if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } - _performBatchUpdates { + _performBatchUpdates({ setData(changeset.data) + dispatchGroup?.enter() if !changeset.sectionDeleted.isEmpty { deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation()) @@ -108,18 +127,22 @@ public extension UITableView { for (source, target) in changeset.elementMoved { moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section)) } - } + }, completionHandler: completionHandler) + } + dispatchGroup?.notify(queue: .main) { + completion!(true) } } - private func _performBatchUpdates(_ updates: () -> Void) { + private func _performBatchUpdates(_ updates: () -> Void, completionHandler: ((Bool) -> Void)? = nil) { if #available(iOS 11.0, tvOS 11.0, *) { - performBatchUpdates(updates) + performBatchUpdates(updates, completion: completionHandler) } else { beginUpdates() updates() endUpdates() + completionHandler?(true) } } } @@ -133,28 +156,43 @@ public extension UICollectionView { /// /// - Parameters: /// - stagedChangeset: A staged set of changes. - /// - interrupt: A closure that takes an changeset as its argument and returns `true` if the animated + /// - interrupt: A closure that takes a changeset as its argument and returns `true` if the animated /// updates should be stopped and performed reloadData. Default is nil. + /// - completion: A closure that is called when the animated updates have finished. + /// The argument is` true` if the animation ran to completion before it stopped or `false` if it did not. /// - setData: A closure that takes the collection as a parameter. /// The collection should be set to data-source of UICollectionView. func reload( using stagedChangeset: StagedChangeset, interrupt: ((Changeset) -> Bool)? = nil, + completion: ((Bool) -> Void)? = nil, setData: (C) -> Void ) { if case .none = window, let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } + let dispatchGroup: DispatchGroup? = completion != nil + ? DispatchGroup() + : nil + let completionHandler: ((Bool) -> Void)? = completion != nil + ? { _ in dispatchGroup!.leave() } + : nil + for changeset in stagedChangeset { if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data { setData(data) - return reloadData() + reloadData() + completion?(false) + return } performBatchUpdates({ setData(changeset.data) + dispatchGroup?.enter() if !changeset.sectionDeleted.isEmpty { deleteSections(IndexSet(changeset.sectionDeleted)) @@ -187,7 +225,10 @@ public extension UICollectionView { for (source, target) in changeset.elementMoved { moveItem(at: IndexPath(item: source.element, section: source.section), to: IndexPath(item: target.element, section: target.section)) } - }) + }, completion: completionHandler) + } + dispatchGroup?.notify(queue: .main) { + completion!(true) } } }