// // Dwifft+UIKit.swift // Dwifft // // Created by Jack Flintermann on 3/13/15. // Copyright (c) 2015 jflinter. All rights reserved. // #if os(iOS) || os(tvOS) import UIKit /// A parent class for all diff calculators. Don't use it directly. public class AbstractDiffCalculator { fileprivate init(initialSectionedValues: SectionedValues) { self._sectionedValues = initialSectionedValues } /// The number of sections in the diff calculator. Return this inside /// `numberOfSections(in: tableView)` or `numberOfSections(in: collectionView)`. /// Don't implement that method any other way (see the docs for `numberOfObjects(inSection:)` /// for more context). public final func numberOfSections() -> Int { return self.sectionedValues.sections.count } /// The section at a given index. If you implement `tableView:titleForHeaderInSection` or /// `collectionView:viewForSupplementaryElementOfKind:atIndexPath`, you can use this /// method to get information about that section out of Dwifft. /// /// - Parameter forSection: the index of the section you care about. /// - Returns: the Section at that index. public final func value(forSection: Int) -> Section { return self.sectionedValues[forSection].0 } /// The, uh, number of objects in a given section. Use this to implement /// `UITableViewDataSource.numberOfRowsInSection:` or `UICollectionViewDataSource.numberOfItemsInSection:`. /// Seriously, don't implement that method any other way - there is some subtle timing stuff /// around when this value should change in order to satisfy `UITableView`/`UICollectionView`'s internal /// assertions, that Dwifft knows how to handle correctly. Read the source for /// Dwifft+UIKit.swift if you don't believe me/want to learn more. /// /// - Parameter section: a section of your table/collection view /// - Returns: the number of objects in that section. public final func numberOfObjects(inSection section: Int) -> Int { return self.sectionedValues[section].1.count } /// The value at a given index path. Use this to implement /// `UITableViewDataSource.cellForRowAtIndexPath` or `UICollectionViewDataSource.cellForItemAtIndexPath`. /// /// - Parameter indexPath: the index path you are interested in /// - Returns: the thing at that index path public final func value(atIndexPath indexPath: IndexPath) -> Value { return self.sectionedValues[indexPath.section].1[indexPath.row] } /// Set this variable to automatically trigger the correct section/row/item insertion/deletions /// on your table/collection view. public final var sectionedValues: SectionedValues { get { return _sectionedValues } set { let oldSectionedValues = sectionedValues let newSectionedValues = newValue let diff = Dwifft.diff(lhs: oldSectionedValues, rhs: newSectionedValues) if (diff.count > 0) { self.processChanges(newState: newSectionedValues, diff: diff) } } } // UITableView and UICollectionView both perform assertions on the *current* number of rows/items before performing any updates. As such, the `sectionedValues` property must be backed by an internal value that does not change until *after* `beginUpdates`/`performBatchUpdates` has been called. fileprivate final var _sectionedValues: SectionedValues fileprivate func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]){ fatalError("override me") } } /// This class manages a `UITableView`'s rows and sections. It will make the necessary calls to /// the table view to ensure that its UI is kept in sync with the contents of the `sectionedValues` property. public final class TableViewDiffCalculator: AbstractDiffCalculator { /// The table view to be managed public weak var tableView: UITableView? /// Initializes a new diff calculator. /// /// - Parameters: /// - tableView: the table view to be managed /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator. public init(tableView: UITableView?, initialSectionedValues: SectionedValues = SectionedValues()) { self.tableView = tableView super.init(initialSectionedValues: initialSectionedValues) } /// You can change insertion/deletion animations like this! Fade works well. /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing. public var insertionAnimation = UITableView.RowAnimation.automatic, deletionAnimation = UITableView.RowAnimation.automatic public var forceOffAnimationEnabled = false override fileprivate func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { guard let tableView = self.tableView else { return } if forceOffAnimationEnabled { UIView.setAnimationsEnabled(false) } tableView.beginUpdates() self._sectionedValues = newState for result in diff { switch result { case let .delete(section, row, _): tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: self.deletionAnimation) case let .insert(section, row, _): tableView.insertRows(at: [IndexPath(row: row, section: section)], with: self.insertionAnimation) case let .sectionDelete(section, _): tableView.deleteSections(IndexSet(integer: section), with: self.deletionAnimation) case let .sectionInsert(section, _): tableView.insertSections(IndexSet(integer: section), with: self.insertionAnimation) } } tableView.endUpdates() if forceOffAnimationEnabled { UIView.setAnimationsEnabled(true) } } } /// This class manages a `UICollectionView`'s items and sections. It will make the necessary /// calls to the collection view to ensure that its UI is kept in sync with the contents /// of the `sectionedValues` property. public final class CollectionViewDiffCalculator : AbstractDiffCalculator { /// The collection view to be managed. public weak var collectionView: UICollectionView? /// Initializes a new diff calculator. /// /// - Parameters: /// - collectionView: the collection view to be managed. /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator. public init(collectionView: UICollectionView?, initialSectionedValues: SectionedValues = SectionedValues()) { self.collectionView = collectionView super.init(initialSectionedValues: initialSectionedValues) } override fileprivate func processChanges(newState: SectionedValues, diff: [SectionedDiffStep]) { guard let collectionView = self.collectionView else { return } collectionView.performBatchUpdates({ self._sectionedValues = newState for result in diff { switch result { case let .delete(section, row, _): collectionView.deleteItems(at: [IndexPath(row: row, section: section)]) case let .insert(section, row, _): collectionView.insertItems(at: [IndexPath(row: row, section: section)]) case let .sectionDelete(section, _): collectionView.deleteSections(IndexSet(integer: section)) case let .sectionInsert(section, _): collectionView.insertSections(IndexSet(integer: section)) } } }, completion: nil) } } /// Let's say your data model consists of different sections containing different model types. Since /// `SectionedValues` requires a uniform type for all of its rows, this can be a clunky situation. You /// can address this in a couple of ways. The first is to define a custom enum that encompasses all of the /// things that *could* be in your data model - if section 1 has a bunch of `String`s, and section 2 has a bunch /// of `Int`s, define a `StringOrInt` enum that conforms to `Equatable`, and fill the `SectionedValues` /// that you use to drive your DiffCalculator up with those. Alternatively, if you are lazy, and your /// models all conform to `Hashable`, you can use a SimpleTableViewDiffCalculator instead. typealias SimpleTableViewDiffCalculator = TableViewDiffCalculator /// See SimpleTableViewDiffCalculator for explanation typealias SimpleCollectionViewDiffCalculator = CollectionViewDiffCalculator /// If your table view only has a single section, or you only want to power a single section of it with Dwifft, /// use a `SingleSectionTableViewDiffCalculator`. Note that this approach is not highly recommended, and you should /// do so only if it *really* doesn't make sense to just power your whole table with a `TableViewDiffCalculator`. /// You'll be less likely to mess up the index math :P public final class SingleSectionTableViewDiffCalculator { /// The table view to be managed public weak var tableView: UITableView? /// All insertion/deletion calls will be made on this index. public let sectionIndex: Int /// You can change insertion/deletion animations like this! Fade works well. /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing. public var insertionAnimation = UITableView.RowAnimation.automatic { didSet { self.internalDiffCalculator.insertionAnimation = self.insertionAnimation } } public var deletionAnimation = UITableView.RowAnimation.automatic { didSet { self.internalDiffCalculator.deletionAnimation = self.deletionAnimation } } public var forceOffAnimationEnabled = false { didSet { self.internalDiffCalculator.forceOffAnimationEnabled = self.forceOffAnimationEnabled } } /// Set this variable to automatically trigger the correct row insertion/deletions /// on your table view. public var rows : [Value] { get { return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1 } set { self.internalDiffCalculator.sectionedValues = SingleSectionTableViewDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) } } /// Initializes a new diff calculator. /// /// - Parameters: /// - tableView: the table view to be managed /// - initialRows: optional - if specified, these will be the initial contents of the diff calculator. /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. public init(tableView: UITableView?, initialRows: [Value] = [], sectionIndex: Int = 0) { self.tableView = tableView self.internalDiffCalculator = TableViewDiffCalculator(tableView: tableView, initialSectionedValues: SingleSectionTableViewDiffCalculator.buildSectionedValues(values: initialRows, sectionIndex: sectionIndex)) self.sectionIndex = sectionIndex } fileprivate static func buildSectionedValues(values: [Value], sectionIndex: Int) -> SectionedValues { let firstRows = (0.. } /// If your collection view only has a single section, or you only want to power a single section of it with Dwifft, /// use a `SingleSectionCollectionViewDiffCalculator`. Note that this approach is not highly recommended, and you should /// do so only if it *really* doesn't make sense to just power your whole view with a `CollectionViewDiffCalculator`. /// You'll be less likely to mess up the index math :P public final class SingleSectionCollectionViewDiffCalculator { /// The collection view to be managed public weak var collectionView: UICollectionView? /// All insertion/deletion calls will be made for items at this section. public let sectionIndex: Int /// Set this variable to automatically trigger the correct item insertion/deletions /// on your collection view. public var items : [Value] { get { return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1 } set { self.internalDiffCalculator.sectionedValues = SingleSectionTableViewDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex) } } /// Initializes a new diff calculator. /// /// - Parameters: /// - tableView: the table view to be managed /// - initialItems: optional - if specified, these will be the initial contents of the diff calculator. /// - sectionIndex: optional - all insertion/deletion calls will be made on this index. public init(collectionView: UICollectionView?, initialItems: [Value] = [], sectionIndex: Int = 0) { self.collectionView = collectionView self.internalDiffCalculator = CollectionViewDiffCalculator(collectionView: collectionView, initialSectionedValues: SingleSectionTableViewDiffCalculator.buildSectionedValues(values: initialItems, sectionIndex: sectionIndex)) self.sectionIndex = sectionIndex } private let internalDiffCalculator: CollectionViewDiffCalculator } #endif