From 6b64eb7650b68eea9792faa908bc30fb74b13c2a Mon Sep 17 00:00:00 2001 From: John Estropia Date: Mon, 14 Oct 2019 21:36:03 +0900 Subject: [PATCH] WIP: ObjectRepresentable utilities --- .../ListObserverDemoViewController.swift | 11 +-- .../ObjectObserverDemoViewController.swift | 25 ++---- Sources/AsynchronousDataTransaction.swift | 48 ++++------ Sources/BaseDataTransaction.swift | 41 ++++----- Sources/CoreStore+Observing.swift | 6 +- Sources/DataStack+Observing.swift | 6 +- Sources/DynamicObject.swift | 38 ++++---- ...edDiffableDataSourceSnapshotDelegate.swift | 22 ++++- ...Internals.SharedNotificationObserver.swift | 5 ++ Sources/ListSnapshot.swift | 8 +- Sources/LiveList.swift | 30 +++---- Sources/LiveObject.swift | 89 ++++++++++++++++--- .../NSManagedObjectContext+CoreStore.swift | 8 +- Sources/ObjectMonitor.swift | 48 +++++++--- Sources/ObjectRepresentation.swift | 74 ++++++++++++++- Sources/SynchronousDataTransaction.swift | 48 ++++------ Sources/UnsafeDataTransaction+Observing.swift | 6 +- 17 files changed, 315 insertions(+), 198 deletions(-) diff --git a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ListObserverDemoViewController.swift b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ListObserverDemoViewController.swift index cb08957..2a5e1b6 100644 --- a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ListObserverDemoViewController.swift +++ b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ListObserverDemoViewController.swift @@ -136,14 +136,15 @@ class ListObserverDemoViewController: UITableViewController { return cell } ) - ColorsDemo.palettes.addObserver(self) { [weak self] (liveList, snapshot) in + ColorsDemo.palettes.addObserver(self) { [weak self] (liveList) in guard let self = self else { return } - self.dataSource?.apply(snapshot, animatingDifferences: true) + self.dataSource?.apply(liveList.snapshot, animatingDifferences: true) } + self.dataSource?.apply(ColorsDemo.palettes.snapshot, animatingDifferences: false) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { @@ -152,8 +153,8 @@ class ListObserverDemoViewController: UITableViewController { switch (segue.identifier, segue.destination, sender) { - case ("ObjectObserverDemoViewController"?, let destinationViewController as ObjectObserverDemoViewController, let palette as Palette): - destinationViewController.palette = palette + case ("ObjectObserverDemoViewController"?, let destinationViewController as ObjectObserverDemoViewController, let palette as LiveObject): + destinationViewController.setPalette(palette) default: break @@ -169,7 +170,7 @@ class ListObserverDemoViewController: UITableViewController { self.performSegue( withIdentifier: "ObjectObserverDemoViewController", - sender: ColorsDemo.palettes[indexPath: indexPath]?.object + sender: ColorsDemo.palettes[indexPath: indexPath] ) } diff --git a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ObjectObserverDemoViewController.swift b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ObjectObserverDemoViewController.swift index c7c006c..6f0d74b 100644 --- a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ObjectObserverDemoViewController.swift +++ b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/ObjectObserverDemoViewController.swift @@ -14,30 +14,23 @@ import CoreStore class ObjectObserverDemoViewController: UIViewController, ObjectObserver { - var palette: Palette? { + func setPalette(_ newValue: O?) where O.ObjectType == Palette { - get { + guard self.monitor?.cs_id() != newValue?.cs_id() else { - return self.monitor?.object + return } - set { + if let newValue = newValue { - guard self.monitor?.object != newValue else { - - return - } + self.monitor = ColorsDemo.stack.monitorObject(newValue) + } + else { - if let palette = newValue { - - self.monitor = ColorsDemo.stack.monitorObject(palette) - } - else { - - self.monitor = nil - } + self.monitor = nil } } + // MARK: NSObject deinit { diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index fd89666..3c926fb 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -108,51 +108,35 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { return super.edit(into, objectID) } - + /** - Deletes a specified `NSManagedObject` or `CoreStoreObject`. - - - parameter object: the `NSManagedObject` or `CoreStoreObject` to be deleted + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by series of `ObjectRepresentation`s. + + - parameter object: the `ObjectRepresentation` representing an `NSManagedObject` or `CoreStoreObject` to be deleted + - parameter objects: other `ObjectRepresentation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public override func delete(_ object: D?) { - - Internals.assert( - !self.isCommitted, - "Attempted to delete an entity of type \(Internals.typeName(object)) from an already committed \(Internals.typeName(self))." - ) - - super.delete(object) - } - - /** - Deletes the specified `DynamicObject`s. - - - parameter object1: the `DynamicObject` to be deleted - - parameter object2: another `DynamicObject` to be deleted - - parameter objects: other `DynamicObject`s to be deleted - */ - public override func delete(_ object1: D?, _ object2: D?, _ objects: D?...) { - + public override func delete(_ object: O?, _ objects: O?...) { + Internals.assert( !self.isCommitted, "Attempted to delete an entities from an already committed \(Internals.typeName(self))." ) - - super.delete(([object1, object2] + objects).compactMap { $0 }) + + super.delete(([object] + objects).compactMap { $0 }) } - + /** - Deletes the specified `DynamicObject`s. - - - parameter objects: the `DynamicObject`s to be deleted + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by an `ObjectRepresenation`. + + - parameter objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public override func delete(_ objects: S) where S.Iterator.Element: DynamicObject { - + public override func delete(_ objects: S) where S.Iterator.Element: ObjectRepresentation { + Internals.assert( !self.isCommitted, "Attempted to delete an entities from an already committed \(Internals.typeName(self))." ) - + super.delete(objects) } diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index 501617a..9353b8c 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -156,47 +156,36 @@ public /*abstract*/ class BaseDataTransaction { } /** - Deletes a specified `NSManagedObject` or `CoreStoreObject`. + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by series of `ObjectRepresentation`s. - - parameter object: the `NSManagedObject` or `CoreStoreObject` to be deleted + - parameter object: the `ObjectRepresentation` representing an `NSManagedObject` or `CoreStoreObject` to be deleted + - parameter objects: other `ObjectRepresentation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public func delete(_ object: D?) { + public func delete(_ object: O?, _ objects: O?...) { Internals.assert( self.isRunningInAllowedQueue(), "Attempted to delete an entity outside its designated queue." ) - let context = self.context - object - .flatMap(context.fetchExisting) - .flatMap({ context.delete($0.cs_toRaw()) }) + self.delete(([object] + objects).compactMap { $0 }) } /** - Deletes the specified `NSManagedObject`s or `CoreStoreObject`s. + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by an `ObjectRepresenation`. - - parameter object1: the `NSManagedObject` or `CoreStoreObject` to be deleted - - parameter object2: another `NSManagedObject` or `CoreStoreObject` to be deleted - - parameter objects: other `NSManagedObject`s or `CoreStoreObject`s to be deleted + - parameter objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public func delete(_ object1: D?, _ object2: D?, _ objects: D?...) { - - self.delete(([object1, object2] + objects).compactMap { $0 }) - } - - /** - Deletes the specified `NSManagedObject`s or `CoreStoreObject`s. - - - parameter objects: the `NSManagedObject`s or `CoreStoreObject`s to be deleted - */ - public func delete(_ objects: S) where S.Iterator.Element: DynamicObject { + public func delete(_ objects: S) where S.Iterator.Element: ObjectRepresentation { Internals.assert( self.isRunningInAllowedQueue(), "Attempted to delete entities outside their designated queue." ) let context = self.context - objects.forEach { context.fetchExisting($0).flatMap({ context.delete($0.cs_toRaw()) }) } + objects.forEach { + + $0.cs_rawObject(in: context).map({ context.delete($0) }) + } } /** @@ -217,10 +206,10 @@ public /*abstract*/ class BaseDataTransaction { /** Returns `true` if the object has any property values changed. This method should not be called after the `commit()` method was called. - - parameter entity: the `DynamicObject` instance + - parameter object: the `DynamicObject` instance - returns: `true` if the object has any property values changed. */ - public func objectHasPersistentChangedValues(_ entity: D) -> Bool { + public func objectHasPersistentChangedValues(_ object: D) -> Bool { Internals.assert( self.isRunningInAllowedQueue(), @@ -230,7 +219,7 @@ public /*abstract*/ class BaseDataTransaction { !self.isCommitted, "Attempted to access inserted objects from an already committed \(Internals.typeName(self))." ) - return entity.cs_toRaw().hasPersistentChangedValues + return object.cs_toRaw().hasPersistentChangedValues } /** diff --git a/Sources/CoreStore+Observing.swift b/Sources/CoreStore+Observing.swift index 9b44ead..79f9188 100644 --- a/Sources/CoreStore+Observing.swift +++ b/Sources/CoreStore+Observing.swift @@ -34,12 +34,12 @@ import CoreData extension CoreStore { /** - Using the `defaultStack`, creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. + Using the `defaultStack`, creates an `ObjectMonitor` for the specified `ObjectRepresentation`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. - - parameter object: the `DynamicObject` to observe changes from + - parameter object: the `ObjectRepresentation` to observe changes from - returns: a `ObjectMonitor` that monitors changes to `object` */ - public static func monitorObject(_ object: D) -> ObjectMonitor { + public static func monitorObject(_ object: O) -> ObjectMonitor { return Shared.defaultStack.monitorObject(object) } diff --git a/Sources/DataStack+Observing.swift b/Sources/DataStack+Observing.swift index b298285..852b135 100644 --- a/Sources/DataStack+Observing.swift +++ b/Sources/DataStack+Observing.swift @@ -33,12 +33,12 @@ import CoreData extension DataStack { /** - Creates an `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. + Creates an `ObjectMonitor` for the specified `ObjectRepresentation`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. - - parameter object: the `DynamicObject` to observe changes from + - parameter object: the `ObjectRepresentation` to observe changes from - returns: a `ObjectMonitor` that monitors changes to `object` */ - public func monitorObject(_ object: D) -> ObjectMonitor { + public func monitorObject(_ object: O) -> ObjectMonitor { Internals.assert( Thread.isMainThread, diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index b0eebc4..c4231f6 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -33,22 +33,21 @@ import CoreData All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`. */ public protocol DynamicObject: AnyObject { - /** The object ID for this instance */ typealias ObjectID = NSManagedObjectID - + /** 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. */ @@ -62,22 +61,17 @@ public protocol DynamicObject: AnyObject { /** Used internally by CoreStore. Do not call directly. */ - func cs_id() -> ObjectID + func cs_toRaw() -> NSManagedObject /** Used internally by CoreStore. Do not call directly. */ - func cs_toRaw() -> NSManagedObject + func cs_id() -> ObjectID } extension DynamicObject { - - // MARK: Internal -// internal static func keyPathBuilder() -> DynamicObjectMeta { -// -// return .init(keyPathString: "SELF") -// } + // MARK: Internal internal func runtimeType() -> Self.Type { @@ -120,15 +114,15 @@ extension NSManagedObject: DynamicObject { return object.isKind(of: self) } - public func cs_id() -> ObjectID { - - return self.objectID - } - public func cs_toRaw() -> NSManagedObject { return self } + + public func cs_id() -> ObjectID { + + return self.objectID + } } @@ -210,13 +204,13 @@ extension CoreStoreObject { return (self as AnyClass).isSubclass(of: type as AnyClass) } - public func cs_id() -> ObjectID { - - return self.rawObject!.objectID - } - public func cs_toRaw() -> NSManagedObject { return self.rawObject! } + + public func cs_id() -> ObjectID { + + return self.rawObject!.objectID + } } diff --git a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift index 30725ca..7098e2d 100644 --- a/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift +++ b/Sources/Internals.FetchedDiffableDataSourceSnapshotDelegate.swift @@ -116,12 +116,16 @@ extension Internals { @objc dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + var snapshot = Internals.DiffableDataSourceSnapshot( + sections: controller.sections ?? [] + ) + snapshot.reloadItems(self.reloadedIDs) + self.handler?.controller( controller, - didChangeContentWith: Internals.DiffableDataSourceSnapshot( - sections: controller.sections ?? [] - ) + didChangeContentWith: snapshot ) + self.reloadedIDs.removeAll() } @objc @@ -132,5 +136,17 @@ extension Internals { sectionIndexTitleForSectionName: sectionName ) } + + @objc + dynamic func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + + let object = anObject as! NSManagedObject + self.reloadedIDs.append(object.objectID) + } + + + // MARK: Private + + private var reloadedIDs: [NSManagedObjectID] = [] } } diff --git a/Sources/Internals.SharedNotificationObserver.swift b/Sources/Internals.SharedNotificationObserver.swift index 5551d1a..7c4d177 100644 --- a/Sources/Internals.SharedNotificationObserver.swift +++ b/Sources/Internals.SharedNotificationObserver.swift @@ -63,6 +63,11 @@ extension Internals { self.observers.setObject(Closure(closure), forKey: observer) } + + internal func removeObserver(_ observer: U) { + + self.observers.removeObject(forKey: observer) + } // MARK: Private diff --git a/Sources/ListSnapshot.swift b/Sources/ListSnapshot.swift index 539dad2..5f84726 100644 --- a/Sources/ListSnapshot.swift +++ b/Sources/ListSnapshot.swift @@ -68,7 +68,7 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec return indices.map { position in let itemID = itemIDs[position] - return LiveObject(id: itemID, context: context) + return LiveObject(objectID: itemID, context: context) } } @@ -78,7 +78,7 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID) return itemIDs.map { - return LiveObject(id: $0, context: context) + return LiveObject(objectID: $0, context: context) } } @@ -89,7 +89,7 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec return itemIndices.map { position in let itemID = itemIDs[position] - return LiveObject(id: itemID, context: context) + return LiveObject(objectID: itemID, context: context) } } @@ -241,7 +241,7 @@ public struct ListSnapshot: SnapshotResult, RandomAccessCollec let context = self.context! let itemID = self.diffableSnapshot.itemIdentifiers[position] - return LiveObject(id: itemID, context: context) + return LiveObject(objectID: itemID, context: context) } diff --git a/Sources/LiveList.swift b/Sources/LiveList.swift index 992e61b..f1537b2 100644 --- a/Sources/LiveList.swift +++ b/Sources/LiveList.swift @@ -50,7 +50,7 @@ public final class LiveList: Hashable { let context = self.context return self.snapshot .itemIdentifiers(inSection: sectionID) - .map({ context.liveObject(id: $0) }) + .map({ context.liveObject(objectID: $0) }) } public subscript(itemID itemID: ItemID) -> LiveObject? { @@ -59,7 +59,7 @@ public final class LiveList: Hashable { return nil } - return self.context.liveObject(id: validID) + return self.context.liveObject(objectID: validID) } public subscript(indexPath indexPath: IndexPath) -> LiveObject? { @@ -77,7 +77,7 @@ public final class LiveList: Hashable { return nil } let itemID = itemIdentifiers[indexPath.item] - return self.context.liveObject(id: itemID) + return self.context.liveObject(objectID: itemID) } public subscript(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject] where S.Element == Int { @@ -87,7 +87,7 @@ public final class LiveList: Hashable { return itemIndices.map { position in let itemID = itemIDs[position] - return context.liveObject(id: itemID) + return context.liveObject(objectID: itemID) } } @@ -99,7 +99,7 @@ public final class LiveList: Hashable { } didSet { - self.notifyObservers(self.snapshot) + self.notifyObservers() self.didChange() } } @@ -123,7 +123,7 @@ public final class LiveList: Hashable { let context = self.context return self.snapshot.itemIdentifiers - .map({ context.liveObject(id: $0) }) + .map({ context.liveObject(objectID: $0) }) } public func numberOfItems(inSection identifier: SectionID) -> Int { @@ -136,7 +136,7 @@ public final class LiveList: Hashable { let context = self.context return self.snapshot .itemIdentifiers(inSection: identifier) - .map({ context.liveObject(id: $0) }) + .map({ context.liveObject(objectID: $0) }) } public func items(inSection identifier: SectionID, atIndices indices: IndexSet) -> [LiveObject] { @@ -146,18 +146,18 @@ public final class LiveList: Hashable { return indices.map { position in let itemID = itemIDs[position] - return context.liveObject(id: itemID) + return context.liveObject(objectID: itemID) } } public func section(containingItem item: LiveObject) -> SectionID? { - return self.snapshot.sectionIdentifier(containingItem: item.id) + return self.snapshot.sectionIdentifier(containingItem: item.cs_id()) } public func indexOfItem(_ item: LiveObject) -> Int? { - return self.snapshot.indexOfItem(item.id) + return self.snapshot.indexOfItem(item.cs_id()) } public func indexOfSection(_ identifier: SectionID) -> Int? { @@ -165,14 +165,12 @@ public final class LiveList: Hashable { return self.snapshot.indexOfSection(identifier) } - public func addObserver(_ observer: T, _ callback: @escaping (LiveList, ListSnapshot) -> Void) { + public func addObserver(_ observer: T, _ callback: @escaping (LiveList) -> Void) { self.observers.setObject( Internals.Closure(callback), forKey: observer ) - - callback(self, self.snapshot) } public func removeObserver(_ observer: T) { @@ -272,7 +270,7 @@ public final class LiveList: Hashable { private let from: From private let sectionBy: SectionBy? - private let observers: NSMapTable, ListSnapshot), Void>> = .weakToStrongObjects() + private lazy var observers: NSMapTable, Void>> = .weakToStrongObjects() private lazy var context: NSManagedObjectContext = self.fetchedResultsController.managedObjectContext @@ -338,7 +336,7 @@ public final class LiveList: Hashable { try! self.fetchedResultsController.performFetchFromSpecifiedStores() } - private func notifyObservers(_ snapshot: ListSnapshot) { + private func notifyObservers() { guard let enumerator = self.observers.objectEnumerator() else { @@ -346,7 +344,7 @@ public final class LiveList: Hashable { } for closure in enumerator { - (closure as! Internals.Closure<(LiveList, ListSnapshot), Void>).invoke(with: (self, snapshot)) + (closure as! Internals.Closure, Void>).invoke(with: self) } } } diff --git a/Sources/LiveObject.swift b/Sources/LiveObject.swift index 545c8bc..a21cb8b 100644 --- a/Sources/LiveObject.swift +++ b/Sources/LiveObject.swift @@ -39,7 +39,7 @@ import SwiftUI // MARK: - LiveObject @dynamicMemberLookup -public final class LiveObject: Identifiable, Hashable { +public final class LiveObject: ObjectRepresentation, Hashable { // MARK: Public @@ -51,19 +51,60 @@ public final class LiveObject: Identifiable, Hashable { return self.lazySnapshot } - public private(set) lazy var object: O = self.context.fetchExisting(self.id)! + public private(set) lazy var object: O = self.context.fetchExisting(self.objectID)! + + public func addObserver(_ observer: T, _ callback: @escaping (LiveObject) -> Void) { + + self.observers.setObject( + Internals.Closure(callback), + forKey: observer + ) + } + + public func removeObserver(_ observer: T) { + + self.observers.removeObject(forKey: observer) + } + deinit { - // MARK: Identifiable + self.observers.removeAllObjects() + } + + + // MARK: ObjectRepresentation - public let id: O.ObjectID + public typealias ObjectType = O + + public static func cs_fromRaw(object: NSManagedObject) -> Self { + + return self.init( + objectID: object.objectID, + context: object.managedObjectContext! + ) + } + + public func cs_id() -> O.ObjectID { + + return self.objectID + } + + public func cs_object() -> O? { + + return self.context.fetchExisting(self.objectID) + } + + public func cs_rawObject(in context: NSManagedObjectContext) -> NSManagedObject? { + + return self.object.cs_toRaw() + } // MARK: Equatable public static func == (_ lhs: LiveObject, _ rhs: LiveObject) -> Bool { - return lhs.id == rhs.id + return lhs.objectID == rhs.objectID && lhs.context == rhs.context } @@ -72,23 +113,25 @@ public final class LiveObject: Identifiable, Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(self.objectID) hasher.combine(self.context) } // MARK: LiveResult - public typealias ObjectType = O - public typealias SnapshotType = ObjectSnapshot // MARK: Internal - internal convenience init(id: ID, context: NSManagedObjectContext) { + internal convenience init(objectID: O.ObjectID, context: NSManagedObjectContext) { - self.init(id: id, context: context, initializer: ObjectSnapshot.init(id:context:)) + self.init( + objectID: objectID, + context: context, + initializer: ObjectSnapshot.init(id:context:) + ) } @@ -96,9 +139,9 @@ public final class LiveObject: Identifiable, Hashable { fileprivate let rawObjectWillChange: Any? - fileprivate init(id: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot) { + fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot) { - self.id = id + self.objectID = objectID self.context = context if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { @@ -114,7 +157,7 @@ public final class LiveObject: Identifiable, Hashable { self.rawObjectWillChange = nil } - self.$lazySnapshot.initialize({ initializer(id, context) }) + self.$lazySnapshot.initialize({ initializer(objectID, context) }) context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (objectIDs) in @@ -122,18 +165,36 @@ public final class LiveObject: Identifiable, Hashable { return } - self.$lazySnapshot.reset({ initializer(id, context) }) self.willChange() + self.$lazySnapshot.reset({ initializer(objectID, context) }) + self.notifyObservers() + self.didChange() } } // MARK: Private + private let objectID: O.ObjectID private let context: NSManagedObjectContext @Internals.LazyNonmutating(uninitialized: ()) private var lazySnapshot: ObjectSnapshot + + private lazy var observers: NSMapTable, Void>> = .weakToStrongObjects() + + private func notifyObservers() { + + guard let enumerator = self.observers.objectEnumerator() else { + + return + } + for closure in enumerator { + + (closure as! Internals.Closure, Void>).invoke(with: self) + } + } } diff --git a/Sources/NSManagedObjectContext+CoreStore.swift b/Sources/NSManagedObjectContext+CoreStore.swift index 10524c4..28a54e9 100644 --- a/Sources/NSManagedObjectContext+CoreStore.swift +++ b/Sources/NSManagedObjectContext+CoreStore.swift @@ -87,7 +87,7 @@ extension NSManagedObjectContext { } @nonobjc - internal func liveObject(id: NSManagedObjectID) -> LiveObject { + internal func liveObject(objectID: NSManagedObjectID) -> LiveObject { let cache: NSMapTable> = self.userInfo(for: .liveObjectsCache(D.self)) { @@ -95,12 +95,12 @@ extension NSManagedObjectContext { } return Internals.with { - if let liveObject = cache.object(forKey: id) { + if let liveObject = cache.object(forKey: objectID) { return liveObject } - let liveObject = LiveObject(id: id, context: self) - cache.setObject(liveObject, forKey: id) + let liveObject = LiveObject(objectID: objectID, context: self) + cache.setObject(liveObject, forKey: objectID) return liveObject } } diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index ffa36c4..584b8e9 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -40,12 +40,7 @@ import CoreData Observers registered via `addObserver(_:)` are not retained. `ObjectMonitor` only keeps a `weak` reference to all observers, thus keeping itself free from retain-cycles. */ @available(macOS 10.12, *) -public final class ObjectMonitor: Equatable { - - /** - The type for the object contained by the `ObjectMonitor` - */ - public typealias ObjectType = D +public final class ObjectMonitor: ObjectRepresentation, Equatable { /** Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. @@ -125,6 +120,34 @@ public final class ObjectMonitor: Equatable { public let userInfo = UserInfo() + // MARK: ObjectRepresentation + + public typealias ObjectType = D + + public static func cs_fromRaw(object: NSManagedObject) -> Self { + + return self.init( + context: object.managedObjectContext!, + objectID: object.objectID + ) + } + + public func cs_id() -> ObjectType.ObjectID { + + return self.objectID + } + + public func cs_object() -> D? { + + return self.object + } + + public func cs_rawObject(in context: NSManagedObjectContext) -> NSManagedObject? { + + return context.fetchExisting(self.objectID) + } + + // MARK: Equatable public static func == (lhs: ObjectMonitor, rhs: ObjectMonitor) -> Bool { @@ -158,14 +181,14 @@ public final class ObjectMonitor: Equatable { // MARK: Internal - internal convenience init(dataStack: DataStack, object: ObjectType) { + internal convenience init(dataStack: DataStack, object: O) where O.ObjectType == ObjectType { - self.init(context: dataStack.mainContext, object: object) + self.init(context: dataStack.mainContext, objectID: object.cs_id()) } - internal convenience init(unsafeTransaction: UnsafeDataTransaction, object: ObjectType) { + internal convenience init(unsafeTransaction: UnsafeDataTransaction, object: O) where O.ObjectType == ObjectType { - self.init(context: unsafeTransaction.context, object: object) + self.init(context: unsafeTransaction.context, objectID: object.cs_id()) } internal func registerObserver(_ observer: U, willChangeObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: ObjectType) -> Void, didDeleteObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: ObjectType) -> Void, didUpdateObject: @escaping (_ observer: U, _ monitor: ObjectMonitor, _ object: ObjectType, _ changedPersistentKeys: Set) -> Void) { @@ -250,6 +273,7 @@ public final class ObjectMonitor: Equatable { // MARK: Private + private let objectID: ObjectType.ObjectID private let fetchedResultsController: Internals.CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate private var lastCommittedAttributes = [String: NSObject]() @@ -258,9 +282,8 @@ public final class ObjectMonitor: Equatable { private var didDeleteObjectKey: Void? private var didUpdateObjectKey: Void? - private init(context: NSManagedObjectContext, object: ObjectType) { + private init(context: NSManagedObjectContext, objectID: ObjectType.ObjectID) { - let objectID = object.cs_id() let fetchRequest = Internals.CoreStoreFetchRequest() fetchRequest.entity = objectID.entity fetchRequest.fetchLimit = 0 @@ -278,6 +301,7 @@ public final class ObjectMonitor: Equatable { let fetchedResultsControllerDelegate = Internals.FetchedResultsControllerDelegate() + self.objectID = objectID self.fetchedResultsController = fetchedResultsController self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate diff --git a/Sources/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index d68b8e2..6fd7eb5 100644 --- a/Sources/ObjectRepresentation.swift +++ b/Sources/ObjectRepresentation.swift @@ -2,8 +2,76 @@ // ObjectRepresentation.swift // CoreStore // -// Created by John Estropia on 2019/10/11. -// Copyright © 2019 John Rommel Estropia. All rights reserved. +// 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 - ObjectRepresentation + +/** + An object that acts as interfaces for `CoreStoreObject`s or `NSManagedObject`s + */ +public protocol ObjectRepresentation { + + /** + The object type represented by this protocol + */ + associatedtype ObjectType: DynamicObject + + /** + Used internally by CoreStore. Do not call directly. + */ + static func cs_fromRaw(object: NSManagedObject) -> Self + + /** + Used internally by CoreStore. Do not call directly. + */ + func cs_id() -> ObjectType.ObjectID + + /** + Used internally by CoreStore. Do not call directly. + */ + func cs_object() -> ObjectType? + + /** + Used internally by CoreStore. Do not call directly. + */ + func cs_rawObject(in context: NSManagedObjectContext) -> NSManagedObject? +} + +extension NSManagedObject: ObjectRepresentation {} + +extension CoreStoreObject: ObjectRepresentation {} + +extension DynamicObject where Self: ObjectRepresentation { + + public func cs_object() -> Self? { + + return self + } + + public func cs_rawObject(in context: NSManagedObjectContext) -> NSManagedObject? { + + return context.fetchExisting(self.cs_id()) + } +} diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index c8970de..f456229 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -97,51 +97,35 @@ public final class SynchronousDataTransaction: BaseDataTransaction { return super.edit(into, objectID) } - + /** - Deletes a specified `NSManagedObject` or `CoreStoreObject`. - - - parameter object: the `NSManagedObject` or `CoreStoreObject` type to be deleted + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by series of `ObjectRepresentation`s. + + - parameter object: the `ObjectRepresentation` representing an `NSManagedObject` or `CoreStoreObject` to be deleted + - parameter objects: other `ObjectRepresentation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public override func delete(_ object: D?) { - - Internals.assert( - !self.isCommitted, - "Attempted to delete an entity of type \(Internals.typeName(object)) from an already committed \(Internals.typeName(self))." - ) - - super.delete(object) - } - - /** - Deletes the specified `DynamicObject`s. - - - parameter object1: the `DynamicObject` to be deleted - - parameter object2: another `DynamicObject` to be deleted - - parameter objects: other `DynamicObject`s to be deleted - */ - public override func delete(_ object1: D?, _ object2: D?, _ objects: D?...) { - + public override func delete(_ object: O?, _ objects: O?...) { + Internals.assert( !self.isCommitted, "Attempted to delete an entities from an already committed \(Internals.typeName(self))." ) - - super.delete(([object1, object2] + objects).compactMap { $0 }) + + super.delete(([object] + objects).compactMap { $0 }) } - + /** - Deletes the specified `DynamicObject`s. - - - parameter objects: the `DynamicObject`s to be deleted + Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by an `ObjectRepresenation`. + + - parameter objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted */ - public override func delete(_ objects: S) where S.Iterator.Element: DynamicObject { - + public override func delete(_ objects: S) where S.Iterator.Element: ObjectRepresentation { + Internals.assert( !self.isCommitted, "Attempted to delete an entities from an already committed \(Internals.typeName(self))." ) - + super.delete(objects) } diff --git a/Sources/UnsafeDataTransaction+Observing.swift b/Sources/UnsafeDataTransaction+Observing.swift index e51af42..7cedb7f 100644 --- a/Sources/UnsafeDataTransaction+Observing.swift +++ b/Sources/UnsafeDataTransaction+Observing.swift @@ -33,12 +33,12 @@ import CoreData extension UnsafeDataTransaction { /** - Creates a `ObjectMonitor` for the specified `DynamicObject`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. + Creates a `ObjectMonitor` for the specified `ObjectRepresentation`. Multiple `ObjectObserver`s may then register themselves to be notified when changes are made to the `DynamicObject`. - - parameter object: the `DynamicObject` to observe changes from + - parameter object: the `ObjectRepresentation` to observe changes from - returns: a `ObjectMonitor` that monitors changes to `object` */ - public func monitorObject(_ object: D) -> ObjectMonitor { + public func monitorObject(_ object: O) -> ObjectMonitor { return ObjectMonitor( unsafeTransaction: self,