WIP: editable datasources

This commit is contained in:
John Estropia
2019-10-11 07:47:49 +09:00
parent d5114fc4bc
commit 81dfb8e3e5
21 changed files with 1253 additions and 479 deletions

View File

@@ -43,7 +43,12 @@ public protocol DynamicObject: AnyObject {
Used internally by CoreStore. Do not call directly.
*/
static func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]
/**
Used internally by CoreStore. Do not call directly.
*/
@@ -97,6 +102,13 @@ extension NSManagedObject: DynamicObject {
}
return object
}
public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any] {
let object = context.fetchExisting(id)! as Self
let rawObject = object.cs_toRaw()
return rawObject.dictionaryWithValues(forKeys: rawObject.entity.properties.map({ $0.name }))
}
public class func cs_fromRaw(object: NSManagedObject) -> Self {
@@ -119,16 +131,6 @@ extension NSManagedObject: DynamicObject {
}
}
extension DynamicObject where Self: NSManagedObject {
// MARK: Public
public func createSnapshot() -> ObjectSnapshot<Self> {
return ObjectSnapshot(from: self)
}
}
// MARK: - CoreStoreObject
@@ -146,6 +148,43 @@ extension CoreStoreObject {
}
return self.cs_fromRaw(object: object)
}
public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any] {
func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) {
if let superClassMirror = mirror.superclassMirror {
initializeAttributes(
mirror: superClassMirror,
object: object,
into: &attributes
)
}
for child in mirror.children {
switch child.value {
case let property as AttributeProtocol:
attributes[property.keyPath] = property.valueForSnapshot
case let property as RelationshipProtocol:
attributes[property.keyPath] = property.valueForSnapshot
default:
continue
}
}
}
let object = context.fetchExisting(id)! as Self
var values: [KeyPathString: Any] = [:]
initializeAttributes(
mirror: Mirror(reflecting: object),
object: object,
into: &values
)
return values
}
public class func cs_fromRaw(object: NSManagedObject) -> Self {
@@ -181,13 +220,3 @@ extension CoreStoreObject {
return self.rawObject!
}
}
extension DynamicObject where Self: CoreStoreObject {
// MARK: Public
public func createSnapshot() -> ObjectSnapshot<Self> {
return ObjectSnapshot(from: self)
}
}

View File

@@ -40,88 +40,111 @@ import AppKit
extension Internals {
// MARK: Internal
internal typealias DiffableDataSourceSnapshot = _Internal_DiffableDataSourceSnapshot
// MARK: - FallbackDiffableDataSourceSnapshot
// MARK: - DiffableDataSourceSnapshot
// Implementation based on https://github.com/ra1028/DiffableDataSources
internal struct FallbackDiffableDataSourceSnapshot: DiffableDataSourceSnapshot {
internal struct DiffableDataSourceSnapshot {
// MARK: Internal
init(sections: [NSFetchedResultsSectionInfo]) {
self.structure = .init(sections: sections)
}
// MARK: DiffableDataSourceSnapshot
internal let nextStateTag: UUID
init() {
self.structure = .init()
self.nextStateTag = .init()
}
init(sections: [NSFetchedResultsSectionInfo], previousStateTag: UUID, nextStateTag: UUID) {
self.structure = .init(sections: sections, previousStateTag: previousStateTag)
self.nextStateTag = nextStateTag
}
var numberOfItems: Int {
return self.itemIdentifiers.count
return self.structure.allItemIDs.count
}
var numberOfSections: Int {
return self.sectionIdentifiers.count
return self.structure.allSectionIDs.count
}
var sectionIdentifiers: [NSString] {
var allSectionIDs: [String] {
return self.structure.allSectionIDs
}
var itemIdentifiers: [NSManagedObjectID] {
var allSectionStateIDs: [SectionStateID] {
self.structure.allItemIDs
return self.structure.allSectionStateIDs
}
func numberOfItems(inSection identifier: NSString) -> Int {
var allItemIDs: [NSManagedObjectID] {
return self.itemIdentifiers(inSection: identifier).count
return self.structure.allItemIDs
}
func itemIdentifiers(inSection identifier: NSString) -> [NSManagedObjectID] {
var allItemStateIDs: [ItemStateID] {
return self.structure.allItemStateIDs
}
func numberOfItems(inSection identifier: String) -> Int {
return self.itemIDs(inSection: identifier).count
}
func itemIDs(inSection identifier: String) -> [NSManagedObjectID] {
return self.structure.items(in: identifier)
}
func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> NSString? {
func itemStateIDs(inSection identifier: String) -> [ItemStateID] {
return self.structure.itemStateIDs(in: identifier)
}
func sectionIDs(containingItem identifier: NSManagedObjectID) -> String? {
return self.structure.section(containing: identifier)
}
func indexOfItem(_ identifier: NSManagedObjectID) -> Int? {
func sectionStateIDs(containingItem identifier: NSManagedObjectID) -> SectionStateID? {
return self.itemIdentifiers.firstIndex(of: identifier)
return self.structure.sectionStateID(containing: identifier)
}
func indexOfSection(_ identifier: NSString) -> Int? {
func indexOfItemID(_ identifier: NSManagedObjectID) -> Int? {
return self.sectionIdentifiers.firstIndex(of: identifier)
return self.structure.allItemIDs.firstIndex(of: identifier)
}
mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: NSString?) {
func indexOfSectionID(_ identifier: String) -> Int? {
self.structure.append(itemIDs: identifiers, to: sectionIdentifier)
return self.structure.allSectionIDs.firstIndex(of: identifier)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID) {
func itemIDs(where stateCondition: @escaping (UUID) -> Bool) -> [NSManagedObjectID] {
self.structure.insert(itemIDs: identifiers, before: beforeIdentifier)
return self.structure.itemsIDs(where: stateCondition)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID) {
mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: String?, nextStateTag: UUID) {
self.structure.insert(itemIDs: identifiers, after: afterIdentifier)
self.structure.append(itemIDs: identifiers, to: sectionIdentifier, nextStateTag: nextStateTag)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID, nextStateTag: UUID) {
self.structure.insert(itemIDs: identifiers, before: beforeIdentifier, nextStateTag: nextStateTag)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID, nextStateTag: UUID) {
self.structure.insert(itemIDs: identifiers, after: afterIdentifier, nextStateTag: nextStateTag)
}
mutating func deleteItems(_ identifiers: [NSManagedObjectID]) {
@@ -144,44 +167,44 @@ extension Internals {
self.structure.move(itemID: identifier, after: toIdentifier)
}
mutating func reloadItems(_ identifiers: [NSManagedObjectID]) {
mutating func reloadItems<S: Sequence>(_ identifiers: S, nextStateTag: UUID) where S.Element == NSManagedObjectID {
self.structure.update(itemIDs: identifiers)
self.structure.update(itemIDs: identifiers, nextStateTag: nextStateTag)
}
mutating func appendSections(_ identifiers: [NSString]) {
mutating func appendSections(_ identifiers: [String], nextStateTag: UUID) {
self.structure.append(sectionIDs: identifiers)
self.structure.append(sectionIDs: identifiers, nextStateTag: nextStateTag)
}
mutating func insertSections(_ identifiers: [NSString], beforeSection toIdentifier: NSString) {
mutating func insertSections(_ identifiers: [String], beforeSection toIdentifier: String, nextStateTag: UUID) {
self.structure.insert(sectionIDs: identifiers, before: toIdentifier)
self.structure.insert(sectionIDs: identifiers, before: toIdentifier, nextStateTag: nextStateTag)
}
mutating func insertSections(_ identifiers: [NSString], afterSection toIdentifier: NSString) {
mutating func insertSections(_ identifiers: [String], afterSection toIdentifier: String, nextStateTag: UUID) {
self.structure.insert(sectionIDs: identifiers, after: toIdentifier)
self.structure.insert(sectionIDs: identifiers, after: toIdentifier, nextStateTag: nextStateTag)
}
mutating func deleteSections(_ identifiers: [NSString]) {
mutating func deleteSections(_ identifiers: [String]) {
self.structure.remove(sectionIDs: identifiers)
}
mutating func moveSection(_ identifier: NSString, beforeSection toIdentifier: NSString) {
mutating func moveSection(_ identifier: String, beforeSection toIdentifier: String) {
self.structure.move(sectionID: identifier, before: toIdentifier)
}
mutating func moveSection(_ identifier: NSString, afterSection toIdentifier: NSString) {
mutating func moveSection(_ identifier: String, afterSection toIdentifier: String) {
self.structure.move(sectionID: identifier, after: toIdentifier)
}
mutating func reloadSections(_ identifiers: [NSString]) {
mutating func reloadSections<S: Sequence>(_ identifiers: S, nextStateTag: UUID) where S.Element == String {
self.structure.update(sectionIDs: identifiers)
self.structure.update(sectionIDs: identifiers, nextStateTag: nextStateTag)
}
@@ -190,9 +213,54 @@ extension Internals {
private var structure: BackingStructure
// MARK: - ItemStateID
internal struct ItemStateID: Identifiable, Equatable {
let stateTag: UUID
init(id: NSManagedObjectID, stateTag: UUID) {
self.id = id
self.stateTag = stateTag
}
func isContentEqual(to source: ItemStateID) -> Bool {
return self.id == source.id && self.stateTag == source.stateTag
}
// MARK: Identifiable
let id: NSManagedObjectID
}
// MARK: - SectionStateID
internal struct SectionStateID: Identifiable, Equatable {
let stateTag: UUID
init(id: String, stateTag: UUID) {
self.id = id
self.stateTag = stateTag
}
func isContentEqual(to source: SectionStateID) -> Bool {
return self.id == source.id && self.stateTag == source.stateTag
}
// MARK: Identifiable
let id: String
}
// MARK: - BackingStructure
internal struct BackingStructure {
fileprivate struct BackingStructure {
// MARK: Internal
@@ -203,31 +271,41 @@ extension Internals {
self.sections = []
}
init(sections: [NSFetchedResultsSectionInfo]) {
init(sections: [NSFetchedResultsSectionInfo], previousStateTag: UUID) {
self.sections = sections.map {
Section(
id: $0.name as NSString,
id: $0.name,
items: $0.objects?
.compactMap({ ($0 as? NSManagedObject)?.objectID })
.map(Item.init(id:)) ?? [],
isReloaded: false
.map({ Item(id: $0, stateTag: previousStateTag) }) ?? [],
stateTag: previousStateTag
)
}
}
var allSectionIDs: [NSString] {
var allSectionIDs: [String] {
return self.sections.map({ $0.id })
}
var allSectionStateIDs: [SectionStateID] {
return self.sections.map({ $0.stateID })
}
var allItemIDs: [NSManagedObjectID] {
return self.sections.lazy.flatMap({ $0.elements }).map({ $0.id })
}
func items(in sectionID: NSString) -> [NSManagedObjectID] {
var allItemStateIDs: [ItemStateID] {
return self.sections.lazy.flatMap({ $0.elements }).map({ $0.stateID })
}
func items(in sectionID: String) -> [NSManagedObjectID] {
guard let sectionIndex = self.sectionIndex(of: sectionID) else {
@@ -236,12 +314,33 @@ extension Internals {
return self.sections[sectionIndex].elements.map({ $0.id })
}
func section(containing itemID: NSManagedObjectID) -> NSString? {
func itemsIDs(where stateCondition: @escaping (UUID) -> Bool) -> [NSManagedObjectID] {
return self.sections.lazy
.flatMap({ $0.elements.filter({ stateCondition($0.stateTag) }) })
.map({ $0.id })
}
func itemStateIDs(in sectionID: String) -> [ItemStateID] {
guard let sectionIndex = self.sectionIndex(of: sectionID) else {
Internals.abort("Section \"\(sectionID)\" does not exist")
}
return self.sections[sectionIndex].elements.map({ $0.stateID })
}
func section(containing itemID: NSManagedObjectID) -> String? {
return self.itemPositionMap()[itemID]?.section.id
}
mutating func append(itemIDs: [NSManagedObjectID], to sectionID: NSString?) {
func sectionStateID(containing itemID: NSManagedObjectID) -> SectionStateID? {
return self.itemPositionMap()[itemID]?.section.stateID
}
mutating func append(itemIDs: [NSManagedObjectID], to sectionID: String?, nextStateTag: UUID) {
let index: Array<Section>.Index
if let sectionID = sectionID {
@@ -261,23 +360,22 @@ extension Internals {
}
index = section.index(before: section.endIndex)
}
let items = itemIDs.lazy.map(Item.init)
let items = itemIDs.lazy.map({ Item(id: $0, stateTag: nextStateTag) })
self.sections[index].elements.append(contentsOf: items)
}
mutating func insert(itemIDs: [NSManagedObjectID], before beforeItemID: NSManagedObjectID) {
mutating func insert(itemIDs: [NSManagedObjectID], before beforeItemID: NSManagedObjectID, nextStateTag: UUID) {
guard let itemPosition = self.itemPositionMap()[beforeItemID] else {
Internals.abort("Item \(beforeItemID) does not exist")
}
let items = itemIDs.lazy.map(Item.init)
let items = itemIDs.lazy.map({ Item(id: $0, stateTag: nextStateTag) })
self.sections[itemPosition.sectionIndex].elements
.insert(contentsOf: items, at: itemPosition.itemRelativeIndex)
}
mutating func insert(itemIDs: [NSManagedObjectID], after afterItemID: NSManagedObjectID) {
mutating func insert(itemIDs: [NSManagedObjectID], after afterItemID: NSManagedObjectID, nextStateTag: UUID) {
guard let itemPosition = self.itemPositionMap()[afterItemID] else {
@@ -285,7 +383,7 @@ extension Internals {
}
let itemIndex = self.sections[itemPosition.sectionIndex].elements
.index(after: itemPosition.itemRelativeIndex)
let items = itemIDs.lazy.map(Item.init)
let items = itemIDs.lazy.map({ Item(id: $0, stateTag: nextStateTag) })
self.sections[itemPosition.sectionIndex].elements
.insert(contentsOf: items, at: itemIndex)
}
@@ -351,47 +449,48 @@ extension Internals {
.insert(removed, at: itemIndex)
}
mutating func update(itemIDs: [NSManagedObjectID]) {
mutating func update<S: Sequence>(itemIDs: S, nextStateTag: UUID) where S.Element == NSManagedObjectID {
let itemPositionMap = self.itemPositionMap()
for itemID in itemIDs {
guard let itemPosition = itemPositionMap[itemID] else {
Internals.abort("Item \(itemID) does not exist")
continue
}
self.sections[itemPosition.sectionIndex].elements[itemPosition.itemRelativeIndex].isReloaded = true
self.sections[itemPosition.sectionIndex]
.elements[itemPosition.itemRelativeIndex].stateTag = nextStateTag
}
}
mutating func append(sectionIDs: [NSString]) {
mutating func append(sectionIDs: [String], nextStateTag: UUID) {
let newSections = sectionIDs.lazy.map(Section.init)
let newSections = sectionIDs.lazy.map({ Section(id: $0, stateTag: nextStateTag) })
self.sections.append(contentsOf: newSections)
}
mutating func insert(sectionIDs: [NSString], before beforeSectionID: NSString) {
mutating func insert(sectionIDs: [String], before beforeSectionID: String, nextStateTag: UUID) {
guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else {
Internals.abort("Section \"\(beforeSectionID)\" does not exist")
}
let newSections = sectionIDs.lazy.map(Section.init)
let newSections = sectionIDs.lazy.map({ Section(id: $0, stateTag: nextStateTag) })
self.sections.insert(contentsOf: newSections, at: sectionIndex)
}
mutating func insert(sectionIDs: [NSString], after afterSectionID: NSString) {
mutating func insert(sectionIDs: [String], after afterSectionID: String, nextStateTag: UUID) {
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.init)
let newSections = sectionIDs.lazy.map({ Section(id: $0, stateTag: nextStateTag) })
self.sections.insert(contentsOf: newSections, at: sectionIndex)
}
mutating func remove(sectionIDs: [NSString]) {
mutating func remove(sectionIDs: [String]) {
for sectionID in sectionIDs {
@@ -399,7 +498,7 @@ extension Internals {
}
}
mutating func move(sectionID: NSString, before beforeSectionID: NSString) {
mutating func move(sectionID: String, before beforeSectionID: String) {
guard let removed = self.remove(sectionID: sectionID) else {
@@ -412,7 +511,7 @@ extension Internals {
self.sections.insert(removed, at: sectionIndex)
}
mutating func move(sectionID: NSString, after afterSectionID: NSString) {
mutating func move(sectionID: String, after afterSectionID: String) {
guard let removed = self.remove(sectionID: sectionID) else {
@@ -426,7 +525,7 @@ extension Internals {
self.sections.insert(removed, at: sectionIndex)
}
mutating func update(sectionIDs: [NSString]) {
mutating func update<S: Sequence>(sectionIDs: S, nextStateTag: UUID) where S.Element == String {
for sectionID in sectionIDs {
@@ -434,14 +533,16 @@ extension Internals {
continue
}
self.sections[sectionIndex].isReloaded = true
self.sections[sectionIndex].stateTag = nextStateTag
}
}
// MARK: Private
private func sectionIndex(of sectionID: NSString) -> Array<Section>.Index? {
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 })
}
@@ -458,7 +559,7 @@ extension Internals {
}
@discardableResult
private mutating func remove(sectionID: NSString) -> Section? {
private mutating func remove(sectionID: String) -> Section? {
guard let sectionIndex = self.sectionIndex(of: sectionID) else {
@@ -486,63 +587,63 @@ extension Internals {
// MARK: - Item
internal struct Item: Identifiable, Equatable {
fileprivate struct Item: Identifiable, Equatable {
var isReloaded: Bool
var stateTag: UUID
init(id: NSManagedObjectID, isReloaded: Bool) {
init(id: NSManagedObjectID, stateTag: UUID) {
self.id = id
self.isReloaded = isReloaded
self.stateTag = stateTag
}
init(id: NSManagedObjectID) {
var stateID: ItemStateID {
self.init(id: id, isReloaded: false)
return .init(id: self.id, stateTag: self.stateTag)
}
func isContentEqual(to source: Item) -> Bool {
return !self.isReloaded && self.id == source.id
return self.id == source.id && self.stateTag == source.stateTag
}
// MARK: Identifiable
var id: NSManagedObjectID
let id: NSManagedObjectID
}
// MARK: - Section
internal struct Section: Identifiable, Equatable {
fileprivate struct Section: Identifiable, Equatable {
var elements: [Item] = []
var isReloaded: Bool
var stateTag: UUID
init(id: NSString, items: [Item], isReloaded: Bool) {
init(id: String, items: [Item] = [], stateTag: UUID) {
self.id = id
self.elements = items
self.isReloaded = isReloaded
}
init(id: NSString) {
self.init(id: id, items: [], isReloaded: false)
self.stateTag = stateTag
}
init<S: Sequence>(source: Section, elements: S) where S.Element == Item {
self.init(id: source.id, items: Array(elements), isReloaded: source.isReloaded)
self.init(id: source.id, items: Array(elements), stateTag: source.stateTag)
}
var stateID: SectionStateID {
return .init(id: self.id, stateTag: self.stateTag)
}
func isContentEqual(to source: Section) -> Bool {
return !self.isReloaded && self.id == source.id
return self.id == source.id && self.stateTag == source.stateTag
}
// MARK: Identifiable
var id: NSString
let id: String
}
@@ -550,57 +651,14 @@ extension Internals {
fileprivate struct ItemPosition {
var item: Item
var itemRelativeIndex: Int
var section: Section
var sectionIndex: Int
let item: Item
let itemRelativeIndex: Int
let section: Section
let sectionIndex: Int
}
}
}
}
// MARK: - NSDiffableDataSourceSnapshot: Internals.DiffableDataSourceSnapshot
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
extension NSDiffableDataSourceSnapshot: Internals.DiffableDataSourceSnapshot where SectionIdentifierType == NSString, ItemIdentifierType == NSManagedObjectID {}
// MARK: - Internals.DiffableDataSourceSnapshot
internal protocol _Internal_DiffableDataSourceSnapshot {
init()
var numberOfItems: Int { get }
var numberOfSections: Int { get }
var sectionIdentifiers: [NSString] { get }
var itemIdentifiers: [NSManagedObjectID] { get }
func numberOfItems(inSection identifier: NSString) -> Int
func itemIdentifiers(inSection identifier: NSString) -> [NSManagedObjectID]
func sectionIdentifier(containingItem identifier: NSManagedObjectID) -> NSString?
func indexOfItem(_ identifier: NSManagedObjectID) -> Int?
func indexOfSection(_ identifier: NSString) -> Int?
mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: NSString?)
mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID)
mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID)
mutating func deleteItems(_ identifiers: [NSManagedObjectID])
mutating func deleteAllItems()
mutating func moveItem(_ identifier: NSManagedObjectID, beforeItem toIdentifier: NSManagedObjectID)
mutating func moveItem(_ identifier: NSManagedObjectID, afterItem toIdentifier: NSManagedObjectID)
mutating func reloadItems(_ identifiers: [NSManagedObjectID])
mutating func appendSections(_ identifiers: [NSString])
mutating func insertSections(_ identifiers: [NSString], beforeSection toIdentifier: NSString)
mutating func insertSections(_ identifiers: [NSString], afterSection toIdentifier: NSString)
mutating func deleteSections(_ identifiers: [NSString])
mutating func moveSection(_ identifier: NSString, beforeSection toIdentifier: NSString)
mutating func moveSection(_ identifier: NSString, afterSection toIdentifier: NSString)
mutating func reloadSections(_ identifiers: [NSString])
}
#endif

View File

@@ -75,14 +75,14 @@ extension Internals {
internal func initialFetch() {
#if canImport(UIKit) || canImport(AppKit)
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) {
return
}
#endif
// #if canImport(UIKit) || canImport(AppKit)
//
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) {
//
// return
// }
//
// #endif
guard let fetchedResultsController = self.fetchedResultsController else {
@@ -94,50 +94,65 @@ extension Internals {
// MARK: NSFetchedResultsControllerDelegate
#if canImport(UIKit) || canImport(AppKit)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
self.handler?.controller(
controller,
didChangContentWith: snapshot as NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
)
}
#endif
@objc
dynamic func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.reloadedIDs = []
}
// #if canImport(UIKit) || canImport(AppKit)
//
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
// @objc
// dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
//
// self.handler?.controller(
// controller,
// didChangContentWith: snapshot as NSDiffableDataSourceSnapshot<NSString, NSManagedObjectID>
// )
// }
//
// #endif
@objc
dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let nextStateTag: UUID = .init()
var dataSourceSnapshot = Internals.DiffableDataSourceSnapshot(
sections: controller.sections ?? [],
previousStateTag: self.previousStateTag,
nextStateTag: nextStateTag
)
dataSourceSnapshot.reloadItems(self.reloadedItemIDs, nextStateTag: nextStateTag)
dataSourceSnapshot.reloadSections(self.reloadedSectionIDs, nextStateTag: nextStateTag)
self.handler?.controller(
controller,
didChangContentWith: FallbackDiffableDataSourceSnapshot(
sections: controller.sections ?? []
)
didChangContentWith: dataSourceSnapshot
)
self.reloadedIDs = []
self.previousStateTag = nextStateTag
}
// @objc
// dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
//
//
//// [1].difference(from: <#T##BidirectionalCollection#>)
// let managedObject = anObject as! NSManagedObject
// self.reloadedIDs.insert(managedObject.objectID)
// }
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
let managedObject = anObject as! NSManagedObject
self.reloadedItemIDs.insert(managedObject.objectID)
}
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
self.reloadedSectionIDs.insert(sectionInfo.name)
}
// MARK: Private
private var reloadedIDs: Set<NSManagedObjectID> = []
private var previousStateTag: UUID = .init() {
didSet {
self.reloadedItemIDs = []
self.reloadedSectionIDs = []
}
}
private var reloadedItemIDs: Set<NSManagedObjectID> = []
private var reloadedSectionIDs: Set<String> = []
}
}

View File

@@ -0,0 +1,94 @@
//
// Internals.LazyNonmutating.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: - LazyNonmutating
@propertyWrapper
internal final class LazyNonmutating<Value> {
// MARK: Internal
init(_ initializer: @escaping () -> Value) {
self.initializer = initializer
}
init(uninitialized: Void) {
self.initializer = { fatalError() }
}
func initialize(_ initializer: @escaping () -> Value) {
self.initializer = initializer
}
func reset(_ initializer: @escaping () -> Value) {
self.initializer = initializer
self.initializedValue = nil
}
// MARK: @propertyWrapper
var wrappedValue: Value {
get {
if let initializedValue = self.initializedValue {
return initializedValue
}
let initializedValue = self.initializer()
self.initializedValue = initializedValue
return initializedValue
}
set {
self.initializer = { newValue }
self.initializedValue = newValue
}
}
var projectedValue: Internals.LazyNonmutating<Value> {
return self
}
// MARK: Private
private var initializer: () -> Value
private var initializedValue: Value? = nil
}
}

View File

@@ -34,7 +34,7 @@ extension Internals {
internal final class NotificationObserver {
// MARK: Public
// MARK: Internal
let observer: NSObjectProtocol

View File

@@ -0,0 +1,56 @@
//
// Internals.SharedNotificationObserver.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: - Internal
extension Internals {
// MARK: - SharedNotificationObserver
internal final class SharedNotificationObserver {
// MARK: Internal
let observer: NSObjectProtocol
init(notificationName: Notification.Name, object: Any?, queue: OperationQueue? = nil, closure: @escaping (_ note: Notification) -> Void) {
self.observer = NotificationCenter.default.addObserver(
forName: notificationName,
object: object,
queue: queue,
using: closure
)
}
deinit {
NotificationCenter.default.removeObserver(self.observer)
}
}
}

View File

@@ -34,44 +34,44 @@ import AppKit
#endif
// MARK: - LiveList
// MARK: - ListSnapshot
public struct ListSnapshot<D: DynamicObject>: SnapshotResult, RandomAccessCollection, Hashable {
public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollection, Hashable {
// MARK: Public
public typealias SectionID = String
public typealias ItemID = D.ObjectID
public typealias ItemID = O.ObjectID
public subscript<S: Sequence>(indices indices: S) -> [ObjectType] where S.Element == Index {
public subscript<S: Sequence>(indices indices: S) -> [LiveObject<O>] where S.Element == Index {
let context = self.context!
let objectIDs = self.diffableSnapshot.itemIdentifiers
let itemIDs = self.diffableSnapshot.allItemIDs
return indices.map { position in
let objectID = objectIDs[position]
return context.fetchExisting(objectID)!
let itemID = itemIDs[position]
return LiveObject<O>(id: itemID, context: context)
}
}
public subscript(section sectionID: SectionID) -> [ObjectType] {
public subscript(section sectionID: SectionID) -> [LiveObject<O>] {
let context = self.context!
let objectIDs = self.itemIdentifiers(inSection: sectionID)
return objectIDs.map {
return context.fetchExisting($0)!
let itemIDs = self.diffableSnapshot.itemIDs(inSection: sectionID)
return itemIDs.map {
return LiveObject<O>(id: $0, context: context)
}
}
public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [ObjectType] where S.Element == Int {
public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject<O>] where S.Element == Int {
let context = self.context!
let objectIDs = self.itemIdentifiers(inSection: sectionID)
let itemIDs = self.diffableSnapshot.itemIDs(inSection: sectionID)
return itemIndices.map { position in
let objectID = objectIDs[position]
return context.fetchExisting(objectID)!
let itemID = itemIDs[position]
return LiveObject<O>(id: itemID, context: context)
}
}
@@ -85,60 +85,60 @@ public struct ListSnapshot<D: DynamicObject>: SnapshotResult, RandomAccessCollec
return self.diffableSnapshot.numberOfSections
}
public var sectionIdentifiers: [String] {
public var sectionIDs: [SectionID] {
return self.diffableSnapshot.sectionIdentifiers as [String]
return self.diffableSnapshot.allSectionIDs
}
public var itemIdentifiers: [ItemID] {
return self.diffableSnapshot.itemIdentifiers as [ItemID]
return self.diffableSnapshot.allItemIDs
}
public func numberOfItems(inSection identifier: SectionID) -> Int {
return self.diffableSnapshot.numberOfItems(inSection: identifier as NSString)
return self.diffableSnapshot.numberOfItems(inSection: identifier)
}
public func itemIdentifiers(inSection identifier: SectionID) -> [ItemID] {
return self.diffableSnapshot.itemIdentifiers(inSection: identifier as NSString)
return self.diffableSnapshot.itemIDs(inSection: identifier)
}
public func itemIdentifiers(inSection identifier: SectionID, atIndices indices: IndexSet) -> [ItemID] {
let itemIDs = self.itemIdentifiers(inSection: identifier)
let itemIDs = self.diffableSnapshot.itemIDs(inSection: identifier)
return indices.map({ itemIDs[$0] })
}
public func sectionIdentifier(containingItem identifier: ItemID) -> SectionID? {
return self.diffableSnapshot.sectionIdentifier(containingItem: identifier) as SectionID?
return self.diffableSnapshot.sectionIDs(containingItem: identifier)
}
public func indexOfItem(_ identifier: ItemID) -> Index? {
return self.diffableSnapshot.indexOfItem(identifier)
return self.diffableSnapshot.indexOfItemID(identifier)
}
public func indexOfSection(_ identifier: SectionID) -> Int? {
return self.diffableSnapshot.indexOfSection(identifier as NSString)
return self.diffableSnapshot.indexOfSectionID(identifier)
}
public mutating func appendItems(_ identifiers: [ItemID], toSection sectionIdentifier: SectionID? = nil) {
self.diffableSnapshot.appendItems(identifiers, toSection: sectionIdentifier as NSString?)
self.diffableSnapshot.appendItems(identifiers, toSection: sectionIdentifier, nextStateTag: .init())
}
public mutating func insertItems(_ identifiers: [ItemID], beforeItem beforeIdentifier: ItemID) {
self.diffableSnapshot.insertItems(identifiers, beforeItem: beforeIdentifier)
self.diffableSnapshot.insertItems(identifiers, beforeItem: beforeIdentifier, nextStateTag: .init())
}
public mutating func insertItems(_ identifiers: [ItemID], afterItem afterIdentifier: ItemID) {
self.diffableSnapshot.insertItems(identifiers, afterItem: afterIdentifier)
self.diffableSnapshot.insertItems(identifiers, afterItem: afterIdentifier, nextStateTag: .init())
}
public mutating func deleteItems(_ identifiers: [ItemID]) {
@@ -163,73 +163,73 @@ public struct ListSnapshot<D: DynamicObject>: SnapshotResult, RandomAccessCollec
public mutating func reloadItems(_ identifiers: [ItemID]) {
self.diffableSnapshot.reloadItems(identifiers)
self.diffableSnapshot.reloadItems(identifiers, nextStateTag: .init())
}
public mutating func appendSections(_ identifiers: [SectionID]) {
self.diffableSnapshot.appendSections(identifiers as [NSString])
self.diffableSnapshot.appendSections(identifiers, nextStateTag: .init())
}
public mutating func insertSections(_ identifiers: [SectionID], beforeSection toIdentifier: SectionID) {
self.diffableSnapshot.insertSections(identifiers as [NSString], beforeSection: toIdentifier as NSString)
self.diffableSnapshot.insertSections(identifiers, beforeSection: toIdentifier, nextStateTag: .init())
}
public mutating func insertSections(_ identifiers: [SectionID], afterSection toIdentifier: SectionID) {
self.diffableSnapshot.insertSections(identifiers as [NSString], afterSection: toIdentifier as NSString)
self.diffableSnapshot.insertSections(identifiers, afterSection: toIdentifier, nextStateTag: .init())
}
public mutating func deleteSections(_ identifiers: [SectionID]) {
self.diffableSnapshot.deleteSections(identifiers as [NSString])
self.diffableSnapshot.deleteSections(identifiers)
}
public mutating func moveSection(_ identifier: SectionID, beforeSection toIdentifier: SectionID) {
self.diffableSnapshot.moveSection(identifier as NSString, beforeSection: toIdentifier as NSString)
self.diffableSnapshot.moveSection(identifier, beforeSection: toIdentifier)
}
public mutating func moveSection(_ identifier: SectionID, afterSection toIdentifier: SectionID) {
self.diffableSnapshot.moveSection(identifier as NSString, afterSection: toIdentifier as NSString)
self.diffableSnapshot.moveSection(identifier, afterSection: toIdentifier)
}
public mutating func reloadSections(_ identifiers: [SectionID]) {
self.diffableSnapshot.reloadSections(identifiers as [NSString])
self.diffableSnapshot.reloadSections(identifiers, nextStateTag: .init())
}
// MARK: SnapshotResult
public typealias ObjectType = D
public typealias ObjectType = O
// MARK: RandomAccessCollection
public var startIndex: Index {
return self.diffableSnapshot.itemIdentifiers.startIndex
return self.diffableSnapshot.allItemIDs.startIndex
}
public var endIndex: Index {
return self.diffableSnapshot.itemIdentifiers.endIndex
return self.diffableSnapshot.allItemIDs.endIndex
}
public subscript(position: Index) -> ObjectType {
public subscript(position: Index) -> Element {
let context = self.context!
let objectID = self.diffableSnapshot.itemIdentifiers[position]
return context.fetchExisting(objectID)!
let itemID = self.diffableSnapshot.allItemIDs[position]
return LiveObject<O>(id: itemID, context: context)
}
// MARK: Sequence
public typealias Element = ObjectType
public typealias Element = LiveObject<O>
public typealias Index = Int
@@ -254,7 +254,7 @@ public struct ListSnapshot<D: DynamicObject>: SnapshotResult, RandomAccessCollec
internal init() {
self.diffableSnapshot = Internals.FallbackDiffableDataSourceSnapshot()
self.diffableSnapshot = .init()
self.context = nil
}
@@ -263,11 +263,22 @@ public struct ListSnapshot<D: DynamicObject>: SnapshotResult, RandomAccessCollec
self.diffableSnapshot = diffableSnapshot
self.context = context
}
internal var nextStateTag: UUID {
return self.diffableSnapshot.nextStateTag
}
internal func itemIDs(where stateCondition: @escaping (UUID) -> Bool) -> [ItemID] {
return self.diffableSnapshot.itemIDs(where: stateCondition)
}
// MARK: Private
private let id: UUID = .init()
private let context: NSManagedObjectContext?
private var diffableSnapshot: Internals.DiffableDataSourceSnapshot
}

View File

@@ -38,40 +38,135 @@ import SwiftUI
// MARK: - LiveList
public final class LiveList<D: DynamicObject> {
public final class LiveList<O: DynamicObject>: Hashable {
// MARK: Public
public fileprivate(set) var snapshot: ListSnapshot<ObjectType> = .init() {
public typealias SectionID = SnapshotType.SectionID
public typealias ItemID = SnapshotType.ItemID
public fileprivate(set) var snapshot: SnapshotType = .init() {
willSet {
guard #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) else {
return
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) {
self.willChange()
}
#if canImport(Combine)
#if canImport(SwiftUI)
withAnimation {
self.objectWillChange.send()
}
#else
self.objectWillChange.send()
#endif
#endif
}
}
public var numberOfItems: Int {
return self.snapshot.numberOfItems
}
public var numberOfSections: Int {
return self.snapshot.numberOfSections
}
public var sections: [SectionID] {
return self.snapshot.sectionIDs
}
public subscript(section sectionID: SectionID) -> [LiveObject<O>] {
let context = self.context
return self.snapshot
.itemIdentifiers(inSection: sectionID)
.map({ context.liveObject(id: $0) })
}
public subscript(itemID itemID: ItemID) -> LiveObject<O>? {
guard let validID = self.snapshot.itemIdentifiers.first(where: { $0 == itemID }) else {
return nil
}
return self.context.liveObject(id: validID)
}
public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject<O>] where S.Element == Int {
let context = self.context
let itemIDs = self.snapshot.itemIdentifiers(inSection: sectionID)
return itemIndices.map { position in
let itemID = itemIDs[position]
return context.liveObject(id: itemID)
}
}
public var items: [LiveObject<O>] {
let context = self.context
return self.snapshot.itemIdentifiers
.map({ context.liveObject(id: $0) })
}
public func numberOfItems(inSection identifier: SectionID) -> Int {
return self.snapshot.numberOfItems(inSection: identifier)
}
public func items(inSection identifier: SectionID) -> [LiveObject<O>] {
let context = self.context
return self.snapshot
.itemIdentifiers(inSection: identifier)
.map({ context.liveObject(id: $0) })
}
public func items(inSection identifier: SectionID, atIndices indices: IndexSet) -> [LiveObject<O>] {
let context = self.context
let itemIDs = self.snapshot.itemIdentifiers(inSection: identifier)
return indices.map { position in
let itemID = itemIDs[position]
return context.liveObject(id: itemID)
}
}
public func section(containingItem item: LiveObject<O>) -> SectionID? {
return self.snapshot.sectionIdentifier(containingItem: item.id)
}
public func indexOfItem(_ item: LiveObject<O>) -> Int? {
return self.snapshot.indexOfItem(item.id)
}
public func indexOfSection(_ identifier: SectionID) -> Int? {
return self.snapshot.indexOfSection(identifier)
}
// MARK: Equatable
public static func == (_ lhs: LiveList, _ rhs: LiveList) -> Bool {
return lhs === rhs
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
// MARK: LiveResult
public typealias ObjectType = D
public typealias ObjectType = O
public typealias SnapshotType = ListSnapshot<D>
public typealias SnapshotType = ListSnapshot<O>
// MARK: Internal
@@ -137,6 +232,8 @@ public final class LiveList<D: DynamicObject> {
private let from: From<ObjectType>
private let sectionBy: SectionBy<ObjectType>?
private lazy var context: NSManagedObjectContext = self.fetchedResultsController.managedObjectContext
private static func recreateFetchedResultsController(context: NSManagedObjectContext, from: From<ObjectType>, sectionBy: SectionBy<ObjectType>?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void) -> (controller: Internals.CoreStoreFetchedResultsController, delegate: Internals.FetchedDiffableDataSourceSnapshotDelegate) {
let fetchRequest = Internals.CoreStoreFetchRequest<NSManagedObject>()
@@ -197,76 +294,7 @@ public final class LiveList<D: DynamicObject> {
self.applyFetchClauses = applyFetchClauses
self.fetchedResultsControllerDelegate.handler = self
guard let coordinator = context.parentStack?.coordinator else {
return
}
self.observerForWillChangePersistentStore = Internals.NotificationObserver(
notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresWillChange,
object: coordinator,
queue: OperationQueue.main,
closure: { [weak self] (note) -> Void in
guard let `self` = self else {
return
}
// self.isPersistentStoreChanging = true
//
// guard let removedStores = (note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore]).flatMap(Set.init),
// !Set(self.fetchedResultsController.typedFetchRequest.safeAffectedStores() ?? []).intersection(removedStores).isEmpty else {
//
// return
// }
// self.refetch(self.applyFetchClauses)
}
)
self.observerForDidChangePersistentStore = Internals.NotificationObserver(
notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresDidChange,
object: coordinator,
queue: OperationQueue.main,
closure: { [weak self] (note) -> Void in
guard let `self` = self else {
return
}
// if !self.isPendingRefetch {
//
// let previousStores = Set(self.fetchedResultsController.typedFetchRequest.safeAffectedStores() ?? [])
// let currentStores = previousStores
// .subtracting(note.userInfo?[NSRemovedPersistentStoresKey] as? [NSPersistentStore] ?? [])
// .union(note.userInfo?[NSAddedPersistentStoresKey] as? [NSPersistentStore] ?? [])
//
// if previousStores != currentStores {
//
// self.refetch(self.applyFetchClauses)
// }
// }
//
// self.isPersistentStoreChanging = false
}
)
if let createAsynchronously = createAsynchronously {
// transactionQueue.async {
//
// try!internal self.fetchedResultsController.performFetchFromSpecifiedStores()
// self.taskGroup.notify(queue: .main) {
//
// createAsynchronously(self)
// }
// }
}
else {
try! self.fetchedResultsController.performFetchFromSpecifiedStores()
}
try! self.fetchedResultsController.performFetchFromSpecifiedStores()
}
}
@@ -301,6 +329,29 @@ extension LiveList: LiveResult {
return self.rawObjectWillChange! as! ObservableObjectPublisher
}
public func willChange() {
#if canImport(Combine)
#if canImport(SwiftUI)
withAnimation {
self.objectWillChange.send()
}
#else
self.objectWillChange.send()
#endif
#endif
}
public func didChange() {
// TODO:
}
}
#endif

285
Sources/LiveObject.swift Normal file
View File

@@ -0,0 +1,285 @@
//
// LiveObject.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 CoreData
#if canImport(Combine)
import Combine
#endif
#if canImport(SwiftUI)
import SwiftUI
#endif
// MARK: - LiveObject
@dynamicMemberLookup
public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
// MARK: Public
public typealias SectionID = String
public typealias ItemID = O.ObjectID
public var snapshot: SnapshotType {
return self.lazySnapshot
}
public private(set) lazy var object: O = self.context.fetchExisting(self.id)!
// MARK: Identifiable
public let id: O.ObjectID
// MARK: Equatable
public static func == (_ lhs: LiveObject, _ rhs: LiveObject) -> Bool {
return lhs.id == rhs.id
&& lhs.context == rhs.context
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
hasher.combine(self.context)
}
// MARK: LiveResult
public typealias ObjectType = O
public typealias SnapshotType = ObjectSnapshot<O>
// MARK: Internal
internal convenience init(id: ID, context: NSManagedObjectContext) {
self.init(id: id, context: context, initializer: ObjectSnapshot<O>.init(id:context:))
}
// MARK: FilePrivate
fileprivate let rawObjectWillChange: Any?
fileprivate init(id: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot<O>) {
self.id = id
self.context = context
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) {
#if canImport(Combine)
self.rawObjectWillChange = ObservableObjectPublisher()
#else
self.rawObjectWillChange = nil
#endif
}
else {
self.rawObjectWillChange = nil
}
self.observer = NotificationCenter.default.addObserver(
forName: .NSManagedObjectContextObjectsDidChange,
object: context,
queue: .main,
using: { [weak self] (notification) in
guard let self = self, let userInfo = notification.userInfo else {
return
}
let updatedObjects = (userInfo[NSUpdatedObjectsKey] as! NSSet? ?? [])
let mergedObjects = (userInfo[NSRefreshedObjectsKey] as! NSSet? ?? [])
guard mergedObjects.contains(where: { ($0 as! NSManagedObject).objectID == id })
|| updatedObjects.contains(where: { ($0 as! NSManagedObject).objectID == id }) else {
return
}
self.$lazySnapshot.reset({ initializer(id, context) })
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *) {
self.willChange()
}
}
)
self.$lazySnapshot.initialize({ initializer(id, context) })
}
// MARK: Private
private let context: NSManagedObjectContext
private var observer: NSObjectProtocol?
@Internals.LazyNonmutating(uninitialized: ())
private var lazySnapshot: ObjectSnapshot<O>
}
#if canImport(Combine)
import Combine
// MARK: - LiveObject: LiveResult
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 15.0, *)
extension LiveObject: LiveResult {
// MARK: ObservableObject
public var objectWillChange: ObservableObjectPublisher {
return self.rawObjectWillChange! as! ObservableObjectPublisher
}
public func willChange() {
#if canImport(Combine)
#if canImport(SwiftUI)
withAnimation {
self.objectWillChange.send()
}
#else
self.objectWillChange.send()
#endif
#endif
}
public func didChange() {
// TODO:
}
}
#endif
// MARK: - LiveObject where O: NSManagedObject
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
extension LiveObject where O: NSManagedObject {
// MARK: Public
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: AllowedObjectiveCKeyPathValue>(dynamicMember member: KeyPath<O, V>) -> V {
fatalError()
// return self.snapshot[dynamicMember: member]
}
}
// MARK: - LiveObject where O: CoreStoreObject
extension LiveObject where O: CoreStoreObject {
// MARK: Public
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Required<V>>) -> V {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Optional<V>>) -> V? {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Required<V>>) -> V {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Optional<V>>) -> V? {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToOne<D>>) -> D? {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [D] {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<D> {
return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<T>(dynamicMember member: KeyPath<O, T>) -> T {
return self.object[keyPath: member]
}
}

View File

@@ -41,6 +41,9 @@ public protocol LiveResult: ObservableObject {
associatedtype ObjectType
associatedtype SnapshotType: SnapshotResult where SnapshotType.ObjectType == Self.ObjectType
func willChange()
func didChange()
}
#endif

View File

@@ -38,15 +38,7 @@ extension NSManagedObject {
return nil
}
if context.isTransactionContext {
return context.parentTransaction?.isRunningInAllowedQueue()
}
if context.isDataStackContext {
return Thread.isMainThread
}
return nil
return context.isRunningInAllowedQueue()
}
@nonobjc
@@ -56,14 +48,6 @@ extension NSManagedObject {
return nil
}
if context.isTransactionContext {
return true
}
if context.isDataStackContext {
return false
}
return nil
return context.isEditableInContext()
}
}

View File

@@ -85,6 +85,35 @@ extension NSManagedObjectContext {
}
)
}
@nonobjc
internal func liveObject<D: DynamicObject>(id: NSManagedObjectID) -> LiveObject<D> {
let cache = self.liveObjectsCache(D.self)
return Internals.with {
if let liveObject = cache.object(forKey: id) {
return liveObject
}
let liveObject = LiveObject<D>(id: id, context: self)
cache.setObject(liveObject, forKey: id)
return liveObject
}
}
@nonobjc
private func liveObjectsCache<D: DynamicObject>(_ objectType: D.Type) -> NSMapTable<NSManagedObjectID, LiveObject<D>> {
let key = Internals.typeName(objectType)
if let cache = self.userInfo[key] {
return cache as! NSMapTable<NSManagedObjectID, LiveObject<D>>
}
let cache = NSMapTable<NSManagedObjectID, LiveObject<D>>.strongToWeakObjects()
self.userInfo[key] = cache
return cache
}
// MARK: Private

View File

@@ -0,0 +1,61 @@
//
// NSManagedObjectContext+Logging.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
import CoreData
// MARK: - NSManagedObjectContext
extension NSManagedObjectContext {
@nonobjc
internal func isRunningInAllowedQueue() -> Bool? {
if self.isTransactionContext {
return self.parentTransaction?.isRunningInAllowedQueue()
}
if self.isDataStackContext {
return Thread.isMainThread
}
return nil
}
@nonobjc
internal func isEditableInContext() -> Bool? {
if self.isTransactionContext {
return true
}
if self.isDataStackContext {
return false
}
return nil
}
}

View File

@@ -24,58 +24,78 @@
//
import CoreData
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
// MARK: - ObjectSnapshot
/**
An `ObjectSnapshot` contains "snapshot" values from a `DynamicObject` instance copied at a specific point in time.
*/
@dynamicMemberLookup
public struct ObjectSnapshot<O: DynamicObject> {
// MARK: FilePrivate
fileprivate var attributes: [KeyPathString: Any]
// MARK: Private
private init() {
self.attributes = [:]
public struct ObjectSnapshot<O: DynamicObject>: SnapshotResult, Identifiable, Hashable {
// MARK: SnapshotResult
public typealias ObjectType = O
// MARK: Identifiable
public let id: O.ObjectID
// MARK: Equatable
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
return lhs.id == rhs.id
&& lhs.values == rhs.values
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
hasher.combine(self.values)
}
// MARK: Internal
internal init(id: ID, context: NSManagedObjectContext) {
self.id = id
self.context = context
self.values = O.cs_snapshotDictionary(id: id, context: context) as NSDictionary
}
// MARK: Private
private let context: NSManagedObjectContext
private let values: NSDictionary
}
// MARK: - ObjectSnapshot where O: NSManagedObject
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
extension ObjectSnapshot where O: NSManagedObject {
/**
Initializes an `ObjectSnapshot` instance by copying all attribute values from the given `NSManagedObject`.
*/
public init(from object: O) {
self.attributes = object.dictionaryWithValues(
forKeys: Array(object.entity.attributesByName.keys)
)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: AllowedObjectiveCKeyPathValue>(dynamicMember member: KeyPath<O, V>) -> V {
get {
let key = String(keyPath: member)
return self.attributes[key]! as! V
}
set {
let key = String(keyPath: member)
self.attributes[key] = newValue
}
let key = String(keyPath: member)
return self.values[key] as! V
}
}
@@ -83,61 +103,67 @@ extension ObjectSnapshot where O: NSManagedObject {
// MARK: - ObjectSnapshot where O: CoreStoreObject
extension ObjectSnapshot where O: CoreStoreObject {
/**
Initializes an `ObjectSnapshot` instance by copying all attribute values from the given `CoreStoreObject`.
*/
public init(from object: O) {
var attributes: [KeyPathString: Any] = [:]
Self.initializeAttributes(
mirror: Mirror(reflecting: object),
object: object,
into: &attributes
)
self.attributes = attributes
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<K: AttributeKeyPathStringConvertible>(dynamicMember member: KeyPath<O, K>) -> K.ReturnValueType {
get {
let key = String(keyPath: member)
return self.attributes[key]! as! K.ReturnValueType
}
set {
let key = String(keyPath: member)
self.attributes[key] = newValue
}
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Required<V>>) -> V {
let key = String(keyPath: member)
return self.values[key] as! V
}
// MARK: Private
private static func initializeAttributes(mirror: Mirror, object: CoreStoreObject, into attributes: inout [KeyPathString: Any]) {
if let superClassMirror = mirror.superclassMirror {
self.initializeAttributes(
mirror: superClassMirror,
object: object,
into: &attributes
)
}
for child in mirror.children {
switch child.value {
case let property as AttributeProtocol:
attributes[property.keyPath] = property.valueForSnapshot
default:
continue
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Optional<V>>) -> V? {
let key = String(keyPath: member)
return self.values[key] as! V?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Required<V>>) -> V {
let key = String(keyPath: member)
return self.values[key] as! V
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Optional<V>>) -> V? {
let key = String(keyPath: member)
return self.values[key] as! V?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToOne<D>>) -> D? {
let key = String(keyPath: member)
return self.values[key] as! D?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [D] {
let key = String(keyPath: member)
return self.values[key] as! [D]
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<D> {
let key = String(keyPath: member)
return self.values[key] as! Set<D>
}
}

View File

@@ -308,6 +308,11 @@ public enum RelationshipContainer<O: CoreStoreObject> {
}
}
}
internal var valueForSnapshot: Any {
return self.value as Any
}
// MARK: Private
@@ -595,6 +600,11 @@ public enum RelationshipContainer<O: CoreStoreObject> {
}
}
}
internal var valueForSnapshot: Any {
return self.value as Any
}
// MARK: Private
@@ -887,6 +897,11 @@ public enum RelationshipContainer<O: CoreStoreObject> {
}
}
}
internal var valueForSnapshot: Any {
return self.value as Any
}
// MARK: Private

View File

@@ -42,4 +42,5 @@ internal protocol RelationshipProtocol: AnyObject {
var renamingIdentifier: () -> String? { get }
var minCount: Int { get }
var maxCount: Int { get }
var valueForSnapshot: Any { get }
}

View File

@@ -269,7 +269,8 @@ public enum TransformableContainer<O: CoreStoreObject> {
}
internal var valueForSnapshot: Any {
return self.value
return self.value as Any
}
@@ -486,6 +487,7 @@ public enum TransformableContainer<O: CoreStoreObject> {
}
internal var valueForSnapshot: Any {
return self.value as Any
}

View File

@@ -264,7 +264,8 @@ public enum ValueContainer<O: CoreStoreObject> {
}
internal var valueForSnapshot: Any {
return self.value
return self.value as Any
}
@@ -481,6 +482,7 @@ public enum ValueContainer<O: CoreStoreObject> {
}
internal var valueForSnapshot: Any {
return self.value as Any
}