// // 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]) { self.structure = .init(sections: sections) } 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.allItemIDs.count } var numberOfSections: Int { return self.structure.allSectionIDs.count } var sectionIdentifiers: [String] { return self.structure.allSectionIDs } var itemIdentifiers: [NSManagedObjectID] { return self.structure.allItemIDs } func numberOfItems(inSection identifier: String) -> Int { return self.itemIdentifiers(inSection: identifier).count } 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: [NSManagedObjectID], toSection sectionIdentifier: String?) { self.structure.append(itemIDs: identifiers, to: sectionIdentifier) } mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID) { self.structure.insert(itemIDs: identifiers, before: beforeIdentifier) } mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID) { self.structure.insert(itemIDs: identifiers, after: afterIdentifier) } mutating func deleteItems(_ identifiers: [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: [NSManagedObjectID]) { self.structure.update(itemIDs: identifiers) } mutating func appendSections(_ identifiers: [String]) { self.structure.append(sectionIDs: identifiers) } mutating func insertSections(_ identifiers: [String], beforeSection toIdentifier: String) { self.structure.insert(sectionIDs: identifiers, before: toIdentifier) } mutating func insertSections(_ identifiers: [String], afterSection toIdentifier: String) { self.structure.insert(sectionIDs: identifiers, after: toIdentifier) } mutating func deleteSections(_ identifiers: [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: [String]) { self.structure.update(sectionIDs: identifiers) } mutating func resize(limit: Int) { let itemIdentifiers = self.itemIdentifiers guard itemIdentifiers.endIndex > limit else { return } self.structure.remove(itemIDs: itemIdentifiers.suffix(from: limit)) self.structure.removeAllEmptySections() } // MARK: Private private var structure: BackingStructure // MARK: - Section internal struct Section: DifferentiableSection, Equatable { var isReloaded: Bool init(differenceIdentifier: String, items: [Item] = [], isReloaded: Bool = false) { self.differenceIdentifier = differenceIdentifier 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, 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 var sections: [Section] init() { self.sections = [] } init(sections: [NSFetchedResultsSectionInfo]) { self.sections = sections.map { Section( differenceIdentifier: $0.name, items: $0.objects? .compactMap({ ($0 as? NSManagedObject)?.objectID }) .map({ Item(differenceIdentifier: $0) }) ?? [] ) } } var allSectionIDs: [String] { return self.sections.map({ $0.differenceIdentifier }) } 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 section(containing itemID: NSManagedObjectID) -> String? { return self.itemPositionMap()[itemID]?.section.differenceIdentifier } mutating func append(itemIDs: [NSManagedObjectID], to sectionID: String?) { 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 insert(itemIDs: [NSManagedObjectID], before beforeItemID: 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: [NSManagedObjectID], after afterItemID: 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 remove(itemIDs: C) where C.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 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 update(itemIDs: S) where S.Element == NSManagedObjectID { let itemPositionMap = self.itemPositionMap() for itemID in itemIDs { guard let itemPosition = itemPositionMap[itemID] else { continue } self.sections[itemPosition.sectionIndex] .elements[itemPosition.itemRelativeIndex].isReloaded = true } } mutating func append(sectionIDs: [String]) { let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) }) self.sections.append(contentsOf: newSections) } mutating func insert(sectionIDs: [String], before beforeSectionID: String) { guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else { Internals.abort("Section \"\(beforeSectionID)\" does not exist") } let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) }) self.sections.insert(contentsOf: newSections, at: sectionIndex) } mutating func insert(sectionIDs: [String], after afterSectionID: String) { guard let beforeIndex = self.sectionIndex(of: afterSectionID) else { Internals.abort("Section \"\(afterSectionID)\" does not exist") } let sectionIndex = self.sections.index(after: beforeIndex) let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) }) self.sections.insert(contentsOf: newSections, at: sectionIndex) } mutating func remove(sectionIDs: [String]) { for sectionID in sectionIDs { self.remove(sectionID: sectionID) } } 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 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 } } // 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() -> [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