mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-03-20 16:43:55 +01:00
back-portable TableView DiffableDataSource
This commit is contained in:
@@ -31,7 +31,6 @@ import CoreData
|
||||
|
||||
// MARK: - DataStack
|
||||
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
extension DataStack {
|
||||
|
||||
public func liveList<D>(_ from: From<D>, _ fetchClauses: FetchClause...) -> LiveList<D> {
|
||||
|
||||
@@ -29,79 +29,324 @@ import UIKit
|
||||
import CoreData
|
||||
|
||||
|
||||
//// MARK: - DiffableDataSource
|
||||
//
|
||||
//extension DiffableDataSource {
|
||||
//
|
||||
// // MARK: - TableView
|
||||
//
|
||||
// public open class TableView<D: DynamicObject>: NSObject, UITableViewDataSource {
|
||||
//
|
||||
// // MARK: Public
|
||||
//
|
||||
// public typealias ObjectType = D
|
||||
//
|
||||
// public var defaultRowAnimation: UITableView.RowAnimation = .automatic
|
||||
//
|
||||
// public init(tableView: UITableView, cellProvider: @escaping (UITableView, IndexPath, ObjectType) -> UITableViewCell?) {
|
||||
//
|
||||
// self.tableView = tableView
|
||||
// self.cellProvider = cellProvider
|
||||
//
|
||||
// MARK: - DiffableDataSource
|
||||
|
||||
extension DiffableDataSource {
|
||||
|
||||
// MARK: - TableView
|
||||
|
||||
open class TableView<O: DynamicObject>: NSObject, UITableViewDataSource {
|
||||
|
||||
// MARK: Open
|
||||
|
||||
@nonobjc
|
||||
open var defaultRowAnimation: UITableView.RowAnimation = .automatic
|
||||
|
||||
|
||||
// MARK: Public
|
||||
|
||||
public typealias ObjectType = O
|
||||
|
||||
@nonobjc
|
||||
public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, ObjectType) -> UITableViewCell?) {
|
||||
|
||||
self.tableView = tableView
|
||||
self.cellProvider = cellProvider
|
||||
self.dataStack = dataStack
|
||||
|
||||
super.init()
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// self.rawDataSource = UITableViewDiffableDataSource<String, D.ObjectID>(
|
||||
// self.rawDataSource = UITableViewDiffableDataSource<String, O.ObjectID>(
|
||||
// tableView: tableView,
|
||||
// cellProvider: { (tableView, indexPath, managedObjectID) -> UITableViewCell? in
|
||||
// cellProvider: { [weak self] (tableView, indexPath, objectID) -> UITableViewCell? in
|
||||
//
|
||||
// cellProvider(
|
||||
// guard let self = self else {
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
// guard let object = self.dataStack.fetchExisting(objectID) as O? else {
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
// return self.cellProvider(tableView, indexPath, object)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// else {
|
||||
//
|
||||
// self.rawDataSource = nil
|
||||
|
||||
self.rawDataSource = Internals.DiffableDataUIDispatcher<O>(dataStack: dataStack)
|
||||
// }
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// tableView.dataSource = self
|
||||
// }
|
||||
//
|
||||
// public func apply(_ snapshot: ListSnapshot<ObjectType>, animatingDifferences: Bool = true) {
|
||||
//
|
||||
// let dataSource = UITableViewDiffableDataSource<String, D>.
|
||||
|
||||
tableView.dataSource = self
|
||||
}
|
||||
|
||||
public func apply(_ snapshot: ListSnapshot<ObjectType>, animatingDifferences: Bool = true) {
|
||||
|
||||
let diffableSnapshot = snapshot.diffableSnapshot
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// self.rawDataSource! as! UITableViewDiffableDataSource<String, D>
|
||||
//
|
||||
// self.modernDataSource.apply(
|
||||
// diffableSnapshot as! NSDiffableDataSourceSnapshot<String, NSManagedObjectID>,
|
||||
// animatingDifferences: animatingDifferences,
|
||||
// completion: nil
|
||||
// )
|
||||
// }
|
||||
// else {
|
||||
//
|
||||
|
||||
self.legacyDataSource.apply(
|
||||
diffableSnapshot as! Internals.DiffableDataSourceSnapshot,
|
||||
view: self.tableView,
|
||||
animatingDifferences: animatingDifferences,
|
||||
performUpdates: { tableView, changeset, setSections in
|
||||
|
||||
tableView.reload(
|
||||
using: changeset,
|
||||
with: self.defaultRowAnimation,
|
||||
setData: setSections
|
||||
)
|
||||
}
|
||||
)
|
||||
// }
|
||||
// core.apply(
|
||||
// snapshot,
|
||||
// view: tableView,
|
||||
// animatingDifferences: animatingDifferences,
|
||||
// performUpdates: { tableView, changeset, setSections in
|
||||
// tableView.reload(using: changeset, with: self.defaultRowAnimation, setData: setSections)
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
public func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return self.modernDataSource.itemIdentifier(for: indexPath)
|
||||
// }
|
||||
// else {
|
||||
|
||||
return self.legacyDataSource.itemIdentifier(for: indexPath)
|
||||
// }
|
||||
}
|
||||
|
||||
public func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// // MARK: Private
|
||||
// return self.modernDataSource.indexPath(for: itemIdentifier)
|
||||
// }
|
||||
// else {
|
||||
|
||||
return self.legacyDataSource.indexPath(for: itemIdentifier)
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
@objc
|
||||
public dynamic func numberOfSections(in tableView: UITableView) -> Int {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// private weak var tableView: UITableView?
|
||||
// private let cellProvider: (UITableView, IndexPath, ObjectType) -> UITableViewCell?
|
||||
// private let rawDataSource: Any?
|
||||
// return self.modernDataSource.numberOfSections(in: tableView)
|
||||
// }
|
||||
// else {
|
||||
|
||||
return self.legacyDataSource.numberOfSections()
|
||||
// }
|
||||
}
|
||||
|
||||
@objc
|
||||
public dynamic func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return self.modernDataSource.tableView(tableView, numberOfRowsInSection: section)
|
||||
// }
|
||||
// else {
|
||||
|
||||
return self.legacyDataSource.numberOfItems(inSection: section)
|
||||
// }
|
||||
}
|
||||
|
||||
@objc
|
||||
open dynamic func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return self.modernDataSource.snapshot().sectionIdentifiers[section]
|
||||
// }
|
||||
// else {
|
||||
|
||||
return self.legacyDataSource.sectionIdentifier(inSection: section)
|
||||
// }
|
||||
}
|
||||
|
||||
@objc
|
||||
open dynamic func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return self.modernDataSource.tableView(tableView, titleForFooterInSection: section)
|
||||
// }
|
||||
// else {
|
||||
|
||||
return nil
|
||||
// }
|
||||
}
|
||||
|
||||
@objc
|
||||
open dynamic func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return self.modernDataSource.tableView(tableView, cellForRowAt: indexPath)
|
||||
// }
|
||||
// else {
|
||||
|
||||
guard let objectID = self.legacyDataSource.itemIdentifier(for: indexPath) else {
|
||||
|
||||
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
|
||||
}
|
||||
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
|
||||
|
||||
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
|
||||
}
|
||||
guard let cell = self.cellProvider(tableView, indexPath, object) else {
|
||||
|
||||
Internals.abort("\(Internals.typeName(UITableViewDataSource.self)) returned a `nil` cell for \(Internals.typeName(IndexPath.self)) \(indexPath)")
|
||||
}
|
||||
return cell
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private weak var tableView: UITableView?
|
||||
|
||||
private let dataStack: DataStack
|
||||
private let cellProvider: (UITableView, IndexPath, ObjectType) -> UITableViewCell?
|
||||
private var rawDataSource: Any!
|
||||
|
||||
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
// private var diffableDataSource: UITableViewDiffableDataSource<String, D.ObjectID> {
|
||||
// private var modernDataSource: UITableViewDiffableDataSource<String, O.ObjectID> {
|
||||
//
|
||||
// return self.rawDataSource! as! UITableViewDiffableDataSource<String, D.ObjectID>
|
||||
// return self.rawDataSource as! UITableViewDiffableDataSource<String, O.ObjectID>
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
private var legacyDataSource: Internals.DiffableDataUIDispatcher<O> {
|
||||
|
||||
return self.rawDataSource as! Internals.DiffableDataUIDispatcher<O>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
extension UITableView {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
@nonobjc
|
||||
fileprivate func reload<C, O>(
|
||||
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
|
||||
with animation: @autoclosure () -> RowAnimation,
|
||||
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
|
||||
setData: (C) -> Void
|
||||
) {
|
||||
|
||||
self.reload(
|
||||
using: stagedChangeset,
|
||||
deleteSectionsAnimation: animation(),
|
||||
insertSectionsAnimation: animation(),
|
||||
reloadSectionsAnimation: animation(),
|
||||
deleteRowsAnimation: animation(),
|
||||
insertRowsAnimation: animation(),
|
||||
reloadRowsAnimation: animation(),
|
||||
interrupt: interrupt,
|
||||
setData: setData
|
||||
)
|
||||
}
|
||||
|
||||
@nonobjc
|
||||
fileprivate func reload<C, O>(
|
||||
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
|
||||
deleteSectionsAnimation: @autoclosure () -> RowAnimation,
|
||||
insertSectionsAnimation: @autoclosure () -> RowAnimation,
|
||||
reloadSectionsAnimation: @autoclosure () -> RowAnimation,
|
||||
deleteRowsAnimation: @autoclosure () -> RowAnimation,
|
||||
insertRowsAnimation: @autoclosure () -> RowAnimation,
|
||||
reloadRowsAnimation: @autoclosure () -> RowAnimation,
|
||||
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
|
||||
setData: (C) -> Void
|
||||
) {
|
||||
|
||||
if case .none = window, let data = stagedChangeset.last?.data {
|
||||
|
||||
setData(data)
|
||||
self.reloadData()
|
||||
return
|
||||
}
|
||||
for changeset in stagedChangeset {
|
||||
|
||||
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
|
||||
|
||||
setData(data)
|
||||
self.reloadData()
|
||||
return
|
||||
}
|
||||
self.cs_performBatchUpdates {
|
||||
|
||||
setData(changeset.data)
|
||||
|
||||
if !changeset.sectionDeleted.isEmpty {
|
||||
|
||||
self.deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation())
|
||||
}
|
||||
if !changeset.sectionInserted.isEmpty {
|
||||
|
||||
self.insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation())
|
||||
}
|
||||
if !changeset.sectionUpdated.isEmpty {
|
||||
|
||||
self.reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation())
|
||||
}
|
||||
for (source, target) in changeset.sectionMoved {
|
||||
|
||||
self.moveSection(source, toSection: target)
|
||||
}
|
||||
if !changeset.elementDeleted.isEmpty {
|
||||
|
||||
self.deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation())
|
||||
}
|
||||
if !changeset.elementInserted.isEmpty {
|
||||
|
||||
self.insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation())
|
||||
}
|
||||
if !changeset.elementUpdated.isEmpty {
|
||||
|
||||
self.reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation())
|
||||
}
|
||||
for (source, target) in changeset.elementMoved {
|
||||
|
||||
self.moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc
|
||||
private func cs_performBatchUpdates(_ updates: () -> Void) {
|
||||
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
|
||||
self.performBatchUpdates(updates)
|
||||
}
|
||||
else {
|
||||
|
||||
self.beginUpdates()
|
||||
updates()
|
||||
self.endUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
62
Sources/Differentiable.swift
Normal file
62
Sources/Differentiable.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Differentiable.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Differentiable
|
||||
|
||||
@usableFromInline
|
||||
internal protocol Differentiable {
|
||||
|
||||
associatedtype DifferenceIdentifier: Hashable
|
||||
|
||||
var differenceIdentifier: DifferenceIdentifier { get }
|
||||
|
||||
func isContentEqual(to source: Self) -> Bool
|
||||
}
|
||||
|
||||
extension Differentiable where Self: AnyObject {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal var differenceIdentifier: ObjectIdentifier {
|
||||
|
||||
return .init(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - DifferentiableSection
|
||||
|
||||
@usableFromInline
|
||||
internal protocol DifferentiableSection: Differentiable {
|
||||
|
||||
associatedtype Collection: Swift.Collection where Collection.Element: Differentiable
|
||||
|
||||
var elements: Collection { get }
|
||||
|
||||
init<S: Sequence>(source: Self, elements: S) where S.Element == Collection.Element
|
||||
}
|
||||
54
Sources/Internals.Closure.swift
Normal file
54
Sources/Internals.Closure.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Internals.Closure.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension Internals {
|
||||
|
||||
// MARK: - Closure
|
||||
|
||||
internal final class Closure<T, U> {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
internal init(_ closure: @escaping (T) -> U) {
|
||||
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
internal func invoke(with argument: T) -> U {
|
||||
|
||||
return self.closure(argument)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let closure: (T) -> U
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,18 @@ extension Internals {
|
||||
|
||||
self.structure = .init(sections: sections)
|
||||
}
|
||||
|
||||
var sections: [Section] {
|
||||
|
||||
get {
|
||||
|
||||
return self.structure.sections
|
||||
}
|
||||
set {
|
||||
|
||||
self.structure.sections = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: DiffableDataSourceSnapshotProtocol
|
||||
@@ -186,48 +198,66 @@ extension Internals {
|
||||
private var structure: BackingStructure
|
||||
|
||||
|
||||
// MARK: - ItemStateID
|
||||
// MARK: - Section
|
||||
|
||||
internal struct ItemStateID: Identifiable, Equatable {
|
||||
internal struct Section: DifferentiableSection, Equatable {
|
||||
|
||||
let stateTag: UUID
|
||||
var isReloaded: Bool
|
||||
|
||||
init(id: NSManagedObjectID, stateTag: UUID) {
|
||||
|
||||
self.id = id
|
||||
self.stateTag = stateTag
|
||||
init(differenceIdentifier: String, items: [Item] = [], isReloaded: Bool = false) {
|
||||
|
||||
self.differenceIdentifier = differenceIdentifier
|
||||
self.elements = items
|
||||
self.isReloaded = isReloaded
|
||||
}
|
||||
|
||||
func isContentEqual(to source: ItemStateID) -> Bool {
|
||||
// MARK: Differentiable
|
||||
|
||||
return self.id == source.id && self.stateTag == source.stateTag
|
||||
let differenceIdentifier: String
|
||||
|
||||
func isContentEqual(to source: Section) -> Bool {
|
||||
|
||||
return !self.isReloaded
|
||||
&& self.differenceIdentifier == source.differenceIdentifier
|
||||
}
|
||||
|
||||
|
||||
// MARK: DifferentiableSection
|
||||
|
||||
var elements: [Item] = []
|
||||
|
||||
// MARK: Identifiable
|
||||
init<S: Sequence>(source: Section, elements: S) where S.Element == Item {
|
||||
|
||||
let id: NSManagedObjectID
|
||||
self.init(
|
||||
differenceIdentifier: source.differenceIdentifier,
|
||||
items: Array(elements),
|
||||
isReloaded: source.isReloaded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SectionStateID
|
||||
// MARK: - Item
|
||||
|
||||
internal struct SectionStateID: Identifiable, Equatable {
|
||||
internal struct Item: Differentiable, Equatable {
|
||||
|
||||
let stateTag: UUID
|
||||
var isReloaded: Bool
|
||||
|
||||
init(id: String, stateTag: UUID) {
|
||||
self.id = id
|
||||
self.stateTag = stateTag
|
||||
init(differenceIdentifier: NSManagedObjectID, isReloaded: Bool = false) {
|
||||
|
||||
self.differenceIdentifier = differenceIdentifier
|
||||
self.isReloaded = isReloaded
|
||||
}
|
||||
|
||||
func isContentEqual(to source: SectionStateID) -> Bool {
|
||||
// MARK: Differentiable
|
||||
|
||||
return self.id == source.id && self.stateTag == source.stateTag
|
||||
let differenceIdentifier: NSManagedObjectID
|
||||
|
||||
func isContentEqual(to source: Item) -> Bool {
|
||||
|
||||
return !self.isReloaded
|
||||
&& self.differenceIdentifier == source.differenceIdentifier
|
||||
}
|
||||
|
||||
// MARK: Identifiable
|
||||
|
||||
let id: String
|
||||
}
|
||||
|
||||
|
||||
@@ -249,22 +279,22 @@ extension Internals {
|
||||
self.sections = sections.map {
|
||||
|
||||
Section(
|
||||
id: $0.name,
|
||||
differenceIdentifier: $0.name,
|
||||
items: $0.objects?
|
||||
.compactMap({ ($0 as? NSManagedObject)?.objectID })
|
||||
.map({ Item(id: $0) }) ?? []
|
||||
.map({ Item(differenceIdentifier: $0) }) ?? []
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var allSectionIDs: [String] {
|
||||
|
||||
return self.sections.map({ $0.id })
|
||||
return self.sections.map({ $0.differenceIdentifier })
|
||||
}
|
||||
|
||||
var allItemIDs: [NSManagedObjectID] {
|
||||
|
||||
return self.sections.lazy.flatMap({ $0.elements }).map({ $0.id })
|
||||
return self.sections.lazy.flatMap({ $0.elements }).map({ $0.differenceIdentifier })
|
||||
}
|
||||
|
||||
func items(in sectionID: String) -> [NSManagedObjectID] {
|
||||
@@ -273,12 +303,12 @@ extension Internals {
|
||||
|
||||
Internals.abort("Section \"\(sectionID)\" does not exist")
|
||||
}
|
||||
return self.sections[sectionIndex].elements.map({ $0.id })
|
||||
return self.sections[sectionIndex].elements.map({ $0.differenceIdentifier })
|
||||
}
|
||||
|
||||
func section(containing itemID: NSManagedObjectID) -> String? {
|
||||
|
||||
return self.itemPositionMap()[itemID]?.section.id
|
||||
return self.itemPositionMap()[itemID]?.section.differenceIdentifier
|
||||
}
|
||||
|
||||
mutating func append(itemIDs: [NSManagedObjectID], to sectionID: String?) {
|
||||
@@ -301,7 +331,7 @@ extension Internals {
|
||||
}
|
||||
index = section.index(before: section.endIndex)
|
||||
}
|
||||
let items = itemIDs.lazy.map({ Item(id: $0) })
|
||||
let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) })
|
||||
self.sections[index].elements.append(contentsOf: items)
|
||||
}
|
||||
|
||||
@@ -311,7 +341,7 @@ extension Internals {
|
||||
|
||||
Internals.abort("Item \(beforeItemID) does not exist")
|
||||
}
|
||||
let items = itemIDs.lazy.map({ Item(id: $0) })
|
||||
let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) })
|
||||
self.sections[itemPosition.sectionIndex].elements
|
||||
.insert(contentsOf: items, at: itemPosition.itemRelativeIndex)
|
||||
}
|
||||
@@ -324,7 +354,7 @@ extension Internals {
|
||||
}
|
||||
let itemIndex = self.sections[itemPosition.sectionIndex].elements
|
||||
.index(after: itemPosition.itemRelativeIndex)
|
||||
let items = itemIDs.lazy.map({ Item(id: $0) })
|
||||
let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) })
|
||||
self.sections[itemPosition.sectionIndex].elements
|
||||
.insert(contentsOf: items, at: itemIndex)
|
||||
}
|
||||
@@ -406,7 +436,7 @@ extension Internals {
|
||||
|
||||
mutating func append(sectionIDs: [String]) {
|
||||
|
||||
let newSections = sectionIDs.lazy.map({ Section(id: $0) })
|
||||
let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) })
|
||||
self.sections.append(contentsOf: newSections)
|
||||
}
|
||||
|
||||
@@ -416,7 +446,7 @@ extension Internals {
|
||||
|
||||
Internals.abort("Section \"\(beforeSectionID)\" does not exist")
|
||||
}
|
||||
let newSections = sectionIDs.lazy.map({ Section(id: $0) })
|
||||
let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) })
|
||||
self.sections.insert(contentsOf: newSections, at: sectionIndex)
|
||||
}
|
||||
|
||||
@@ -427,7 +457,7 @@ extension Internals {
|
||||
Internals.abort("Section \"\(afterSectionID)\" does not exist")
|
||||
}
|
||||
let sectionIndex = self.sections.index(after: beforeIndex)
|
||||
let newSections = sectionIDs.lazy.map({ Section(id: $0) })
|
||||
let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) })
|
||||
self.sections.insert(contentsOf: newSections, at: sectionIndex)
|
||||
}
|
||||
|
||||
@@ -481,11 +511,9 @@ extension Internals {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private static let zeroUUID: UUID = .init(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
|
||||
|
||||
private func sectionIndex(of sectionID: String) -> Array<Section>.Index? {
|
||||
|
||||
return self.sections.firstIndex(where: { $0.id == sectionID })
|
||||
return self.sections.firstIndex(where: { $0.differenceIdentifier == sectionID })
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -515,7 +543,7 @@ extension Internals {
|
||||
|
||||
for (itemRelativeIndex, item) in section.element.elements.enumerated() {
|
||||
|
||||
result[item.id] = ItemPosition(
|
||||
result[item.differenceIdentifier] = ItemPosition(
|
||||
item: item,
|
||||
itemRelativeIndex: itemRelativeIndex,
|
||||
section: section.element,
|
||||
@@ -526,58 +554,6 @@ extension Internals {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Item
|
||||
|
||||
fileprivate struct Item: Identifiable, Equatable {
|
||||
|
||||
var isReloaded: Bool
|
||||
|
||||
init(id: NSManagedObjectID, isReloaded: Bool = false) {
|
||||
|
||||
self.id = id
|
||||
self.isReloaded = isReloaded
|
||||
}
|
||||
|
||||
func isContentEqual(to source: Item) -> Bool {
|
||||
|
||||
return !self.isReloaded && self.id == source.id
|
||||
}
|
||||
|
||||
// MARK: Identifiable
|
||||
|
||||
let id: NSManagedObjectID
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Section
|
||||
|
||||
fileprivate struct Section: Identifiable, Equatable {
|
||||
|
||||
var elements: [Item] = []
|
||||
var isReloaded: Bool
|
||||
|
||||
init(id: String, items: [Item] = [], isReloaded: Bool = false) {
|
||||
self.id = id
|
||||
self.elements = items
|
||||
self.isReloaded = isReloaded
|
||||
}
|
||||
|
||||
init<S: Sequence>(source: Section, elements: S) where S.Element == Item {
|
||||
|
||||
self.init(id: source.id, items: Array(elements), isReloaded: source.isReloaded)
|
||||
}
|
||||
|
||||
func isContentEqual(to source: Section) -> Bool {
|
||||
|
||||
return !self.isReloaded && self.id == source.id
|
||||
}
|
||||
|
||||
// MARK: Identifiable
|
||||
|
||||
let id: String
|
||||
}
|
||||
|
||||
|
||||
// MARK: - ItemPosition
|
||||
|
||||
fileprivate struct ItemPosition {
|
||||
@@ -595,73 +571,7 @@ extension Internals {
|
||||
// MARK: - NSDiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol
|
||||
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
|
||||
extension NSDiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol where SectionIdentifierType == NSString, ItemIdentifierType == NSManagedObjectID {
|
||||
|
||||
internal var sectionIdentifiers: [String] {
|
||||
|
||||
return self.sectionIdentifiers as [NSString] as [String]
|
||||
}
|
||||
|
||||
internal func numberOfItems(inSection identifier: String) -> Int {
|
||||
|
||||
return self.numberOfItems(inSection: identifier as NSString)
|
||||
}
|
||||
|
||||
internal func itemIdentifiers(inSection identifier: String) -> [NSManagedObjectID] {
|
||||
|
||||
return self.itemIdentifiers(inSection: identifier as NSString)
|
||||
}
|
||||
|
||||
internal func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> String? {
|
||||
|
||||
return self.sectionIdentifier(containingItem: identifier) as NSString? as String?
|
||||
}
|
||||
|
||||
internal func indexOfSection(_ identifier: String) -> Int? {
|
||||
|
||||
return self.indexOfSection(identifier as NSString)
|
||||
}
|
||||
|
||||
internal mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: String?) {
|
||||
|
||||
self.appendItems(identifiers, toSection: sectionIdentifier as NSString?)
|
||||
}
|
||||
|
||||
internal mutating func appendSections(_ identifiers: [String]) {
|
||||
|
||||
self.appendSections(identifiers as [NSString])
|
||||
}
|
||||
|
||||
internal mutating func insertSections(_ identifiers: [String], beforeSection toIdentifier: String) {
|
||||
|
||||
self.insertSections(identifiers as [NSString], beforeSection: toIdentifier as NSString)
|
||||
}
|
||||
|
||||
internal mutating func insertSections(_ identifiers: [String], afterSection toIdentifier: String) {
|
||||
|
||||
return self.insertSections(identifiers as [NSString], afterSection: toIdentifier as NSString)
|
||||
}
|
||||
|
||||
internal mutating func deleteSections(_ identifiers: [String]) {
|
||||
|
||||
self.deleteSections(identifiers as [NSString])
|
||||
}
|
||||
|
||||
internal mutating func moveSection(_ identifier: String, beforeSection toIdentifier: String) {
|
||||
|
||||
self.moveSection(identifier as NSString, beforeSection: toIdentifier as NSString)
|
||||
}
|
||||
|
||||
internal mutating func moveSection(_ identifier: String, afterSection toIdentifier: String) {
|
||||
|
||||
self.moveSection(identifier as NSString, afterSection: toIdentifier as NSString)
|
||||
}
|
||||
|
||||
internal mutating func reloadSections(_ identifiers: [String]) {
|
||||
|
||||
self.reloadSections(identifiers as [NSString])
|
||||
}
|
||||
}
|
||||
extension NSDiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol where SectionIdentifierType == String, ItemIdentifierType == NSManagedObjectID {}
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
141
Sources/Internals.DiffableDataUIDispatcher.Changeset.swift
Normal file
141
Sources/Internals.DiffableDataUIDispatcher.Changeset.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// Internals.DiffableDataUIDispatcher.Changeset.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Internals.DiffableDataUIDispatcher
|
||||
|
||||
extension Internals.DiffableDataUIDispatcher {
|
||||
|
||||
// MARK: - ChangeSet
|
||||
|
||||
internal struct Changeset<C: Collection>: Equatable where C: Equatable {
|
||||
|
||||
var data: C
|
||||
var sectionDeleted: [Int]
|
||||
var sectionInserted: [Int]
|
||||
var sectionUpdated: [Int]
|
||||
var sectionMoved: [(source: Int, target: Int)]
|
||||
|
||||
var elementDeleted: [ElementPath]
|
||||
var elementInserted: [ElementPath]
|
||||
var elementUpdated: [ElementPath]
|
||||
var elementMoved: [(source: ElementPath, target: ElementPath)]
|
||||
|
||||
@inlinable
|
||||
init(
|
||||
data: C,
|
||||
sectionDeleted: [Int] = [],
|
||||
sectionInserted: [Int] = [],
|
||||
sectionUpdated: [Int] = [],
|
||||
sectionMoved: [(source: Int, target: Int)] = [],
|
||||
elementDeleted: [ElementPath] = [],
|
||||
elementInserted: [ElementPath] = [],
|
||||
elementUpdated: [ElementPath] = [],
|
||||
elementMoved: [(source: ElementPath, target: ElementPath)] = []
|
||||
) {
|
||||
self.data = data
|
||||
self.sectionDeleted = sectionDeleted
|
||||
self.sectionInserted = sectionInserted
|
||||
self.sectionUpdated = sectionUpdated
|
||||
self.sectionMoved = sectionMoved
|
||||
self.elementDeleted = elementDeleted
|
||||
self.elementInserted = elementInserted
|
||||
self.elementUpdated = elementUpdated
|
||||
self.elementMoved = elementMoved
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var sectionChangeCount: Int {
|
||||
|
||||
return self.sectionDeleted.count
|
||||
+ self.sectionInserted.count
|
||||
+ self.sectionUpdated.count
|
||||
+ self.sectionMoved.count
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var elementChangeCount: Int {
|
||||
|
||||
return self.elementDeleted.count
|
||||
+ self.elementInserted.count
|
||||
+ self.elementUpdated.count
|
||||
+ self.elementMoved.count
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var changeCount: Int {
|
||||
|
||||
return self.sectionChangeCount + self.elementChangeCount
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var hasSectionChanges: Bool {
|
||||
|
||||
return self.sectionChangeCount > 0
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var hasElementChanges: Bool {
|
||||
|
||||
return self.elementChangeCount > 0
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var hasChanges: Bool {
|
||||
|
||||
return self.changeCount > 0
|
||||
}
|
||||
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
static func == (lhs: Changeset, rhs: Changeset) -> Bool {
|
||||
return lhs.data == rhs.data
|
||||
&& Set(lhs.sectionDeleted) == Set(rhs.sectionDeleted)
|
||||
&& Set(lhs.sectionInserted) == Set(rhs.sectionInserted)
|
||||
&& Set(lhs.sectionUpdated) == Set(rhs.sectionUpdated)
|
||||
&& Set(lhs.sectionMoved.map(HashablePair.init)) == Set(rhs.sectionMoved.map(HashablePair.init))
|
||||
&& Set(lhs.elementDeleted) == Set(rhs.elementDeleted)
|
||||
&& Set(lhs.elementInserted) == Set(rhs.elementInserted)
|
||||
&& Set(lhs.elementUpdated) == Set(rhs.elementUpdated)
|
||||
&& Set(lhs.elementMoved.map(HashablePair.init)) == Set(rhs.elementMoved.map(HashablePair.init))
|
||||
}
|
||||
|
||||
|
||||
// MARK: - HashablePair
|
||||
|
||||
private struct HashablePair<H: Hashable>: Hashable {
|
||||
|
||||
let first: H
|
||||
let second: H
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
320
Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift
Normal file
320
Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift
Normal file
@@ -0,0 +1,320 @@
|
||||
//
|
||||
// Internals.DiffableDataUIDispatcher.DiffResult.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Internals.DiffableDataUIDispatcher
|
||||
|
||||
extension Internals.DiffableDataUIDispatcher {
|
||||
|
||||
// MARK: - DiffResult
|
||||
|
||||
@usableFromInline
|
||||
internal struct DiffResult<Index> {
|
||||
|
||||
@usableFromInline
|
||||
internal let deleted: [Index]
|
||||
@usableFromInline
|
||||
internal let inserted: [Index]
|
||||
@usableFromInline
|
||||
internal let updated: [Index]
|
||||
@usableFromInline
|
||||
internal let moved: [(source: Index, target: Index)]
|
||||
@usableFromInline
|
||||
internal let sourceTraces: ContiguousArray<Trace<Int>>
|
||||
@usableFromInline
|
||||
internal let targetReferences: ContiguousArray<Int?>
|
||||
|
||||
@inlinable
|
||||
@discardableResult
|
||||
static func diff<E: Differentiable>(
|
||||
source: ContiguousArray<E>,
|
||||
target: ContiguousArray<E>,
|
||||
useTargetIndexForUpdated: Bool,
|
||||
mapIndex: (Int) -> Index,
|
||||
updatedElementsPointer: UnsafeMutablePointer<ContiguousArray<E>>? = nil,
|
||||
notDeletedElementsPointer: UnsafeMutablePointer<ContiguousArray<E>>? = nil
|
||||
) -> DiffResult<Index> {
|
||||
|
||||
var deleted = [Index]()
|
||||
var inserted = [Index]()
|
||||
var updated = [Index]()
|
||||
var moved = [(source: Index, target: Index)]()
|
||||
|
||||
var sourceTraces = ContiguousArray<Trace<Int>>()
|
||||
var sourceIdentifiers = ContiguousArray<E.DifferenceIdentifier>()
|
||||
var targetReferences = ContiguousArray<Int?>(repeating: nil, count: target.count)
|
||||
|
||||
sourceTraces.reserveCapacity(source.count)
|
||||
sourceIdentifiers.reserveCapacity(source.count)
|
||||
|
||||
for sourceElement in source {
|
||||
|
||||
sourceTraces.append(Trace())
|
||||
sourceIdentifiers.append(sourceElement.differenceIdentifier)
|
||||
}
|
||||
sourceIdentifiers.withUnsafeBufferPointer { bufferPointer in
|
||||
|
||||
var sourceOccurrencesTable = [TableKey<E.DifferenceIdentifier>: Occurrence](minimumCapacity: source.count)
|
||||
|
||||
for sourceIndex in sourceIdentifiers.indices {
|
||||
|
||||
let pointer = bufferPointer.baseAddress!.advanced(by: sourceIndex)
|
||||
let key = TableKey(pointer: pointer)
|
||||
|
||||
switch sourceOccurrencesTable[key] {
|
||||
case .none:
|
||||
sourceOccurrencesTable[key] = .unique(index: sourceIndex)
|
||||
|
||||
case .unique(let otherIndex)?:
|
||||
let reference = IndicesReference([otherIndex, sourceIndex])
|
||||
sourceOccurrencesTable[key] = .duplicate(reference: reference)
|
||||
|
||||
case .duplicate(let reference)?:
|
||||
reference.push(sourceIndex)
|
||||
}
|
||||
}
|
||||
for targetIndex in target.indices {
|
||||
|
||||
var targetIdentifier = target[targetIndex].differenceIdentifier
|
||||
let key = TableKey(pointer: &targetIdentifier)
|
||||
|
||||
switch sourceOccurrencesTable[key] {
|
||||
|
||||
case .none:
|
||||
break
|
||||
|
||||
case .unique(let sourceIndex)?:
|
||||
if case .none = sourceTraces[sourceIndex].reference {
|
||||
|
||||
targetReferences[targetIndex] = sourceIndex
|
||||
sourceTraces[sourceIndex].reference = targetIndex
|
||||
}
|
||||
|
||||
case .duplicate(let reference)?:
|
||||
if let sourceIndex = reference.next() {
|
||||
|
||||
targetReferences[targetIndex] = sourceIndex
|
||||
sourceTraces[sourceIndex].reference = targetIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offsetByDelete = 0
|
||||
var untrackedSourceIndex: Int? = 0
|
||||
|
||||
for sourceIndex in source.indices {
|
||||
|
||||
sourceTraces[sourceIndex].deleteOffset = offsetByDelete
|
||||
|
||||
if let targetIndex = sourceTraces[sourceIndex].reference {
|
||||
|
||||
let targetElement = target[targetIndex]
|
||||
updatedElementsPointer?.pointee.append(targetElement)
|
||||
notDeletedElementsPointer?.pointee.append(targetElement)
|
||||
}
|
||||
else {
|
||||
|
||||
let sourceElement = source[sourceIndex]
|
||||
deleted.append(mapIndex(sourceIndex))
|
||||
sourceTraces[sourceIndex].isTracked = true
|
||||
offsetByDelete += 1
|
||||
updatedElementsPointer?.pointee.append(sourceElement)
|
||||
}
|
||||
}
|
||||
for targetIndex in target.indices {
|
||||
|
||||
untrackedSourceIndex = untrackedSourceIndex.flatMap { index in
|
||||
|
||||
sourceTraces.suffix(from: index).firstIndex { !$0.isTracked }
|
||||
}
|
||||
if let sourceIndex = targetReferences[targetIndex] {
|
||||
|
||||
sourceTraces[sourceIndex].isTracked = true
|
||||
|
||||
let sourceElement = source[sourceIndex]
|
||||
let targetElement = target[targetIndex]
|
||||
|
||||
if !targetElement.isContentEqual(to: sourceElement) {
|
||||
|
||||
updated.append(mapIndex(useTargetIndexForUpdated ? targetIndex : sourceIndex))
|
||||
}
|
||||
if sourceIndex != untrackedSourceIndex {
|
||||
|
||||
let deleteOffset = sourceTraces[sourceIndex].deleteOffset
|
||||
moved.append((source: mapIndex(sourceIndex - deleteOffset), target: mapIndex(targetIndex)))
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
inserted.append(mapIndex(targetIndex))
|
||||
}
|
||||
}
|
||||
return DiffResult(
|
||||
deleted: deleted,
|
||||
inserted: inserted,
|
||||
updated: updated,
|
||||
moved: moved,
|
||||
sourceTraces: sourceTraces,
|
||||
targetReferences: targetReferences
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@inlinable
|
||||
internal init(
|
||||
deleted: [Index] = [],
|
||||
inserted: [Index] = [],
|
||||
updated: [Index] = [],
|
||||
moved: [(source: Index, target: Index)] = [],
|
||||
sourceTraces: ContiguousArray<Trace<Int>>,
|
||||
targetReferences: ContiguousArray<Int?>
|
||||
) {
|
||||
|
||||
self.deleted = deleted
|
||||
self.inserted = inserted
|
||||
self.updated = updated
|
||||
self.moved = moved
|
||||
self.sourceTraces = sourceTraces
|
||||
self.targetReferences = targetReferences
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Trace
|
||||
|
||||
@usableFromInline
|
||||
internal struct Trace<Index> {
|
||||
|
||||
@usableFromInline
|
||||
internal var reference: Index?
|
||||
|
||||
@usableFromInline
|
||||
internal var deleteOffset = 0
|
||||
|
||||
@usableFromInline
|
||||
internal var isTracked = false
|
||||
|
||||
@inlinable
|
||||
init() {}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Occurrence
|
||||
|
||||
@usableFromInline
|
||||
internal enum Occurrence {
|
||||
|
||||
case unique(index: Int)
|
||||
case duplicate(reference: IndicesReference)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - IndicesReference
|
||||
|
||||
@usableFromInline
|
||||
internal final class IndicesReference {
|
||||
|
||||
@usableFromInline
|
||||
internal var indices: ContiguousArray<Int>
|
||||
|
||||
@usableFromInline
|
||||
internal var position = 0
|
||||
|
||||
@inlinable
|
||||
internal init(_ indices: ContiguousArray<Int>) {
|
||||
|
||||
self.indices = indices
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func push(_ index: Int) {
|
||||
|
||||
self.indices.append(index)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal func next() -> Int? {
|
||||
|
||||
guard self.position < self.indices.endIndex else {
|
||||
|
||||
return nil
|
||||
}
|
||||
defer {
|
||||
|
||||
self.position += 1
|
||||
}
|
||||
return self.indices[self.position]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - TableKey
|
||||
|
||||
@usableFromInline
|
||||
internal struct TableKey<T: Hashable>: Hashable {
|
||||
|
||||
@usableFromInline
|
||||
internal let pointeeHashValue: Int
|
||||
|
||||
@usableFromInline
|
||||
internal let pointer: UnsafePointer<T>
|
||||
|
||||
@inlinable
|
||||
internal init(pointer: UnsafePointer<T>) {
|
||||
|
||||
self.pointeeHashValue = pointer.pointee.hashValue
|
||||
self.pointer = pointer
|
||||
}
|
||||
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
@inlinable
|
||||
internal static func == (lhs: TableKey, rhs: TableKey) -> Bool {
|
||||
|
||||
return lhs.pointeeHashValue == rhs.pointeeHashValue
|
||||
&& (lhs.pointer.distance(to: rhs.pointer) == 0
|
||||
|| lhs.pointer.pointee == rhs.pointer.pointee)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Hashable
|
||||
|
||||
@inlinable
|
||||
internal func hash(into hasher: inout Hasher) {
|
||||
|
||||
hasher.combine(pointeeHashValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
516
Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift
Normal file
516
Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift
Normal file
@@ -0,0 +1,516 @@
|
||||
//
|
||||
// Internals.DiffableDataUIDispatcher.StagedChangeset.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
// MARK: - Internals.DiffableDataUIDispatcher
|
||||
|
||||
extension Internals.DiffableDataUIDispatcher {
|
||||
|
||||
// MARK: - StagedChangeset
|
||||
|
||||
internal struct StagedChangeset<C: Collection>: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection where C: Equatable {
|
||||
|
||||
@usableFromInline
|
||||
var changesets: ContiguousArray<Changeset<C>>
|
||||
|
||||
@inlinable
|
||||
init<S: Sequence>(_ changesets: S) where S.Element == Changeset<C> {
|
||||
|
||||
self.changesets = ContiguousArray(changesets)
|
||||
}
|
||||
|
||||
|
||||
// MARK: ExpressibleByArrayLiteral
|
||||
|
||||
@inlinable
|
||||
init(arrayLiteral elements: Changeset<C>...) {
|
||||
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
@inlinable
|
||||
static func == (lhs: StagedChangeset, rhs: StagedChangeset) -> Bool {
|
||||
|
||||
return lhs.changesets == rhs.changesets
|
||||
}
|
||||
|
||||
|
||||
// MARK: Sequence
|
||||
|
||||
typealias Element = Changeset<C>
|
||||
|
||||
|
||||
// MARK: RandomAccessCollection
|
||||
|
||||
@inlinable
|
||||
var startIndex: Int {
|
||||
|
||||
return self.changesets.startIndex
|
||||
}
|
||||
|
||||
@inlinable
|
||||
var endIndex: Int {
|
||||
|
||||
return self.changesets.endIndex
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func index(after i: Int) -> Int {
|
||||
|
||||
return self.changesets.index(after: i)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
subscript(position: Int) -> Changeset<C> {
|
||||
|
||||
get { return self.changesets[position] }
|
||||
set { self.changesets[position] = newValue }
|
||||
}
|
||||
|
||||
|
||||
// MARK: RangeReplaceableCollection
|
||||
|
||||
@inlinable
|
||||
init() {
|
||||
|
||||
self.init([])
|
||||
}
|
||||
|
||||
@inlinable
|
||||
mutating func replaceSubrange<C2: Collection, R: RangeExpression>(_ subrange: R, with newElements: C2) where C2.Element == Changeset<C>, R.Bound == Int {
|
||||
|
||||
self.changesets.replaceSubrange(subrange, with: newElements)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable
|
||||
|
||||
extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable {
|
||||
|
||||
@inlinable
|
||||
internal init(source: C, target: C) {
|
||||
|
||||
self.init(source: source, target: target, section: 0)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
internal init(source: C, target: C, section: Int) {
|
||||
|
||||
typealias Changeset = Internals.DiffableDataUIDispatcher<O>.Changeset
|
||||
typealias ElementPath = Internals.DiffableDataUIDispatcher<O>.ElementPath
|
||||
typealias DiffResult = Internals.DiffableDataUIDispatcher<O>.DiffResult
|
||||
|
||||
let sourceElements = ContiguousArray(source)
|
||||
let targetElements = ContiguousArray(target)
|
||||
if sourceElements.isEmpty && targetElements.isEmpty {
|
||||
|
||||
self.init()
|
||||
return
|
||||
}
|
||||
if !sourceElements.isEmpty && targetElements.isEmpty {
|
||||
|
||||
self.init(
|
||||
[
|
||||
Changeset(
|
||||
data: target,
|
||||
elementDeleted: sourceElements.indices.map {
|
||||
ElementPath(
|
||||
element: $0,
|
||||
section: section
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
if sourceElements.isEmpty && !targetElements.isEmpty {
|
||||
|
||||
self.init(
|
||||
[
|
||||
Changeset(
|
||||
data: target,
|
||||
elementInserted: targetElements.indices.map {
|
||||
ElementPath(
|
||||
element: $0,
|
||||
section: section
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
var firstStageElements = ContiguousArray<C.Element>()
|
||||
var secondStageElements = ContiguousArray<C.Element>()
|
||||
let result = DiffResult.diff(
|
||||
source: sourceElements,
|
||||
target: targetElements,
|
||||
useTargetIndexForUpdated: false,
|
||||
mapIndex: { ElementPath(element: $0, section: section) },
|
||||
updatedElementsPointer: &firstStageElements,
|
||||
notDeletedElementsPointer: &secondStageElements
|
||||
)
|
||||
|
||||
var changesets = ContiguousArray<Changeset<C>>()
|
||||
if !result.updated.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(firstStageElements),
|
||||
elementUpdated: result.updated
|
||||
)
|
||||
)
|
||||
}
|
||||
if !result.deleted.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(secondStageElements),
|
||||
elementDeleted: result.deleted
|
||||
)
|
||||
)
|
||||
}
|
||||
if !result.inserted.isEmpty || !result.moved.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: target,
|
||||
elementInserted: result.inserted,
|
||||
elementMoved: result.moved
|
||||
)
|
||||
)
|
||||
}
|
||||
if !changesets.isEmpty {
|
||||
|
||||
let index = changesets.index(before: changesets.endIndex)
|
||||
changesets[index].data = target
|
||||
}
|
||||
self.init(changesets)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection
|
||||
|
||||
extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection {
|
||||
|
||||
@inlinable
|
||||
internal init(source: C, target: C) {
|
||||
|
||||
typealias Section = C.Element
|
||||
typealias SectionIdentifier = C.Element.DifferenceIdentifier
|
||||
typealias Element = C.Element.Collection.Element
|
||||
typealias ElementIdentifier = C.Element.Collection.Element.DifferenceIdentifier
|
||||
|
||||
typealias Changeset = Internals.DiffableDataUIDispatcher<O>.Changeset
|
||||
typealias ElementPath = Internals.DiffableDataUIDispatcher<O>.ElementPath
|
||||
typealias DiffResult = Internals.DiffableDataUIDispatcher<O>.DiffResult
|
||||
|
||||
typealias Trace = Internals.DiffableDataUIDispatcher<O>.DiffResult<Section>.Trace
|
||||
typealias TableKey = Internals.DiffableDataUIDispatcher<O>.DiffResult<Section>.TableKey
|
||||
typealias Occurrence = Internals.DiffableDataUIDispatcher<O>.DiffResult<Section>.Occurrence
|
||||
typealias IndicesReference = Internals.DiffableDataUIDispatcher<O>.DiffResult<Section>.IndicesReference
|
||||
|
||||
let sourceSections = ContiguousArray(source)
|
||||
let targetSections = ContiguousArray(target)
|
||||
|
||||
let contiguousSourceSections = ContiguousArray(sourceSections.map { ContiguousArray($0.elements) })
|
||||
let contiguousTargetSections = ContiguousArray(targetSections.map { ContiguousArray($0.elements) })
|
||||
|
||||
var firstStageSections = sourceSections
|
||||
var secondStageSections = ContiguousArray<Section>()
|
||||
var thirdStageSections = ContiguousArray<Section>()
|
||||
var fourthStageSections = ContiguousArray<Section>()
|
||||
|
||||
var sourceElementTraces = contiguousSourceSections.map { section in
|
||||
|
||||
ContiguousArray(repeating: Trace<ElementPath>(), count: section.count)
|
||||
}
|
||||
var targetElementReferences = contiguousTargetSections.map { section in
|
||||
|
||||
ContiguousArray<ElementPath?>(repeating: nil, count: section.count)
|
||||
}
|
||||
|
||||
let flattenSourceCount = contiguousSourceSections.reduce(into: 0) { $0 += $1.count }
|
||||
var flattenSourceIdentifiers = ContiguousArray<ElementIdentifier>()
|
||||
var flattenSourceElementPaths = ContiguousArray<ElementPath>()
|
||||
|
||||
thirdStageSections.reserveCapacity(contiguousTargetSections.count)
|
||||
fourthStageSections.reserveCapacity(contiguousTargetSections.count)
|
||||
|
||||
flattenSourceIdentifiers.reserveCapacity(flattenSourceCount)
|
||||
flattenSourceElementPaths.reserveCapacity(flattenSourceCount)
|
||||
|
||||
let sectionResult = DiffResult.diff(
|
||||
source: sourceSections,
|
||||
target: targetSections,
|
||||
useTargetIndexForUpdated: true,
|
||||
mapIndex: { $0 }
|
||||
)
|
||||
|
||||
var elementDeleted = [ElementPath]()
|
||||
var elementInserted = [ElementPath]()
|
||||
var elementUpdated = [ElementPath]()
|
||||
var elementMoved = [(source: ElementPath, target: ElementPath)]()
|
||||
|
||||
for sourceSectionIndex in contiguousSourceSections.indices {
|
||||
|
||||
for sourceElementIndex in contiguousSourceSections[sourceSectionIndex].indices {
|
||||
|
||||
let sourceElementPath = ElementPath(element: sourceElementIndex, section: sourceSectionIndex)
|
||||
let sourceElement = contiguousSourceSections[sourceElementPath]
|
||||
flattenSourceIdentifiers.append(sourceElement.differenceIdentifier)
|
||||
flattenSourceElementPaths.append(sourceElementPath)
|
||||
}
|
||||
}
|
||||
flattenSourceIdentifiers.withUnsafeBufferPointer { bufferPointer in
|
||||
|
||||
var sourceOccurrencesTable = [TableKey<ElementIdentifier>: Occurrence](minimumCapacity: flattenSourceCount)
|
||||
|
||||
for flattenSourceIndex in flattenSourceIdentifiers.indices {
|
||||
let pointer = bufferPointer.baseAddress!.advanced(by: flattenSourceIndex)
|
||||
let key = TableKey(pointer: pointer)
|
||||
|
||||
switch sourceOccurrencesTable[key] {
|
||||
|
||||
case .none:
|
||||
sourceOccurrencesTable[key] = .unique(index: flattenSourceIndex)
|
||||
|
||||
case .unique(let otherIndex)?:
|
||||
let reference = IndicesReference([otherIndex, flattenSourceIndex])
|
||||
sourceOccurrencesTable[key] = .duplicate(reference: reference)
|
||||
|
||||
case .duplicate(let reference)?:
|
||||
reference.push(flattenSourceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
for targetSectionIndex in contiguousTargetSections.indices {
|
||||
|
||||
let targetElements = contiguousTargetSections[targetSectionIndex]
|
||||
for targetElementIndex in targetElements.indices {
|
||||
|
||||
var targetIdentifier = targetElements[targetElementIndex].differenceIdentifier
|
||||
let key = TableKey(pointer: &targetIdentifier)
|
||||
|
||||
switch sourceOccurrencesTable[key] {
|
||||
|
||||
case .none:
|
||||
break
|
||||
|
||||
case .unique(let flattenSourceIndex)?:
|
||||
let sourceElementPath = flattenSourceElementPaths[flattenSourceIndex]
|
||||
let targetElementPath = ElementPath(element: targetElementIndex, section: targetSectionIndex)
|
||||
if case .none = sourceElementTraces[sourceElementPath].reference {
|
||||
|
||||
targetElementReferences[targetElementPath] = sourceElementPath
|
||||
sourceElementTraces[sourceElementPath].reference = targetElementPath
|
||||
}
|
||||
|
||||
case .duplicate(let reference)?:
|
||||
if let flattenSourceIndex = reference.next() {
|
||||
|
||||
let sourceElementPath = flattenSourceElementPaths[flattenSourceIndex]
|
||||
let targetElementPath = ElementPath(element: targetElementIndex, section: targetSectionIndex)
|
||||
targetElementReferences[targetElementPath] = sourceElementPath
|
||||
sourceElementTraces[sourceElementPath].reference = targetElementPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for sourceSectionIndex in contiguousSourceSections.indices {
|
||||
|
||||
let sourceSection = sourceSections[sourceSectionIndex]
|
||||
let sourceElements = contiguousSourceSections[sourceSectionIndex]
|
||||
var firstStageElements = sourceElements
|
||||
|
||||
if case .some = sectionResult.sourceTraces[sourceSectionIndex].reference {
|
||||
|
||||
var offsetByDelete = 0
|
||||
var secondStageElements = ContiguousArray<Element>()
|
||||
for sourceElementIndex in sourceElements.indices {
|
||||
|
||||
let sourceElementPath = ElementPath(element: sourceElementIndex, section: sourceSectionIndex)
|
||||
sourceElementTraces[sourceElementPath].deleteOffset = offsetByDelete
|
||||
|
||||
if let targetElementPath = sourceElementTraces[sourceElementPath].reference,
|
||||
case .some = sectionResult.targetReferences[targetElementPath.section] {
|
||||
|
||||
let targetElement = contiguousTargetSections[targetElementPath]
|
||||
firstStageElements[sourceElementIndex] = targetElement
|
||||
secondStageElements.append(targetElement)
|
||||
continue
|
||||
}
|
||||
elementDeleted.append(sourceElementPath)
|
||||
sourceElementTraces[sourceElementPath].isTracked = true
|
||||
offsetByDelete += 1
|
||||
}
|
||||
|
||||
let secondStageSection = Section(source: sourceSection, elements: secondStageElements)
|
||||
secondStageSections.append(secondStageSection)
|
||||
}
|
||||
|
||||
let firstStageSection = Section(source: sourceSection, elements: firstStageElements)
|
||||
firstStageSections[sourceSectionIndex] = firstStageSection
|
||||
}
|
||||
for targetSectionIndex in contiguousTargetSections.indices {
|
||||
|
||||
guard let sourceSectionIndex = sectionResult.targetReferences[targetSectionIndex] else {
|
||||
|
||||
thirdStageSections.append(targetSections[targetSectionIndex])
|
||||
fourthStageSections.append(targetSections[targetSectionIndex])
|
||||
continue
|
||||
}
|
||||
|
||||
var untrackedSourceIndex: Int? = 0
|
||||
let targetElements = contiguousTargetSections[targetSectionIndex]
|
||||
let sectionDeleteOffset = sectionResult.sourceTraces[sourceSectionIndex].deleteOffset
|
||||
let thirdStageSection = secondStageSections[sourceSectionIndex - sectionDeleteOffset]
|
||||
thirdStageSections.append(thirdStageSection)
|
||||
|
||||
var fourthStageElements = ContiguousArray<Element>()
|
||||
fourthStageElements.reserveCapacity(targetElements.count)
|
||||
|
||||
for targetElementIndex in targetElements.indices {
|
||||
|
||||
untrackedSourceIndex = untrackedSourceIndex.flatMap { index in
|
||||
|
||||
sourceElementTraces[sourceSectionIndex].suffix(from: index).firstIndex { !$0.isTracked }
|
||||
}
|
||||
let targetElementPath = ElementPath(element: targetElementIndex, section: targetSectionIndex)
|
||||
let targetElement = contiguousTargetSections[targetElementPath]
|
||||
|
||||
guard
|
||||
let sourceElementPath = targetElementReferences[targetElementPath],
|
||||
let movedSourceSectionIndex = sectionResult.sourceTraces[sourceElementPath.section].reference
|
||||
else {
|
||||
|
||||
fourthStageElements.append(targetElement)
|
||||
elementInserted.append(targetElementPath)
|
||||
continue
|
||||
}
|
||||
sourceElementTraces[sourceElementPath].isTracked = true
|
||||
|
||||
let sourceElement = contiguousSourceSections[sourceElementPath]
|
||||
fourthStageElements.append(targetElement)
|
||||
|
||||
if !targetElement.isContentEqual(to: sourceElement) {
|
||||
|
||||
elementUpdated.append(sourceElementPath)
|
||||
}
|
||||
if sourceElementPath.section != sourceSectionIndex || sourceElementPath.element != untrackedSourceIndex {
|
||||
|
||||
let deleteOffset = sourceElementTraces[sourceElementPath].deleteOffset
|
||||
let moveSourceElementPath = ElementPath(element: sourceElementPath.element - deleteOffset, section: movedSourceSectionIndex)
|
||||
elementMoved.append((source: moveSourceElementPath, target: targetElementPath))
|
||||
}
|
||||
}
|
||||
let fourthStageSection = Section(source: thirdStageSection, elements: fourthStageElements)
|
||||
fourthStageSections.append(fourthStageSection)
|
||||
}
|
||||
|
||||
var changesets = ContiguousArray<Changeset<C>>()
|
||||
if !elementUpdated.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(firstStageSections),
|
||||
elementUpdated: elementUpdated
|
||||
)
|
||||
)
|
||||
}
|
||||
if !sectionResult.deleted.isEmpty || !elementDeleted.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(secondStageSections),
|
||||
sectionDeleted: sectionResult.deleted,
|
||||
elementDeleted: elementDeleted
|
||||
)
|
||||
)
|
||||
}
|
||||
if !sectionResult.inserted.isEmpty || !sectionResult.moved.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(thirdStageSections),
|
||||
sectionInserted: sectionResult.inserted,
|
||||
sectionMoved: sectionResult.moved
|
||||
)
|
||||
)
|
||||
}
|
||||
if !elementInserted.isEmpty || !elementMoved.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: C(fourthStageSections),
|
||||
elementInserted: elementInserted,
|
||||
elementMoved: elementMoved
|
||||
)
|
||||
)
|
||||
}
|
||||
if !sectionResult.updated.isEmpty {
|
||||
|
||||
changesets.append(
|
||||
Changeset(
|
||||
data: target,
|
||||
sectionUpdated: sectionResult.updated
|
||||
)
|
||||
)
|
||||
}
|
||||
if !changesets.isEmpty {
|
||||
|
||||
let index = changesets.index(before: changesets.endIndex)
|
||||
changesets[index].data = target
|
||||
}
|
||||
self.init(changesets)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MutableCollection
|
||||
|
||||
extension MutableCollection where Element: MutableCollection, Index == Int, Element.Index == Int {
|
||||
|
||||
@inlinable
|
||||
internal subscript<O>(path: Internals.DiffableDataUIDispatcher<O>.ElementPath) -> Element.Element {
|
||||
|
||||
get { return self[path.section][path.element] }
|
||||
set { self[path.section][path.element] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
229
Sources/Internals.DiffableDataUIDispatcher.swift
Normal file
229
Sources/Internals.DiffableDataUIDispatcher.swift
Normal file
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// Internals.DiffableDataUIDispatcher.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
import CoreData
|
||||
|
||||
#if canImport(QuartzCore)
|
||||
import QuartzCore
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension Internals {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
// Implementation based on https://github.com/ra1028/DiffableDataSources
|
||||
@usableFromInline
|
||||
internal final class DiffableDataUIDispatcher<O: DynamicObject> {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
typealias ObjectType = O
|
||||
|
||||
init(dataStack: DataStack) {
|
||||
|
||||
self.dataStack = dataStack
|
||||
}
|
||||
|
||||
func apply<View: AnyObject>(_ snapshot: DiffableDataSourceSnapshot, view: View?, animatingDifferences: Bool, performUpdates: @escaping (View, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, @escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void) -> Void) {
|
||||
|
||||
self.dispatcher.dispatch { [weak self] in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.currentSnapshot = snapshot
|
||||
|
||||
let newSections = snapshot.sections
|
||||
guard let view = view else {
|
||||
|
||||
return self.sections = newSections
|
||||
}
|
||||
|
||||
let performDiffingUpdates: () -> Void = {
|
||||
|
||||
let changeset = StagedChangeset(source: self.sections, target: newSections)
|
||||
performUpdates(view, changeset) { sections in
|
||||
|
||||
self.sections = sections
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(QuartzCore)
|
||||
|
||||
if !animatingDifferences {
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
||||
performDiffingUpdates()
|
||||
|
||||
CATransaction.commit()
|
||||
return
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
performDiffingUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
func snapshot() -> DiffableDataSourceSnapshot {
|
||||
|
||||
var snapshot: DiffableDataSourceSnapshot = .init()
|
||||
snapshot.sections = self.currentSnapshot.sections
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
|
||||
|
||||
guard (0 ..< self.sections.endIndex) ~= indexPath.section else {
|
||||
|
||||
return nil
|
||||
}
|
||||
let items = self.sections[indexPath.section].elements
|
||||
guard (0 ..< items.endIndex) ~= indexPath.item else {
|
||||
|
||||
return nil
|
||||
}
|
||||
return items[indexPath.item].differenceIdentifier
|
||||
}
|
||||
|
||||
func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? {
|
||||
|
||||
let indexPathMap: [O.ObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in
|
||||
|
||||
for (itemIndex, item) in section.element.elements.enumerated() {
|
||||
|
||||
result[item.differenceIdentifier] = IndexPath(
|
||||
item: itemIndex,
|
||||
section: section.offset
|
||||
)
|
||||
}
|
||||
}
|
||||
return indexPathMap[itemIdentifier]
|
||||
}
|
||||
|
||||
func numberOfSections() -> Int {
|
||||
|
||||
return self.sections.count
|
||||
}
|
||||
|
||||
func numberOfItems(inSection section: Int) -> Int {
|
||||
|
||||
return self.sections[section].elements.count
|
||||
}
|
||||
|
||||
func sectionIdentifier(inSection section: Int) -> String {
|
||||
|
||||
return self.sections[section].differenceIdentifier
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let dispatcher: MainThreadSerialDispatcher = .init()
|
||||
private let dataStack: DataStack
|
||||
|
||||
private var currentSnapshot: Internals.DiffableDataSourceSnapshot = .init()
|
||||
private var sections: [Internals.DiffableDataSourceSnapshot.Section] = []
|
||||
|
||||
|
||||
// MARK: - ElementPath
|
||||
|
||||
@usableFromInline
|
||||
internal struct ElementPath: Hashable {
|
||||
|
||||
@usableFromInline
|
||||
var element: Int
|
||||
|
||||
@usableFromInline
|
||||
var section: Int
|
||||
|
||||
@inlinable
|
||||
init(element: Int, section: Int) {
|
||||
|
||||
self.element = element
|
||||
self.section = section
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MainThreadSerialDispatcher
|
||||
|
||||
fileprivate final class MainThreadSerialDispatcher {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
fileprivate init() {
|
||||
|
||||
self.executingCount.initialize(to: 0)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
self.executingCount.deinitialize(count: 1)
|
||||
self.executingCount.deallocate()
|
||||
}
|
||||
|
||||
fileprivate func dispatch(_ action: @escaping () -> Void) {
|
||||
|
||||
let count = OSAtomicIncrement32(self.executingCount)
|
||||
if Thread.isMainThread && count == 1 {
|
||||
|
||||
action()
|
||||
OSAtomicDecrement32(executingCount)
|
||||
}
|
||||
else {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
|
||||
guard let self = self else {
|
||||
|
||||
return
|
||||
}
|
||||
action()
|
||||
OSAtomicDecrement32(self.executingCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let executingCount: UnsafeMutablePointer<Int32> = .allocate(capacity: 1)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,157 +0,0 @@
|
||||
//
|
||||
// Internals.FallbackDiffableDataUIDispatcher.swift
|
||||
// CoreStore
|
||||
//
|
||||
// Copyright © 2018 John Rommel Estropia
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
import CoreData
|
||||
|
||||
#if canImport(QuartzCore)
|
||||
import QuartzCore
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
extension Internals {
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
// // Implementation based on https://github.com/ra1028/DiffableDataSources
|
||||
// internal final class FallbackDiffableDataUIDispatcher {
|
||||
//
|
||||
// // MARK: Internal
|
||||
//
|
||||
// internal func apply() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// func snapshot() -> DiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> {
|
||||
// var snapshot = DiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()
|
||||
// snapshot.structure.sections = currentSnapshot.structure.sections
|
||||
// return snapshot
|
||||
// }
|
||||
//
|
||||
// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? {
|
||||
// guard 0..<sections.endIndex ~= indexPath.section else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let items = sections[indexPath.section].elements
|
||||
//
|
||||
// guard 0..<items.endIndex ~= indexPath.item else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return items[indexPath.item].differenceIdentifier
|
||||
// }
|
||||
//
|
||||
// func unsafeItemIdentifier(for indexPath: IndexPath, file: StaticString = #file, line: UInt = #line) -> ItemIdentifierType {
|
||||
// guard let itemIdentifier = itemIdentifier(for: indexPath) else {
|
||||
// universalError("Item not found at the specified index path(\(indexPath)).")
|
||||
// }
|
||||
//
|
||||
// return itemIdentifier
|
||||
// }
|
||||
//
|
||||
// func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? {
|
||||
// let indexPathMap: [ItemIdentifierType: IndexPath] = sections.enumerated()
|
||||
// .reduce(into: [:]) { result, section in
|
||||
// for (itemIndex, item) in section.element.elements.enumerated() {
|
||||
// result[item.differenceIdentifier] = IndexPath(
|
||||
// item: itemIndex,
|
||||
// section: section.offset
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// return indexPathMap[itemIdentifier]
|
||||
// }
|
||||
//
|
||||
// func numberOfSections() -> Int {
|
||||
// return sections.count
|
||||
// }
|
||||
//
|
||||
// func numberOfItems(inSection section: Int) -> Int {
|
||||
// return sections[section].elements.count
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// // MARK: Private
|
||||
//
|
||||
// private let dispatcher: MainThreadSerialDispatcher = .init()
|
||||
//
|
||||
// private var currentSnapshot: Internals.DiffableDataSourceSnapshot = FallbackDiffableDataSourceSnapshot()
|
||||
// private var sections: [FallbackDiffableDataSourceSnapshot.BackingStructure.Section] = []
|
||||
//
|
||||
//
|
||||
// // MARK: - MainThreadSerialDispatcher
|
||||
//
|
||||
// fileprivate final class MainThreadSerialDispatcher {
|
||||
//
|
||||
// // MARK: FilePrivate
|
||||
//
|
||||
// fileprivate init() {
|
||||
//
|
||||
// self.executingCount.initialize(to: 0)
|
||||
// }
|
||||
//
|
||||
// deinit {
|
||||
//
|
||||
// self.executingCount.deinitialize(count: 1)
|
||||
// self.executingCount.deallocate()
|
||||
// }
|
||||
//
|
||||
// fileprivate func dispatch(_ action: @escaping () -> Void) {
|
||||
//
|
||||
// let count = OSAtomicIncrement32(self.executingCount)
|
||||
// if Thread.isMainThread && count == 1 {
|
||||
//
|
||||
// action()
|
||||
// OSAtomicDecrement32(executingCount)
|
||||
// }
|
||||
// else {
|
||||
//
|
||||
// DispatchQueue.main.async { [weak self] in
|
||||
//
|
||||
// guard let self = self else {
|
||||
//
|
||||
// return
|
||||
// }
|
||||
// action()
|
||||
// OSAtomicDecrement32(self.executingCount)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // MARK: Private
|
||||
//
|
||||
// private let executingCount: UnsafeMutablePointer<Int32> = .allocate(capacity: 1)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -41,7 +41,12 @@ import AppKit
|
||||
|
||||
internal protocol FetchedDiffableDataSourceSnapshotHandler: AnyObject {
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: DiffableDataSourceSnapshotProtocol)
|
||||
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
// func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>)
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot)
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String?
|
||||
}
|
||||
|
||||
|
||||
@@ -75,14 +80,14 @@ extension Internals {
|
||||
|
||||
internal func initialFetch() {
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
#endif
|
||||
// #if canImport(UIKit) || canImport(AppKit)
|
||||
//
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// #endif
|
||||
|
||||
guard let fetchedResultsController = self.fetchedResultsController else {
|
||||
|
||||
@@ -94,19 +99,19 @@ extension Internals {
|
||||
|
||||
// MARK: NSFetchedResultsControllerDelegate
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
|
||||
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
@objc
|
||||
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
|
||||
self.handler?.controller(
|
||||
controller,
|
||||
didChangeContentWith: snapshot as NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
// #if canImport(UIKit) || canImport(AppKit)
|
||||
//
|
||||
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
// @objc
|
||||
// dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
//
|
||||
// self.handler?.controller(
|
||||
// controller,
|
||||
// didChangeContentWith: snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// #endif
|
||||
|
||||
@objc
|
||||
dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
@@ -118,5 +123,14 @@ extension Internals {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@objc
|
||||
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String) -> String? {
|
||||
|
||||
return self.handler?.controller(
|
||||
controller,
|
||||
sectionIndexTitleForSectionName: sectionName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,18 +56,19 @@ extension Internals {
|
||||
deinit {
|
||||
|
||||
self.observer.map(NotificationCenter.default.removeObserver(_:))
|
||||
self.observers.removeAllObjects()
|
||||
}
|
||||
|
||||
internal func addObserver<U: AnyObject>(_ observer: U, closure: @escaping (T) -> Void) {
|
||||
|
||||
self.observers.setObject(Closure(closure), forKey: observer)
|
||||
self.observers.setObject(Closure<T, Void>(closure), forKey: observer)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var observer: NSObjectProtocol!
|
||||
private let observers: NSMapTable<AnyObject, Closure> = .weakToStrongObjects()
|
||||
private let observers: NSMapTable<AnyObject, Closure<T, Void>> = .weakToStrongObjects()
|
||||
|
||||
private func notifyObservers(_ sharedValue: T) {
|
||||
|
||||
@@ -77,31 +78,8 @@ extension Internals {
|
||||
}
|
||||
for closure in enumerator {
|
||||
|
||||
(closure as! Closure).invoke(with: sharedValue)
|
||||
(closure as! Closure<T, Void>).invoke(with: sharedValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Closure
|
||||
|
||||
fileprivate final class Closure {
|
||||
|
||||
// MARK: FilePrivate
|
||||
|
||||
fileprivate init(_ closure: @escaping (T) -> Void) {
|
||||
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
fileprivate func invoke(with argument: T) {
|
||||
|
||||
self.closure(argument)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let closure: (T) -> Void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import Foundation
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
@usableFromInline
|
||||
internal enum Internals {
|
||||
|
||||
// MARK: Associated Objects
|
||||
|
||||
@@ -1453,7 +1453,7 @@ extension ListMonitor: FetchedResultsControllerHandler {
|
||||
)
|
||||
}
|
||||
|
||||
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String? {
|
||||
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String? {
|
||||
|
||||
return self.sectionIndexTransformer(sectionName)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,24 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
|
||||
|
||||
public typealias SectionID = String
|
||||
public typealias ItemID = O.ObjectID
|
||||
|
||||
public init(byCloning snapshot: ListSnapshot<O>, for dataStack: DataStack) {
|
||||
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// self.init(
|
||||
// diffableSnapshot: snapshot.diffableSnapshot as! NSDiffableDataSourceSnapshot<String, NSManagedObjectID>,
|
||||
// context: dataStack.mainContext
|
||||
// )
|
||||
// }
|
||||
// else {
|
||||
|
||||
self.init(
|
||||
diffableSnapshot: snapshot.diffableSnapshot as! Internals.DiffableDataSourceSnapshot,
|
||||
context: dataStack.mainContext
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
public subscript<S: Sequence>(indices indices: S) -> [LiveObject<O>] where S.Element == Index {
|
||||
|
||||
@@ -252,13 +270,29 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
|
||||
|
||||
// MARK: Internal
|
||||
|
||||
internal private(set) var diffableSnapshot: DiffableDataSourceSnapshotProtocol
|
||||
|
||||
internal init() {
|
||||
|
||||
self.diffableSnapshot = Internals.DiffableDataSourceSnapshot()
|
||||
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
|
||||
//
|
||||
// self.diffableSnapshot = NSDiffableDataSourceSnapshot<String, NSManagedObjectID>()
|
||||
// }
|
||||
// else {
|
||||
|
||||
self.diffableSnapshot = Internals.DiffableDataSourceSnapshot()
|
||||
// }
|
||||
self.context = nil
|
||||
}
|
||||
|
||||
internal init(diffableSnapshot: DiffableDataSourceSnapshotProtocol, context: NSManagedObjectContext) {
|
||||
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
// internal init(diffableSnapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>, context: NSManagedObjectContext) {
|
||||
//
|
||||
// self.diffableSnapshot = diffableSnapshot
|
||||
// self.context = context
|
||||
// }
|
||||
|
||||
internal init(diffableSnapshot: Internals.DiffableDataSourceSnapshot, context: NSManagedObjectContext) {
|
||||
|
||||
self.diffableSnapshot = diffableSnapshot
|
||||
self.context = context
|
||||
@@ -270,5 +304,4 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
|
||||
private let id: UUID = .init()
|
||||
private let context: NSManagedObjectContext?
|
||||
|
||||
private var diffableSnapshot: DiffableDataSourceSnapshotProtocol
|
||||
}
|
||||
|
||||
@@ -45,29 +45,6 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
public typealias SectionID = SnapshotType.SectionID
|
||||
public typealias ItemID = SnapshotType.ItemID
|
||||
|
||||
public fileprivate(set) var snapshot: SnapshotType = .init() {
|
||||
|
||||
willSet {
|
||||
|
||||
self.willChange()
|
||||
}
|
||||
}
|
||||
|
||||
public var numberOfItems: Int {
|
||||
|
||||
return self.snapshot.numberOfItems
|
||||
}
|
||||
|
||||
public var numberOfSections: Int {
|
||||
|
||||
return self.snapshot.numberOfSections
|
||||
}
|
||||
|
||||
public var sectionIdentifiers: [SectionID] {
|
||||
|
||||
return self.snapshot.sectionIdentifiers
|
||||
}
|
||||
|
||||
public subscript(section sectionID: SectionID) -> [LiveObject<O>] {
|
||||
|
||||
let context = self.context
|
||||
@@ -85,6 +62,24 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
return self.context.liveObject(id: validID)
|
||||
}
|
||||
|
||||
public subscript(indexPath indexPath: IndexPath) -> LiveObject<O>? {
|
||||
|
||||
let snapshot = self.snapshot
|
||||
let sectionIdentifiers = snapshot.sectionIdentifiers
|
||||
guard sectionIdentifiers.indices.contains(indexPath.section) else {
|
||||
|
||||
return nil
|
||||
}
|
||||
let sectionID = sectionIdentifiers[indexPath.section]
|
||||
let itemIdentifiers = snapshot.itemIdentifiers(inSection: sectionID)
|
||||
guard itemIdentifiers.indices.contains(indexPath.item) else {
|
||||
|
||||
return nil
|
||||
}
|
||||
let itemID = itemIdentifiers[indexPath.item]
|
||||
return self.context.liveObject(id: itemID)
|
||||
}
|
||||
|
||||
public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject<O>] where S.Element == Int {
|
||||
|
||||
let context = self.context
|
||||
@@ -96,6 +91,34 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public fileprivate(set) var snapshot: SnapshotType = .init() {
|
||||
|
||||
willSet {
|
||||
|
||||
self.willChange()
|
||||
}
|
||||
didSet {
|
||||
|
||||
self.notifyObservers(self.snapshot)
|
||||
self.didChange()
|
||||
}
|
||||
}
|
||||
|
||||
public var numberOfItems: Int {
|
||||
|
||||
return self.snapshot.numberOfItems
|
||||
}
|
||||
|
||||
public var numberOfSections: Int {
|
||||
|
||||
return self.snapshot.numberOfSections
|
||||
}
|
||||
|
||||
public var sectionIdentifiers: [SectionID] {
|
||||
|
||||
return self.snapshot.sectionIdentifiers
|
||||
}
|
||||
|
||||
public var items: [LiveObject<O>] {
|
||||
|
||||
let context = self.context
|
||||
@@ -141,6 +164,26 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
|
||||
return self.snapshot.indexOfSection(identifier)
|
||||
}
|
||||
|
||||
public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (LiveList<O>, ListSnapshot<O>) -> Void) {
|
||||
|
||||
self.observers.setObject(
|
||||
Internals.Closure(callback),
|
||||
forKey: observer
|
||||
)
|
||||
|
||||
callback(self, self.snapshot)
|
||||
}
|
||||
|
||||
public func removeObserver<T: AnyObject>(_ observer: T) {
|
||||
|
||||
self.observers.removeObject(forKey: observer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
||||
self.observers.removeAllObjects()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Equatable
|
||||
@@ -222,12 +265,14 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
|
||||
private var fetchedResultsController: Internals.CoreStoreFetchedResultsController
|
||||
private var fetchedResultsControllerDelegate: Internals.FetchedDiffableDataSourceSnapshotDelegate
|
||||
private let sectionIndexTransformer: (_ sectionName: KeyPathString?) -> String?
|
||||
private var applyFetchClauses: (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void
|
||||
private var observerForWillChangePersistentStore: Internals.NotificationObserver!
|
||||
private var observerForDidChangePersistentStore: Internals.NotificationObserver!
|
||||
|
||||
private let from: From<ObjectType>
|
||||
private let sectionBy: SectionBy<ObjectType>?
|
||||
private let observers: NSMapTable<AnyObject, Internals.Closure<(LiveList<O>, ListSnapshot<O>), Void>> = .weakToStrongObjects()
|
||||
|
||||
private lazy var context: NSManagedObjectContext = self.fetchedResultsController.managedObjectContext
|
||||
|
||||
@@ -279,20 +324,31 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
self.rawObjectWillChange = nil
|
||||
}
|
||||
|
||||
if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer {
|
||||
|
||||
// if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer {
|
||||
//
|
||||
// self.sectionIndexTransformer = sectionIndexTransformer
|
||||
// }
|
||||
// else {
|
||||
//
|
||||
// self.sectionIndexTransformer = { $0 }
|
||||
// }
|
||||
self.sectionIndexTransformer = sectionIndexTransformer
|
||||
}
|
||||
else {
|
||||
|
||||
self.sectionIndexTransformer = { $0 }
|
||||
}
|
||||
self.applyFetchClauses = applyFetchClauses
|
||||
self.fetchedResultsControllerDelegate.handler = self
|
||||
|
||||
try! self.fetchedResultsController.performFetchFromSpecifiedStores()
|
||||
}
|
||||
|
||||
private func notifyObservers(_ snapshot: ListSnapshot<O>) {
|
||||
|
||||
guard let enumerator = self.observers.objectEnumerator() else {
|
||||
|
||||
return
|
||||
}
|
||||
for closure in enumerator {
|
||||
|
||||
(closure as! Internals.Closure<(LiveList<O>, ListSnapshot<O>), Void>).invoke(with: (self, snapshot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -301,14 +357,28 @@ public final class LiveList<O: DynamicObject>: Hashable {
|
||||
extension LiveList: FetchedDiffableDataSourceSnapshotHandler {
|
||||
|
||||
// MARK: FetchedDiffableDataSourceSnapshotHandler
|
||||
|
||||
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
|
||||
// internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>) {
|
||||
//
|
||||
// self.snapshot = .init(
|
||||
// diffableSnapshot: snapshot,
|
||||
// context: controller.managedObjectContext
|
||||
// )
|
||||
// }
|
||||
|
||||
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: DiffableDataSourceSnapshotProtocol) {
|
||||
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot) {
|
||||
|
||||
self.snapshot = .init(
|
||||
diffableSnapshot: snapshot,
|
||||
context: controller.managedObjectContext
|
||||
)
|
||||
}
|
||||
|
||||
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String? {
|
||||
|
||||
return self.sectionIndexTransformer(sectionName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user