// // Internals.DiffableDataSourceSnapshot.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(UIKit) import UIKit #elseif canImport(AppKit) import AppKit #endif // MARK: - Internals extension Internals { // MARK: - DiffableDataSourceSnapshot // Implementation based on https://github.com/ra1028/DiffableDataSources internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol { // MARK: Internal init( sections: [NSFetchedResultsSectionInfo], sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?, fetchOffset: Int, fetchLimit: Int ) { self.structure = .init( sections: sections, sectionIndexTransformer: sectionIndexTransformer, fetchOffset: Swift.max(0, fetchOffset), fetchLimit: (fetchLimit > 0) ? fetchLimit : nil ) } var sections: [Section] { get { return self.structure.sections } set { self.structure.sections = newValue } } // MARK: DiffableDataSourceSnapshotProtocol init() { self.structure = .init() } var numberOfItems: Int { return self.structure.allItemsCount } var numberOfSections: Int { return self.structure.allSectionIDs.count } var sectionIdentifiers: [String] { return self.structure.allSectionIDs } var itemIdentifiers: [NSManagedObjectID] { return self.structure.allItemIDs } var updatedItemIdentifiers: Set { return self.structure.reloadedItems } func numberOfItems(inSection identifier: String) -> Int { return self.itemIdentifiers(inSection: identifier).count } func itemIdentifier(atAllItemsIndex index: Int) -> NSManagedObjectID? { guard index >= 0 else { return nil } var remainingIndex = index for section in self.structure.sections { let elements = section.elements let sectionCount = elements.count if remainingIndex < sectionCount { return elements[remainingIndex].differenceIdentifier } remainingIndex -= sectionCount } return nil } func itemIdentifiers(atAllItemsBounds bounds: Range) -> [NSManagedObjectID] { var remainingIndex = bounds.lowerBound var itemIdentifiers: [NSManagedObjectID] = [] for section in self.structure.sections { let elements = section.elements let sectionCount = elements.count if remainingIndex < sectionCount { itemIdentifiers.append( contentsOf: elements[remainingIndex..= bounds.count { return itemIdentifiers } remainingIndex -= sectionCount } return itemIdentifiers } func itemIdentifiers(inSection identifier: String) -> [NSManagedObjectID] { return self.structure.items(in: identifier) } func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> String? { return self.structure.section(containing: identifier) } func indexOfItem(_ identifier: NSManagedObjectID) -> Int? { return self.structure.allItemIDs.firstIndex(of: identifier) } func indexOfSection(_ identifier: String) -> Int? { return self.structure.allSectionIDs.firstIndex(of: identifier) } mutating func appendItems(_ identifiers: C, toSection sectionIdentifier: String?) where C.Element == NSManagedObjectID { self.structure.append(itemIDs: identifiers, to: sectionIdentifier) } mutating func insertItems(_ identifiers: C, beforeItem beforeIdentifier: NSManagedObjectID) where C.Element == NSManagedObjectID { self.structure.insert(itemIDs: identifiers, before: beforeIdentifier) } mutating func insertItems(_ identifiers: C, afterItem afterIdentifier: NSManagedObjectID) where C.Element == NSManagedObjectID { self.structure.insert(itemIDs: identifiers, after: afterIdentifier) } mutating func deleteItems(_ identifiers: C) where C.Element == NSManagedObjectID { self.structure.remove(itemIDs: identifiers) } mutating func deleteAllItems() { self.structure.removeAllItems() } mutating func moveItem(_ identifier: NSManagedObjectID, beforeItem toIdentifier: NSManagedObjectID) { self.structure.move(itemID: identifier, before: toIdentifier) } mutating func moveItem(_ identifier: NSManagedObjectID, afterItem toIdentifier: NSManagedObjectID) { self.structure.move(itemID: identifier, after: toIdentifier) } mutating func reloadItems(_ identifiers: C) where C.Element == NSManagedObjectID { self.structure.update(itemIDs: identifiers) } mutating func appendSections(_ identifiers: C) where C.Element == String { self.structure.append(sectionIDs: identifiers) } mutating func insertSections(_ identifiers: C, beforeSection toIdentifier: String) where C.Element == String { self.structure.insert(sectionIDs: identifiers, before: toIdentifier) } mutating func insertSections(_ identifiers: C, afterSection toIdentifier: String) where C.Element == String { self.structure.insert(sectionIDs: identifiers, after: toIdentifier) } mutating func deleteSections(_ identifiers: C) where C.Element == String { self.structure.remove(sectionIDs: identifiers) } mutating func moveSection(_ identifier: String, beforeSection toIdentifier: String) { self.structure.move(sectionID: identifier, before: toIdentifier) } mutating func moveSection(_ identifier: String, afterSection toIdentifier: String) { self.structure.move(sectionID: identifier, after: toIdentifier) } mutating func reloadSections(_ identifiers: C) where C.Element == String { self.structure.update(sectionIDs: identifiers) } mutating func unsafeAppendItems(_ identifiers: C, toSectionAt sectionIndex: Int) where C.Element == NSManagedObjectID { self.structure.unsafeAppend(identifiers, toSectionAt: sectionIndex) } mutating func unsafeInsertItems(_ identifiers: C, at indexPath: IndexPath) where C.Element == NSManagedObjectID { self.structure.unsafeInsert(itemIDs: identifiers, at: indexPath) } mutating func unsafeDeleteItems(at indexPaths: C) where C.Element == IndexPath { self.structure.unsafeRemove(itemsAt: indexPaths) } mutating func unsafeMoveItem(at indexPath: IndexPath, to newIndexPath: IndexPath) { self.structure.unsafeMove(itemAt: indexPath, to: newIndexPath) } mutating func unsafeReloadItems(at indexPaths: C) where C.Element == IndexPath { self.structure.unsafeUpdate(itemsAt: indexPaths) } mutating func unsafeInsertSections(_ identifiers: C, at sectionIndex: Int) where C.Element == String { self.structure.unsafeInsert(identifiers, at: sectionIndex) } mutating func unsafeDeleteSections(at sectionIndices: C) where C.Element == Int { self.structure.unsafeRemove(sectionsAt: sectionIndices) } mutating func unsafeMoveSection(at sectionIndex: Int, to newSectionIndex: Int) { self.structure.unsafeMove(sectionAt: sectionIndex, to: newSectionIndex) } mutating func unsafeReloadSections(at sectionIndices: C) where C.Element == Int { self.structure.unsafeUpdate(sectionsAt: sectionIndices) } // MARK: Private private var structure: BackingStructure // MARK: - Section internal struct Section: DifferentiableSection, Equatable { let indexTitle: String? var isReloaded: Bool init( differenceIdentifier: String, indexTitle: String?, items: [Item] = [], isReloaded: Bool = false ) { self.differenceIdentifier = differenceIdentifier self.indexTitle = indexTitle self.elements = items self.isReloaded = isReloaded } // MARK: Differentiable let differenceIdentifier: String func isContentEqual(to source: Section) -> Bool { return !self.isReloaded && self.differenceIdentifier == source.differenceIdentifier } // MARK: DifferentiableSection var elements: [Item] = [] init(source: Section, elements: S) where S.Element == Item { self.init( differenceIdentifier: source.differenceIdentifier, indexTitle: source.indexTitle, items: Array(elements), isReloaded: source.isReloaded ) } } // MARK: - Item internal struct Item: Differentiable, Equatable { var isReloaded: Bool init(differenceIdentifier: NSManagedObjectID, isReloaded: Bool = false) { self.differenceIdentifier = differenceIdentifier self.isReloaded = isReloaded } // MARK: Differentiable let differenceIdentifier: NSManagedObjectID func isContentEqual(to source: Item) -> Bool { return !self.isReloaded && self.differenceIdentifier == source.differenceIdentifier } } // MARK: - BackingStructure fileprivate struct BackingStructure { // MARK: Internal let sectionIndexTransformer: (_ sectionName: String?) -> String? var sections: [Section] private(set) var reloadedItems: Set init() { self.sectionIndexTransformer = { _ in nil } self.sections = [] self.reloadedItems = [] } init( sections: [NSFetchedResultsSectionInfo], sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?, fetchOffset: Int, fetchLimit: Int? ) { let sliceItems: (_ array: [Any], _ offset: Int) -> Array.SubSequence if let fetchLimit = fetchLimit { var remainingCount = fetchLimit sliceItems = { let slice = $0[$1...].prefix(remainingCount) remainingCount -= slice.count return slice } } else { sliceItems = { $0[$1...] } } var newSections: [Internals.DiffableDataSourceSnapshot.Section] = [] var ignoreCount = fetchOffset for section in sections { let objects = section.objects ?? [] guard objects.indices.contains(ignoreCount) else { ignoreCount -= objects.count continue } let items = sliceItems(objects, ignoreCount) .map({ Item(differenceIdentifier: ($0 as! NSManagedObject).objectID) }) ignoreCount = 0 guard !items.isEmpty else { continue } newSections.append( Section( differenceIdentifier: section.name, indexTitle: section.indexTitle, items: items ) ) } self.sectionIndexTransformer = sectionIndexTransformer self.sections = newSections self.reloadedItems = [] } var allSectionIDs: [String] { return self.sections.map({ $0.differenceIdentifier }) } var allItemsCount: Int { return self.sections.reduce(into: 0) { (result, section) in result += section.elements.count } } var allItemIDs: [NSManagedObjectID] { return self.sections.lazy.flatMap({ $0.elements }).map({ $0.differenceIdentifier }) } func items(in sectionID: String) -> [NSManagedObjectID] { guard let sectionIndex = self.sectionIndex(of: sectionID) else { Internals.abort("Section \"\(sectionID)\" does not exist") } return self.sections[sectionIndex].elements.map({ $0.differenceIdentifier }) } func unsafeItem(at indexPath: IndexPath) -> NSManagedObjectID { return self.sections[indexPath.section] .elements[indexPath.item] .differenceIdentifier } func section(containing itemID: NSManagedObjectID) -> String? { return self.itemPositionMap(itemID)?.section.differenceIdentifier } mutating func append( itemIDs: C, to sectionID: String? ) where C.Element == NSManagedObjectID { let index: Array
.Index if let sectionID = sectionID { guard let sectionIndex = self.sectionIndex(of: sectionID) else { Internals.abort("Section \"\(sectionID)\" does not exist") } index = sectionIndex } else { let section = self.sections guard !section.isEmpty else { Internals.abort("No sections exist") } index = section.index(before: section.endIndex) } let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) }) self.sections[index].elements.append(contentsOf: items) } mutating func unsafeAppend( _ itemIDs: C, toSectionAt sectionIndex: Int? ) where C.Element == NSManagedObjectID { let index: Array
.Index if let sectionIndex = sectionIndex { index = sectionIndex } else { let section = self.sections index = section.index(before: section.endIndex) } let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) }) self.sections[index].elements.append(contentsOf: items) } mutating func insert( itemIDs: C, before beforeItemID: NSManagedObjectID ) where C.Element == NSManagedObjectID { guard let itemPosition = self.itemPositionMap(beforeItemID) else { Internals.abort("Item \(beforeItemID) does not exist") } let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) }) self.sections[itemPosition.sectionIndex].elements .insert(contentsOf: items, at: itemPosition.itemRelativeIndex) } mutating func insert( itemIDs: C, after afterItemID: NSManagedObjectID ) where C.Element == NSManagedObjectID { guard let itemPosition = self.itemPositionMap(afterItemID) else { Internals.abort("Item \(afterItemID) does not exist") } let itemIndex = self.sections[itemPosition.sectionIndex].elements .index(after: itemPosition.itemRelativeIndex) let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) }) self.sections[itemPosition.sectionIndex].elements .insert(contentsOf: items, at: itemIndex) } mutating func unsafeInsert( itemIDs: C, at indexPath: IndexPath ) where C.Element == NSManagedObjectID { let items = itemIDs.lazy.map({ Item(differenceIdentifier: $0) }) self.sections[indexPath.section].elements .insert(contentsOf: items, at: indexPath.item) } mutating func remove(itemIDs: S) where S.Element == NSManagedObjectID { let itemPositionMap = self.itemPositionMap() var removeIndexSetMap: [Int: IndexSet] = [:] for itemID in itemIDs { guard let itemPosition = itemPositionMap[itemID] else { continue } removeIndexSetMap[itemPosition.sectionIndex, default: []] .insert(itemPosition.itemRelativeIndex) } for (sectionIndex, removeIndexSet) in removeIndexSetMap { for range in removeIndexSet.rangeView.reversed() { self.sections[sectionIndex].elements.removeSubrange(range) } } } mutating func unsafeRemove(itemsAt indexPaths: S) where S.Element == IndexPath { var removeIndexSetMap: [Int: IndexSet] = [:] for indexPath in indexPaths { removeIndexSetMap[indexPath.section, default: []] .insert(indexPath.item) } for (sectionIndex, removeIndexSet) in removeIndexSetMap { for range in removeIndexSet.rangeView.reversed() { self.sections[sectionIndex].elements.removeSubrange(range) } } } mutating func removeAllItems() { for sectionIndex in self.sections.indices { self.sections[sectionIndex].elements.removeAll() } } mutating func removeAllEmptySections() { self.sections.removeAll(where: { $0.elements.isEmpty }) } mutating func move( itemID: NSManagedObjectID, before beforeItemID: NSManagedObjectID ) { guard let removed = self.remove(itemID: itemID) else { Internals.abort("Item \(itemID) does not exist") } guard let itemPosition = self.itemPositionMap(beforeItemID) else { Internals.abort("Item \(beforeItemID) does not exist") } self.sections[itemPosition.sectionIndex].elements .insert(removed, at: itemPosition.itemRelativeIndex) } mutating func move( itemID: NSManagedObjectID, after afterItemID: NSManagedObjectID ) { guard let removed = self.remove(itemID: itemID) else { Internals.abort("Item \(itemID) does not exist") } guard let itemPosition = self.itemPositionMap(afterItemID) else { Internals.abort("Item \(afterItemID) does not exist") } let itemIndex = self.sections[itemPosition.sectionIndex].elements .index(after: itemPosition.itemRelativeIndex) self.sections[itemPosition.sectionIndex].elements .insert(removed, at: itemIndex) } mutating func unsafeMove( itemAt indexPath: IndexPath, to newIndexPath: IndexPath ) { let itemID = self.sections[indexPath.section].elements .remove(at: indexPath.item) self.sections[newIndexPath.section].elements .insert(itemID, at: newIndexPath.item) } mutating func update(itemIDs: S) where S.Element == NSManagedObjectID { let itemPositionMap = self.itemPositionMap() var newItemIDs: Set = [] for itemID in itemIDs { guard let itemPosition = itemPositionMap[itemID] else { continue } self.sections[itemPosition.sectionIndex] .elements[itemPosition.itemRelativeIndex].isReloaded = true newItemIDs.insert(itemID) } self.reloadedItems.formUnion(newItemIDs) } mutating func unsafeUpdate(itemsAt indexPaths: S) where S.Element == IndexPath { var newItemIDs: Set = [] for indexPath in indexPaths { self.sections[indexPath.section] .elements[indexPath.item].isReloaded = true newItemIDs.insert(self.unsafeItem(at: indexPath)) } self.reloadedItems.formUnion(newItemIDs) } mutating func append(sectionIDs: C) where C.Element == String { let sectionIndexTransformer = self.sectionIndexTransformer let newSections = sectionIDs.lazy.map { return Section( differenceIdentifier: $0, indexTitle: sectionIndexTransformer($0) ) } self.sections.append(contentsOf: newSections) } mutating func insert( sectionIDs: C, before beforeSectionID: String ) where C.Element == String { guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else { Internals.abort("Section \"\(beforeSectionID)\" does not exist") } let sectionIndexTransformer = self.sectionIndexTransformer let newSections = sectionIDs.lazy.map { return Section( differenceIdentifier: $0, indexTitle: sectionIndexTransformer($0) ) } self.sections.insert(contentsOf: newSections, at: sectionIndex) } mutating func insert( sectionIDs: C, after afterSectionID: String ) where C.Element == String { guard let beforeIndex = self.sectionIndex(of: afterSectionID) else { Internals.abort("Section \"\(afterSectionID)\" does not exist") } let sectionIndexTransformer = self.sectionIndexTransformer let sectionIndex = self.sections.index(after: beforeIndex) let newSections = sectionIDs.lazy.map { return Section( differenceIdentifier: $0, indexTitle: sectionIndexTransformer($0) ) } self.sections.insert(contentsOf: newSections, at: sectionIndex) } mutating func unsafeInsert( _ sectionIDs: C, at sectionIndex: Int ) where C.Element == String { let sectionIndexTransformer = self.sectionIndexTransformer let newSections = sectionIDs.lazy.map { return Section( differenceIdentifier: $0, indexTitle: sectionIndexTransformer($0) ) } self.sections.insert(contentsOf: newSections, at: sectionIndex) } mutating func remove(sectionIDs: S) where S.Element == String { for sectionID in sectionIDs { self.remove(sectionID: sectionID) } } mutating func unsafeRemove( sectionsAt sectionIndices: S ) where S.Element == Int { for sectionIndex in sectionIndices.sorted(by: >) { self.sections.remove(at: sectionIndex) } } mutating func move(sectionID: String, before beforeSectionID: String) { guard let removed = self.remove(sectionID: sectionID) else { Internals.abort("Section \"\(sectionID)\" does not exist") } guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else { Internals.abort("Section \"\(beforeSectionID)\" does not exist") } self.sections.insert(removed, at: sectionIndex) } mutating func move(sectionID: String, after afterSectionID: String) { guard let removed = self.remove(sectionID: sectionID) else { Internals.abort("Section \"\(sectionID)\" does not exist") } guard let beforeIndex = self.sectionIndex(of: afterSectionID) else { Internals.abort("Section \"\(afterSectionID)\" does not exist") } let sectionIndex = self.sections.index(after: beforeIndex) self.sections.insert(removed, at: sectionIndex) } mutating func unsafeMove( sectionAt sectionIndex: Int, to newSectionIndex: Int ) { self.sections.move( fromOffsets: .init(integer: sectionIndex), toOffset: newSectionIndex ) } mutating func update( sectionIDs: S ) where S.Element == String { for sectionID in sectionIDs { guard let sectionIndex = self.sectionIndex(of: sectionID) else { continue } self.sections[sectionIndex].isReloaded = true } } mutating func unsafeUpdate( sectionsAt sectionIndices: S ) where S.Element == Int { for sectionIndex in sectionIndices { self.sections[sectionIndex].isReloaded = true } } // MARK: Private private func sectionIndex(of sectionID: String) -> Array
.Index? { return self.sections.firstIndex(where: { $0.differenceIdentifier == sectionID }) } @discardableResult private mutating func remove(itemID: NSManagedObjectID) -> Item? { guard let itemPosition = self.itemPositionMap(itemID) else { return nil } return self.sections[itemPosition.sectionIndex].elements .remove(at: itemPosition.itemRelativeIndex) } @discardableResult private mutating func remove(sectionID: String) -> Section? { guard let sectionIndex = self.sectionIndex(of: sectionID) else { return nil } return self.sections.remove(at: sectionIndex) } private func itemPositionMap(_ itemID: NSManagedObjectID) -> ItemPosition? { let sections = self.sections for (sectionIndex, section) in sections.enumerated() { for (itemRelativeIndex, item) in section.elements.enumerated() { guard item.differenceIdentifier == itemID else { continue } return ItemPosition( item: item, itemRelativeIndex: itemRelativeIndex, section: section, sectionIndex: sectionIndex ) } } return nil } private func itemPositionMap() -> [NSManagedObjectID: ItemPosition] { return self.sections.enumerated().reduce(into: [:]) { result, section in for (itemRelativeIndex, item) in section.element.elements.enumerated() { result[item.differenceIdentifier] = ItemPosition( item: item, itemRelativeIndex: itemRelativeIndex, section: section.element, sectionIndex: section.offset ) } } } // MARK: - ItemPosition fileprivate struct ItemPosition { let item: Item let itemRelativeIndex: Int let section: Section let sectionIndex: Int } } } } #endif