You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
14 KiB

5 years ago
  1. //
  2. // Dwifft+UIKit.swift
  3. // Dwifft
  4. //
  5. // Created by Jack Flintermann on 3/13/15.
  6. // Copyright (c) 2015 jflinter. All rights reserved.
  7. //
  8. #if os(iOS) || os(tvOS)
  9. import UIKit
  10. /// A parent class for all diff calculators. Don't use it directly.
  11. public class AbstractDiffCalculator<Section: Equatable, Value: Equatable> {
  12. fileprivate init(initialSectionedValues: SectionedValues<Section, Value>) {
  13. self._sectionedValues = initialSectionedValues
  14. }
  15. /// The number of sections in the diff calculator. Return this inside
  16. /// `numberOfSections(in: tableView)` or `numberOfSections(in: collectionView)`.
  17. /// Don't implement that method any other way (see the docs for `numberOfObjects(inSection:)`
  18. /// for more context).
  19. public final func numberOfSections() -> Int {
  20. return self.sectionedValues.sections.count
  21. }
  22. /// The section at a given index. If you implement `tableView:titleForHeaderInSection` or
  23. /// `collectionView:viewForSupplementaryElementOfKind:atIndexPath`, you can use this
  24. /// method to get information about that section out of Dwifft.
  25. ///
  26. /// - Parameter forSection: the index of the section you care about.
  27. /// - Returns: the Section at that index.
  28. public final func value(forSection: Int) -> Section {
  29. return self.sectionedValues[forSection].0
  30. }
  31. /// The, uh, number of objects in a given section. Use this to implement
  32. /// `UITableViewDataSource.numberOfRowsInSection:` or `UICollectionViewDataSource.numberOfItemsInSection:`.
  33. /// Seriously, don't implement that method any other way - there is some subtle timing stuff
  34. /// around when this value should change in order to satisfy `UITableView`/`UICollectionView`'s internal
  35. /// assertions, that Dwifft knows how to handle correctly. Read the source for
  36. /// Dwifft+UIKit.swift if you don't believe me/want to learn more.
  37. ///
  38. /// - Parameter section: a section of your table/collection view
  39. /// - Returns: the number of objects in that section.
  40. public final func numberOfObjects(inSection section: Int) -> Int {
  41. return self.sectionedValues[section].1.count
  42. }
  43. /// The value at a given index path. Use this to implement
  44. /// `UITableViewDataSource.cellForRowAtIndexPath` or `UICollectionViewDataSource.cellForItemAtIndexPath`.
  45. ///
  46. /// - Parameter indexPath: the index path you are interested in
  47. /// - Returns: the thing at that index path
  48. public final func value(atIndexPath indexPath: IndexPath) -> Value {
  49. return self.sectionedValues[indexPath.section].1[indexPath.row]
  50. }
  51. /// Set this variable to automatically trigger the correct section/row/item insertion/deletions
  52. /// on your table/collection view.
  53. public final var sectionedValues: SectionedValues<Section, Value> {
  54. get {
  55. return _sectionedValues
  56. }
  57. set {
  58. let oldSectionedValues = sectionedValues
  59. let newSectionedValues = newValue
  60. let diff = Dwifft.diff(lhs: oldSectionedValues, rhs: newSectionedValues)
  61. if (diff.count > 0) {
  62. self.processChanges(newState: newSectionedValues, diff: diff)
  63. }
  64. }
  65. }
  66. // 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.
  67. fileprivate final var _sectionedValues: SectionedValues<Section, Value>
  68. fileprivate func processChanges(newState: SectionedValues<Section, Value>, diff: [SectionedDiffStep<Section, Value>]){
  69. fatalError("override me")
  70. }
  71. }
  72. /// This class manages a `UITableView`'s rows and sections. It will make the necessary calls to
  73. /// the table view to ensure that its UI is kept in sync with the contents of the `sectionedValues` property.
  74. public final class TableViewDiffCalculator<Section: Equatable, Value: Equatable>: AbstractDiffCalculator<Section, Value> {
  75. /// The table view to be managed
  76. public weak var tableView: UITableView?
  77. /// Initializes a new diff calculator.
  78. ///
  79. /// - Parameters:
  80. /// - tableView: the table view to be managed
  81. /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator.
  82. public init(tableView: UITableView?, initialSectionedValues: SectionedValues<Section, Value> = SectionedValues()) {
  83. self.tableView = tableView
  84. super.init(initialSectionedValues: initialSectionedValues)
  85. }
  86. /// You can change insertion/deletion animations like this! Fade works well.
  87. /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing.
  88. public var insertionAnimation = UITableView.RowAnimation.automatic, deletionAnimation = UITableView.RowAnimation.automatic
  89. public var forceOffAnimationEnabled = false
  90. override fileprivate func processChanges(newState: SectionedValues<Section, Value>, diff: [SectionedDiffStep<Section, Value>]) {
  91. guard let tableView = self.tableView else { return }
  92. if forceOffAnimationEnabled {
  93. UIView.setAnimationsEnabled(false)
  94. }
  95. tableView.beginUpdates()
  96. self._sectionedValues = newState
  97. for result in diff {
  98. switch result {
  99. case let .delete(section, row, _): tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: self.deletionAnimation)
  100. case let .insert(section, row, _): tableView.insertRows(at: [IndexPath(row: row, section: section)], with: self.insertionAnimation)
  101. case let .sectionDelete(section, _): tableView.deleteSections(IndexSet(integer: section), with: self.deletionAnimation)
  102. case let .sectionInsert(section, _): tableView.insertSections(IndexSet(integer: section), with: self.insertionAnimation)
  103. }
  104. }
  105. tableView.endUpdates()
  106. if forceOffAnimationEnabled {
  107. UIView.setAnimationsEnabled(true)
  108. }
  109. }
  110. }
  111. /// This class manages a `UICollectionView`'s items and sections. It will make the necessary
  112. /// calls to the collection view to ensure that its UI is kept in sync with the contents
  113. /// of the `sectionedValues` property.
  114. public final class CollectionViewDiffCalculator<Section: Equatable, Value: Equatable> : AbstractDiffCalculator<Section, Value> {
  115. /// The collection view to be managed.
  116. public weak var collectionView: UICollectionView?
  117. /// Initializes a new diff calculator.
  118. ///
  119. /// - Parameters:
  120. /// - collectionView: the collection view to be managed.
  121. /// - initialSectionedValues: optional - if specified, these will be the initial contents of the diff calculator.
  122. public init(collectionView: UICollectionView?, initialSectionedValues: SectionedValues<Section, Value> = SectionedValues()) {
  123. self.collectionView = collectionView
  124. super.init(initialSectionedValues: initialSectionedValues)
  125. }
  126. override fileprivate func processChanges(newState: SectionedValues<Section, Value>, diff: [SectionedDiffStep<Section, Value>]) {
  127. guard let collectionView = self.collectionView else { return }
  128. collectionView.performBatchUpdates({
  129. self._sectionedValues = newState
  130. for result in diff {
  131. switch result {
  132. case let .delete(section, row, _): collectionView.deleteItems(at: [IndexPath(row: row, section: section)])
  133. case let .insert(section, row, _): collectionView.insertItems(at: [IndexPath(row: row, section: section)])
  134. case let .sectionDelete(section, _): collectionView.deleteSections(IndexSet(integer: section))
  135. case let .sectionInsert(section, _): collectionView.insertSections(IndexSet(integer: section))
  136. }
  137. }
  138. }, completion: nil)
  139. }
  140. }
  141. /// Let's say your data model consists of different sections containing different model types. Since
  142. /// `SectionedValues` requires a uniform type for all of its rows, this can be a clunky situation. You
  143. /// can address this in a couple of ways. The first is to define a custom enum that encompasses all of the
  144. /// things that *could* be in your data model - if section 1 has a bunch of `String`s, and section 2 has a bunch
  145. /// of `Int`s, define a `StringOrInt` enum that conforms to `Equatable`, and fill the `SectionedValues`
  146. /// that you use to drive your DiffCalculator up with those. Alternatively, if you are lazy, and your
  147. /// models all conform to `Hashable`, you can use a SimpleTableViewDiffCalculator instead.
  148. typealias SimpleTableViewDiffCalculator = TableViewDiffCalculator<AnyHashable, AnyHashable>
  149. /// See SimpleTableViewDiffCalculator for explanation
  150. typealias SimpleCollectionViewDiffCalculator = CollectionViewDiffCalculator<AnyHashable, AnyHashable>
  151. /// If your table view only has a single section, or you only want to power a single section of it with Dwifft,
  152. /// use a `SingleSectionTableViewDiffCalculator`. Note that this approach is not highly recommended, and you should
  153. /// do so only if it *really* doesn't make sense to just power your whole table with a `TableViewDiffCalculator`.
  154. /// You'll be less likely to mess up the index math :P
  155. public final class SingleSectionTableViewDiffCalculator<Value: Equatable> {
  156. /// The table view to be managed
  157. public weak var tableView: UITableView?
  158. /// All insertion/deletion calls will be made on this index.
  159. public let sectionIndex: Int
  160. /// You can change insertion/deletion animations like this! Fade works well.
  161. /// So does Top/Bottom. Left/Right/Middle are a little weird, but hey, do your thing.
  162. public var insertionAnimation = UITableView.RowAnimation.automatic {
  163. didSet {
  164. self.internalDiffCalculator.insertionAnimation = self.insertionAnimation
  165. }
  166. }
  167. public var deletionAnimation = UITableView.RowAnimation.automatic {
  168. didSet {
  169. self.internalDiffCalculator.deletionAnimation = self.deletionAnimation
  170. }
  171. }
  172. public var forceOffAnimationEnabled = false {
  173. didSet {
  174. self.internalDiffCalculator.forceOffAnimationEnabled = self.forceOffAnimationEnabled
  175. }
  176. }
  177. /// Set this variable to automatically trigger the correct row insertion/deletions
  178. /// on your table view.
  179. public var rows : [Value] {
  180. get {
  181. return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1
  182. }
  183. set {
  184. self.internalDiffCalculator.sectionedValues = SingleSectionTableViewDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex)
  185. }
  186. }
  187. /// Initializes a new diff calculator.
  188. ///
  189. /// - Parameters:
  190. /// - tableView: the table view to be managed
  191. /// - initialRows: optional - if specified, these will be the initial contents of the diff calculator.
  192. /// - sectionIndex: optional - all insertion/deletion calls will be made on this index.
  193. public init(tableView: UITableView?, initialRows: [Value] = [], sectionIndex: Int = 0) {
  194. self.tableView = tableView
  195. self.internalDiffCalculator = TableViewDiffCalculator(tableView: tableView, initialSectionedValues: SingleSectionTableViewDiffCalculator.buildSectionedValues(values: initialRows, sectionIndex: sectionIndex))
  196. self.sectionIndex = sectionIndex
  197. }
  198. fileprivate static func buildSectionedValues(values: [Value], sectionIndex: Int) -> SectionedValues<Int, Value> {
  199. let firstRows = (0..<sectionIndex).map { ($0, [Value]()) }
  200. return SectionedValues(firstRows + [(sectionIndex, values)])
  201. }
  202. private let internalDiffCalculator: TableViewDiffCalculator<Int, Value>
  203. }
  204. /// If your collection view only has a single section, or you only want to power a single section of it with Dwifft,
  205. /// use a `SingleSectionCollectionViewDiffCalculator`. Note that this approach is not highly recommended, and you should
  206. /// do so only if it *really* doesn't make sense to just power your whole view with a `CollectionViewDiffCalculator`.
  207. /// You'll be less likely to mess up the index math :P
  208. public final class SingleSectionCollectionViewDiffCalculator<Value: Equatable> {
  209. /// The collection view to be managed
  210. public weak var collectionView: UICollectionView?
  211. /// All insertion/deletion calls will be made for items at this section.
  212. public let sectionIndex: Int
  213. /// Set this variable to automatically trigger the correct item insertion/deletions
  214. /// on your collection view.
  215. public var items : [Value] {
  216. get {
  217. return self.internalDiffCalculator.sectionedValues[self.sectionIndex].1
  218. }
  219. set {
  220. self.internalDiffCalculator.sectionedValues = SingleSectionTableViewDiffCalculator.buildSectionedValues(values: newValue, sectionIndex: self.sectionIndex)
  221. }
  222. }
  223. /// Initializes a new diff calculator.
  224. ///
  225. /// - Parameters:
  226. /// - tableView: the table view to be managed
  227. /// - initialItems: optional - if specified, these will be the initial contents of the diff calculator.
  228. /// - sectionIndex: optional - all insertion/deletion calls will be made on this index.
  229. public init(collectionView: UICollectionView?, initialItems: [Value] = [], sectionIndex: Int = 0) {
  230. self.collectionView = collectionView
  231. self.internalDiffCalculator = CollectionViewDiffCalculator(collectionView: collectionView, initialSectionedValues: SingleSectionTableViewDiffCalculator.buildSectionedValues(values: initialItems, sectionIndex: sectionIndex))
  232. self.sectionIndex = sectionIndex
  233. }
  234. private let internalDiffCalculator: CollectionViewDiffCalculator<Int, Value>
  235. }
  236. #endif