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

//
// 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<Section: Equatable, Value: Equatable> {
fileprivate init(initialSectionedValues: SectionedValues<Section, Value>) {
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<Section, Value> {
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<Section, Value>
fileprivate func processChanges(newState: SectionedValues<Section, Value>, diff: [SectionedDiffStep<Section, Value>]){
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<Section: Equatable, Value: Equatable>: AbstractDiffCalculator<Section, Value> {
/// 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<Section, Value> = 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<Section, Value>, diff: [SectionedDiffStep<Section, Value>]) {
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<Section: Equatable, Value: Equatable> : AbstractDiffCalculator<Section, Value> {
/// 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<Section, Value> = SectionedValues()) {
self.collectionView = collectionView
super.init(initialSectionedValues: initialSectionedValues)
}
override fileprivate func processChanges(newState: SectionedValues<Section, Value>, diff: [SectionedDiffStep<Section, Value>]) {
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<AnyHashable, AnyHashable>
/// See SimpleTableViewDiffCalculator for explanation
typealias SimpleCollectionViewDiffCalculator = CollectionViewDiffCalculator<AnyHashable, AnyHashable>
/// 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<Value: Equatable> {
/// 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<Int, Value> {
let firstRows = (0..<sectionIndex).map { ($0, [Value]()) }
return SectionedValues(firstRows + [(sectionIndex, values)])
}
private let internalDiffCalculator: TableViewDiffCalculator<Int, Value>
}
/// 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<Value: Equatable> {
/// 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<Int, Value>
}
#endif