WIP: ObjectRepresentable utilities

This commit is contained in:
John Estropia
2019-10-14 21:36:03 +09:00
parent f5a165d47d
commit 6b64eb7650
17 changed files with 315 additions and 198 deletions

View File

@@ -136,14 +136,15 @@ class ListObserverDemoViewController: UITableViewController {
return cell return cell
} }
) )
ColorsDemo.palettes.addObserver(self) { [weak self] (liveList, snapshot) in ColorsDemo.palettes.addObserver(self) { [weak self] (liveList) in
guard let self = self else { guard let self = self else {
return 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?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -152,8 +153,8 @@ class ListObserverDemoViewController: UITableViewController {
switch (segue.identifier, segue.destination, sender) { switch (segue.identifier, segue.destination, sender) {
case ("ObjectObserverDemoViewController"?, let destinationViewController as ObjectObserverDemoViewController, let palette as Palette): case ("ObjectObserverDemoViewController"?, let destinationViewController as ObjectObserverDemoViewController, let palette as LiveObject<Palette>):
destinationViewController.palette = palette destinationViewController.setPalette(palette)
default: default:
break break
@@ -169,7 +170,7 @@ class ListObserverDemoViewController: UITableViewController {
self.performSegue( self.performSegue(
withIdentifier: "ObjectObserverDemoViewController", withIdentifier: "ObjectObserverDemoViewController",
sender: ColorsDemo.palettes[indexPath: indexPath]?.object sender: ColorsDemo.palettes[indexPath: indexPath]
) )
} }

View File

@@ -14,30 +14,23 @@ import CoreStore
class ObjectObserverDemoViewController: UIViewController, ObjectObserver { class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
var palette: Palette? { func setPalette<O: ObjectRepresentation>(_ 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 { self.monitor = ColorsDemo.stack.monitorObject(newValue)
}
return else {
}
if let palette = newValue { self.monitor = nil
self.monitor = ColorsDemo.stack.monitorObject(palette)
}
else {
self.monitor = nil
}
} }
} }
// MARK: NSObject // MARK: NSObject
deinit { deinit {

View File

@@ -108,51 +108,35 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
return super.edit(into, objectID) return super.edit(into, objectID)
} }
/** /**
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 override func delete<D: DynamicObject>(_ object: D?) { public override func delete<O: ObjectRepresentation>(_ object: O?, _ objects: O?...) {
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<D: DynamicObject>(_ object1: D?, _ object2: D?, _ objects: D?...) {
Internals.assert( Internals.assert(
!self.isCommitted, !self.isCommitted,
"Attempted to delete an entities from an already committed \(Internals.typeName(self))." "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. Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by an `ObjectRepresenation`.
- parameter objects: the `DynamicObject`s to be deleted - parameter objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted
*/ */
public override func delete<S: Sequence>(_ objects: S) where S.Iterator.Element: DynamicObject { public override func delete<S: Sequence>(_ objects: S) where S.Iterator.Element: ObjectRepresentation {
Internals.assert( Internals.assert(
!self.isCommitted, !self.isCommitted,
"Attempted to delete an entities from an already committed \(Internals.typeName(self))." "Attempted to delete an entities from an already committed \(Internals.typeName(self))."
) )
super.delete(objects) super.delete(objects)
} }

View File

@@ -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<D: DynamicObject>(_ object: D?) { public func delete<O: ObjectRepresentation>(_ object: O?, _ objects: O?...) {
Internals.assert( Internals.assert(
self.isRunningInAllowedQueue(), self.isRunningInAllowedQueue(),
"Attempted to delete an entity outside its designated queue." "Attempted to delete an entity outside its designated queue."
) )
let context = self.context self.delete(([object] + objects).compactMap { $0 })
object
.flatMap(context.fetchExisting)
.flatMap({ context.delete($0.cs_toRaw()) })
} }
/** /**
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 objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted
- parameter object2: another `NSManagedObject` or `CoreStoreObject` to be deleted
- parameter objects: other `NSManagedObject`s or `CoreStoreObject`s to be deleted
*/ */
public func delete<D: DynamicObject>(_ object1: D?, _ object2: D?, _ objects: D?...) { public func delete<S: Sequence>(_ objects: S) where S.Iterator.Element: ObjectRepresentation {
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<S: Sequence>(_ objects: S) where S.Iterator.Element: DynamicObject {
Internals.assert( Internals.assert(
self.isRunningInAllowedQueue(), self.isRunningInAllowedQueue(),
"Attempted to delete entities outside their designated queue." "Attempted to delete entities outside their designated queue."
) )
let context = self.context 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. 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. - returns: `true` if the object has any property values changed.
*/ */
public func objectHasPersistentChangedValues<D: DynamicObject>(_ entity: D) -> Bool { public func objectHasPersistentChangedValues<D: DynamicObject>(_ object: D) -> Bool {
Internals.assert( Internals.assert(
self.isRunningInAllowedQueue(), self.isRunningInAllowedQueue(),
@@ -230,7 +219,7 @@ public /*abstract*/ class BaseDataTransaction {
!self.isCommitted, !self.isCommitted,
"Attempted to access inserted objects from an already committed \(Internals.typeName(self))." "Attempted to access inserted objects from an already committed \(Internals.typeName(self))."
) )
return entity.cs_toRaw().hasPersistentChangedValues return object.cs_toRaw().hasPersistentChangedValues
} }
/** /**

View File

@@ -34,12 +34,12 @@ import CoreData
extension CoreStore { 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` - returns: a `ObjectMonitor` that monitors changes to `object`
*/ */
public static func monitorObject<D>(_ object: D) -> ObjectMonitor<D> { public static func monitorObject<O: ObjectRepresentation>(_ object: O) -> ObjectMonitor<O.ObjectType> {
return Shared.defaultStack.monitorObject(object) return Shared.defaultStack.monitorObject(object)
} }

View File

@@ -33,12 +33,12 @@ import CoreData
extension DataStack { 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` - returns: a `ObjectMonitor` that monitors changes to `object`
*/ */
public func monitorObject<D>(_ object: D) -> ObjectMonitor<D> { public func monitorObject<O: ObjectRepresentation>(_ object: O) -> ObjectMonitor<O.ObjectType> {
Internals.assert( Internals.assert(
Thread.isMainThread, Thread.isMainThread,

View File

@@ -33,22 +33,21 @@ import CoreData
All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`. All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`.
*/ */
public protocol DynamicObject: AnyObject { public protocol DynamicObject: AnyObject {
/** /**
The object ID for this instance The object ID for this instance
*/ */
typealias ObjectID = NSManagedObjectID typealias ObjectID = NSManagedObjectID
/** /**
Used internally by CoreStore. Do not call directly. Used internally by CoreStore. Do not call directly.
*/ */
static func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self static func cs_forceCreate(entityDescription: NSEntityDescription, into context: NSManagedObjectContext, assignTo store: NSPersistentStore) -> Self
/** /**
Used internally by CoreStore. Do not call directly. Used internally by CoreStore. Do not call directly.
*/ */
static func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any] static func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]
/** /**
Used internally by CoreStore. Do not call directly. Used internally by CoreStore. Do not call directly.
*/ */
@@ -62,22 +61,17 @@ public protocol DynamicObject: AnyObject {
/** /**
Used internally by CoreStore. Do not call directly. Used internally by CoreStore. Do not call directly.
*/ */
func cs_id() -> ObjectID func cs_toRaw() -> NSManagedObject
/** /**
Used internally by CoreStore. Do not call directly. Used internally by CoreStore. Do not call directly.
*/ */
func cs_toRaw() -> NSManagedObject func cs_id() -> ObjectID
} }
extension DynamicObject { extension DynamicObject {
// MARK: Internal
// internal static func keyPathBuilder() -> DynamicObjectMeta<Never, Self> { // MARK: Internal
//
// return .init(keyPathString: "SELF")
// }
internal func runtimeType() -> Self.Type { internal func runtimeType() -> Self.Type {
@@ -120,15 +114,15 @@ extension NSManagedObject: DynamicObject {
return object.isKind(of: self) return object.isKind(of: self)
} }
public func cs_id() -> ObjectID {
return self.objectID
}
public func cs_toRaw() -> NSManagedObject { public func cs_toRaw() -> NSManagedObject {
return self 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) return (self as AnyClass).isSubclass(of: type as AnyClass)
} }
public func cs_id() -> ObjectID {
return self.rawObject!.objectID
}
public func cs_toRaw() -> NSManagedObject { public func cs_toRaw() -> NSManagedObject {
return self.rawObject! return self.rawObject!
} }
public func cs_id() -> ObjectID {
return self.rawObject!.objectID
}
} }

View File

@@ -116,12 +116,16 @@ extension Internals {
@objc @objc
dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
var snapshot = Internals.DiffableDataSourceSnapshot(
sections: controller.sections ?? []
)
snapshot.reloadItems(self.reloadedIDs)
self.handler?.controller( self.handler?.controller(
controller, controller,
didChangeContentWith: Internals.DiffableDataSourceSnapshot( didChangeContentWith: snapshot
sections: controller.sections ?? []
)
) )
self.reloadedIDs.removeAll()
} }
@objc @objc
@@ -132,5 +136,17 @@ extension Internals {
sectionIndexTitleForSectionName: sectionName sectionIndexTitleForSectionName: sectionName
) )
} }
@objc
dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, 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] = []
} }
} }

View File

@@ -63,6 +63,11 @@ extension Internals {
self.observers.setObject(Closure<T, Void>(closure), forKey: observer) self.observers.setObject(Closure<T, Void>(closure), forKey: observer)
} }
internal func removeObserver<U: AnyObject>(_ observer: U) {
self.observers.removeObject(forKey: observer)
}
// MARK: Private // MARK: Private

View File

@@ -68,7 +68,7 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
return indices.map { position in return indices.map { position in
let itemID = itemIDs[position] let itemID = itemIDs[position]
return LiveObject<O>(id: itemID, context: context) return LiveObject<O>(objectID: itemID, context: context)
} }
} }
@@ -78,7 +78,7 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID) let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID)
return itemIDs.map { return itemIDs.map {
return LiveObject<O>(id: $0, context: context) return LiveObject<O>(objectID: $0, context: context)
} }
} }
@@ -89,7 +89,7 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
return itemIndices.map { position in return itemIndices.map { position in
let itemID = itemIDs[position] let itemID = itemIDs[position]
return LiveObject<O>(id: itemID, context: context) return LiveObject<O>(objectID: itemID, context: context)
} }
} }
@@ -241,7 +241,7 @@ public struct ListSnapshot<O: DynamicObject>: SnapshotResult, RandomAccessCollec
let context = self.context! let context = self.context!
let itemID = self.diffableSnapshot.itemIdentifiers[position] let itemID = self.diffableSnapshot.itemIdentifiers[position]
return LiveObject<O>(id: itemID, context: context) return LiveObject<O>(objectID: itemID, context: context)
} }

View File

@@ -50,7 +50,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
let context = self.context let context = self.context
return self.snapshot return self.snapshot
.itemIdentifiers(inSection: sectionID) .itemIdentifiers(inSection: sectionID)
.map({ context.liveObject(id: $0) }) .map({ context.liveObject(objectID: $0) })
} }
public subscript(itemID itemID: ItemID) -> LiveObject<O>? { public subscript(itemID itemID: ItemID) -> LiveObject<O>? {
@@ -59,7 +59,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
return nil return nil
} }
return self.context.liveObject(id: validID) return self.context.liveObject(objectID: validID)
} }
public subscript(indexPath indexPath: IndexPath) -> LiveObject<O>? { public subscript(indexPath indexPath: IndexPath) -> LiveObject<O>? {
@@ -77,7 +77,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
return nil return nil
} }
let itemID = itemIdentifiers[indexPath.item] let itemID = itemIdentifiers[indexPath.item]
return self.context.liveObject(id: itemID) return self.context.liveObject(objectID: itemID)
} }
public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject<O>] where S.Element == Int { public subscript<S: Sequence>(section sectionID: SectionID, itemIndices itemIndices: S) -> [LiveObject<O>] where S.Element == Int {
@@ -87,7 +87,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
return itemIndices.map { position in return itemIndices.map { position in
let itemID = itemIDs[position] let itemID = itemIDs[position]
return context.liveObject(id: itemID) return context.liveObject(objectID: itemID)
} }
} }
@@ -99,7 +99,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
} }
didSet { didSet {
self.notifyObservers(self.snapshot) self.notifyObservers()
self.didChange() self.didChange()
} }
} }
@@ -123,7 +123,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
let context = self.context let context = self.context
return self.snapshot.itemIdentifiers return self.snapshot.itemIdentifiers
.map({ context.liveObject(id: $0) }) .map({ context.liveObject(objectID: $0) })
} }
public func numberOfItems(inSection identifier: SectionID) -> Int { public func numberOfItems(inSection identifier: SectionID) -> Int {
@@ -136,7 +136,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
let context = self.context let context = self.context
return self.snapshot return self.snapshot
.itemIdentifiers(inSection: identifier) .itemIdentifiers(inSection: identifier)
.map({ context.liveObject(id: $0) }) .map({ context.liveObject(objectID: $0) })
} }
public func items(inSection identifier: SectionID, atIndices indices: IndexSet) -> [LiveObject<O>] { public func items(inSection identifier: SectionID, atIndices indices: IndexSet) -> [LiveObject<O>] {
@@ -146,18 +146,18 @@ public final class LiveList<O: DynamicObject>: Hashable {
return indices.map { position in return indices.map { position in
let itemID = itemIDs[position] let itemID = itemIDs[position]
return context.liveObject(id: itemID) return context.liveObject(objectID: itemID)
} }
} }
public func section(containingItem item: LiveObject<O>) -> SectionID? { public func section(containingItem item: LiveObject<O>) -> SectionID? {
return self.snapshot.sectionIdentifier(containingItem: item.id) return self.snapshot.sectionIdentifier(containingItem: item.cs_id())
} }
public func indexOfItem(_ item: LiveObject<O>) -> Int? { public func indexOfItem(_ item: LiveObject<O>) -> Int? {
return self.snapshot.indexOfItem(item.id) return self.snapshot.indexOfItem(item.cs_id())
} }
public func indexOfSection(_ identifier: SectionID) -> Int? { public func indexOfSection(_ identifier: SectionID) -> Int? {
@@ -165,14 +165,12 @@ public final class LiveList<O: DynamicObject>: Hashable {
return self.snapshot.indexOfSection(identifier) return self.snapshot.indexOfSection(identifier)
} }
public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (LiveList<O>, ListSnapshot<O>) -> Void) { public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (LiveList<O>) -> Void) {
self.observers.setObject( self.observers.setObject(
Internals.Closure(callback), Internals.Closure(callback),
forKey: observer forKey: observer
) )
callback(self, self.snapshot)
} }
public func removeObserver<T: AnyObject>(_ observer: T) { public func removeObserver<T: AnyObject>(_ observer: T) {
@@ -272,7 +270,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
private let from: From<ObjectType> private let from: From<ObjectType>
private let sectionBy: SectionBy<ObjectType>? private let sectionBy: SectionBy<ObjectType>?
private let observers: NSMapTable<AnyObject, Internals.Closure<(LiveList<O>, ListSnapshot<O>), Void>> = .weakToStrongObjects() private lazy var observers: NSMapTable<AnyObject, Internals.Closure<LiveList<O>, Void>> = .weakToStrongObjects()
private lazy var context: NSManagedObjectContext = self.fetchedResultsController.managedObjectContext private lazy var context: NSManagedObjectContext = self.fetchedResultsController.managedObjectContext
@@ -338,7 +336,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
try! self.fetchedResultsController.performFetchFromSpecifiedStores() try! self.fetchedResultsController.performFetchFromSpecifiedStores()
} }
private func notifyObservers(_ snapshot: ListSnapshot<O>) { private func notifyObservers() {
guard let enumerator = self.observers.objectEnumerator() else { guard let enumerator = self.observers.objectEnumerator() else {
@@ -346,7 +344,7 @@ public final class LiveList<O: DynamicObject>: Hashable {
} }
for closure in enumerator { for closure in enumerator {
(closure as! Internals.Closure<(LiveList<O>, ListSnapshot<O>), Void>).invoke(with: (self, snapshot)) (closure as! Internals.Closure<LiveList<O>, Void>).invoke(with: self)
} }
} }
} }

View File

@@ -39,7 +39,7 @@ import SwiftUI
// MARK: - LiveObject // MARK: - LiveObject
@dynamicMemberLookup @dynamicMemberLookup
public final class LiveObject<O: DynamicObject>: Identifiable, Hashable { public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable {
// MARK: Public // MARK: Public
@@ -51,19 +51,60 @@ public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
return self.lazySnapshot 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<T: AnyObject>(_ observer: T, _ callback: @escaping (LiveObject<O>) -> Void) {
self.observers.setObject(
Internals.Closure(callback),
forKey: observer
)
}
public func removeObserver<T: AnyObject>(_ 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 // MARK: Equatable
public static func == (_ lhs: LiveObject, _ rhs: LiveObject) -> Bool { public static func == (_ lhs: LiveObject, _ rhs: LiveObject) -> Bool {
return lhs.id == rhs.id return lhs.objectID == rhs.objectID
&& lhs.context == rhs.context && lhs.context == rhs.context
} }
@@ -72,23 +113,25 @@ public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(self.id) hasher.combine(self.objectID)
hasher.combine(self.context) hasher.combine(self.context)
} }
// MARK: LiveResult // MARK: LiveResult
public typealias ObjectType = O
public typealias SnapshotType = ObjectSnapshot<O> public typealias SnapshotType = ObjectSnapshot<O>
// MARK: Internal // 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<O>.init(id:context:)) self.init(
objectID: objectID,
context: context,
initializer: ObjectSnapshot<O>.init(id:context:)
)
} }
@@ -96,9 +139,9 @@ public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
fileprivate let rawObjectWillChange: Any? fileprivate let rawObjectWillChange: Any?
fileprivate init(id: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot<O>) { fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot<O>) {
self.id = id self.objectID = objectID
self.context = context self.context = context
if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) { if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
@@ -114,7 +157,7 @@ public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
self.rawObjectWillChange = nil 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 context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (objectIDs) in
@@ -122,18 +165,36 @@ public final class LiveObject<O: DynamicObject>: Identifiable, Hashable {
return return
} }
self.$lazySnapshot.reset({ initializer(id, context) })
self.willChange() self.willChange()
self.$lazySnapshot.reset({ initializer(objectID, context) })
self.notifyObservers()
self.didChange()
} }
} }
// MARK: Private // MARK: Private
private let objectID: O.ObjectID
private let context: NSManagedObjectContext private let context: NSManagedObjectContext
@Internals.LazyNonmutating(uninitialized: ()) @Internals.LazyNonmutating(uninitialized: ())
private var lazySnapshot: ObjectSnapshot<O> private var lazySnapshot: ObjectSnapshot<O>
private lazy var observers: NSMapTable<AnyObject, Internals.Closure<LiveObject<O>, Void>> = .weakToStrongObjects()
private func notifyObservers() {
guard let enumerator = self.observers.objectEnumerator() else {
return
}
for closure in enumerator {
(closure as! Internals.Closure<LiveObject
<O>, Void>).invoke(with: self)
}
}
} }

View File

@@ -87,7 +87,7 @@ extension NSManagedObjectContext {
} }
@nonobjc @nonobjc
internal func liveObject<D: DynamicObject>(id: NSManagedObjectID) -> LiveObject<D> { internal func liveObject<D: DynamicObject>(objectID: NSManagedObjectID) -> LiveObject<D> {
let cache: NSMapTable<NSManagedObjectID, LiveObject<D>> = self.userInfo(for: .liveObjectsCache(D.self)) { let cache: NSMapTable<NSManagedObjectID, LiveObject<D>> = self.userInfo(for: .liveObjectsCache(D.self)) {
@@ -95,12 +95,12 @@ extension NSManagedObjectContext {
} }
return Internals.with { return Internals.with {
if let liveObject = cache.object(forKey: id) { if let liveObject = cache.object(forKey: objectID) {
return liveObject return liveObject
} }
let liveObject = LiveObject<D>(id: id, context: self) let liveObject = LiveObject<D>(objectID: objectID, context: self)
cache.setObject(liveObject, forKey: id) cache.setObject(liveObject, forKey: objectID)
return liveObject return liveObject
} }
} }

View File

@@ -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. 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, *) @available(macOS 10.12, *)
public final class ObjectMonitor<D: DynamicObject>: Equatable { public final class ObjectMonitor<D: DynamicObject>: ObjectRepresentation, Equatable {
/**
The type for the object contained by the `ObjectMonitor`
*/
public typealias ObjectType = D
/** /**
Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted.
@@ -125,6 +120,34 @@ public final class ObjectMonitor<D: DynamicObject>: Equatable {
public let userInfo = UserInfo() 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 // MARK: Equatable
public static func == (lhs: ObjectMonitor<ObjectType>, rhs: ObjectMonitor<ObjectType>) -> Bool { public static func == (lhs: ObjectMonitor<ObjectType>, rhs: ObjectMonitor<ObjectType>) -> Bool {
@@ -158,14 +181,14 @@ public final class ObjectMonitor<D: DynamicObject>: Equatable {
// MARK: Internal // MARK: Internal
internal convenience init(dataStack: DataStack, object: ObjectType) { internal convenience init<O: ObjectRepresentation>(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<O: ObjectRepresentation>(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<U: AnyObject>(_ observer: U, willChangeObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType) -> Void, didDeleteObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType) -> Void, didUpdateObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType, _ changedPersistentKeys: Set<String>) -> Void) { internal func registerObserver<U: AnyObject>(_ observer: U, willChangeObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType) -> Void, didDeleteObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType) -> Void, didUpdateObject: @escaping (_ observer: U, _ monitor: ObjectMonitor<ObjectType>, _ object: ObjectType, _ changedPersistentKeys: Set<String>) -> Void) {
@@ -250,6 +273,7 @@ public final class ObjectMonitor<D: DynamicObject>: Equatable {
// MARK: Private // MARK: Private
private let objectID: ObjectType.ObjectID
private let fetchedResultsController: Internals.CoreStoreFetchedResultsController private let fetchedResultsController: Internals.CoreStoreFetchedResultsController
private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate
private var lastCommittedAttributes = [String: NSObject]() private var lastCommittedAttributes = [String: NSObject]()
@@ -258,9 +282,8 @@ public final class ObjectMonitor<D: DynamicObject>: Equatable {
private var didDeleteObjectKey: Void? private var didDeleteObjectKey: Void?
private var didUpdateObjectKey: 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<NSManagedObject>() let fetchRequest = Internals.CoreStoreFetchRequest<NSManagedObject>()
fetchRequest.entity = objectID.entity fetchRequest.entity = objectID.entity
fetchRequest.fetchLimit = 0 fetchRequest.fetchLimit = 0
@@ -278,6 +301,7 @@ public final class ObjectMonitor<D: DynamicObject>: Equatable {
let fetchedResultsControllerDelegate = Internals.FetchedResultsControllerDelegate() let fetchedResultsControllerDelegate = Internals.FetchedResultsControllerDelegate()
self.objectID = objectID
self.fetchedResultsController = fetchedResultsController self.fetchedResultsController = fetchedResultsController
self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate

View File

@@ -2,8 +2,76 @@
// ObjectRepresentation.swift // ObjectRepresentation.swift
// CoreStore // CoreStore
// //
// Created by John Estropia on 2019/10/11. // Copyright © 2018 John Rommel Estropia
// Copyright © 2019 John Rommel Estropia. All rights reserved. //
// 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())
}
}

View File

@@ -97,51 +97,35 @@ public final class SynchronousDataTransaction: BaseDataTransaction {
return super.edit(into, objectID) return super.edit(into, objectID)
} }
/** /**
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` type 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 override func delete<D: DynamicObject>(_ object: D?) { public override func delete<O: ObjectRepresentation>(_ object: O?, _ objects: O?...) {
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<D: DynamicObject>(_ object1: D?, _ object2: D?, _ objects: D?...) {
Internals.assert( Internals.assert(
!self.isCommitted, !self.isCommitted,
"Attempted to delete an entities from an already committed \(Internals.typeName(self))." "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. Deletes the specified `NSManagedObject`s or `CoreStoreObject`s represented by an `ObjectRepresenation`.
- parameter objects: the `DynamicObject`s to be deleted - parameter objects: the `ObjectRepresenation`s representing `NSManagedObject`s or `CoreStoreObject`s to be deleted
*/ */
public override func delete<S: Sequence>(_ objects: S) where S.Iterator.Element: DynamicObject { public override func delete<S: Sequence>(_ objects: S) where S.Iterator.Element: ObjectRepresentation {
Internals.assert( Internals.assert(
!self.isCommitted, !self.isCommitted,
"Attempted to delete an entities from an already committed \(Internals.typeName(self))." "Attempted to delete an entities from an already committed \(Internals.typeName(self))."
) )
super.delete(objects) super.delete(objects)
} }

View File

@@ -33,12 +33,12 @@ import CoreData
extension UnsafeDataTransaction { 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` - returns: a `ObjectMonitor` that monitors changes to `object`
*/ */
public func monitorObject<D>(_ object: D) -> ObjectMonitor<D> { public func monitorObject<O: ObjectRepresentation>(_ object: O) -> ObjectMonitor<O.ObjectType> {
return ObjectMonitor( return ObjectMonitor(
unsafeTransaction: self, unsafeTransaction: self,