diff --git a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift index 98ddd76..688b1ee 100644 --- a/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift +++ b/CoreStoreDemo/CoreStoreDemo/List and Object Observers Demo/Palette.swift @@ -75,24 +75,6 @@ extension Palette { } } -extension LiveObject where ObjectType == Palette { - - var color: UIColor { - - return UIColor( - hue: CGFloat(self.hue) / 360.0, - saturation: CGFloat(self.saturation), - brightness: CGFloat(self.brightness), - alpha: 1.0 - ) - } - - var colorText: String { - - return "H: \(self.hue)˚, S: \(round(self.saturation * 100.0))%, B: \(round(self.brightness * 100.0))%" - } -} - extension Palette { func setInitialValues(in transaction: BaseDataTransaction) { diff --git a/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift b/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift index 7b52c71..5c2ab8c 100644 --- a/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift +++ b/CoreStoreDemo/CoreStoreDemo/SwiftUI Demo/SwiftUIView.swift @@ -116,10 +116,10 @@ struct ColorCell: View { var body: some View { HStack { - Color(palette.color) + Color(palette.color ?? UIColor.clear) .cornerRadius(5) .frame(width: 30, height: 30, alignment: .leading) - Text(palette.colorText) + Text(palette.colorText ?? "") } } } @@ -140,18 +140,18 @@ struct DetailView: View { init(palette: LiveObject) { self.palette = palette - self.hue = Float(palette.hue) - self.saturation = palette.saturation - self.brightness = palette.brightness + self.hue = Float(palette.hue ?? 0) + self.saturation = palette.saturation ?? 0 + self.brightness = palette.brightness ?? 0 } var body: some View { ZStack { - Color(palette.color) + Color(palette.color ?? UIColor.clear) .cornerRadius(20) .padding(20) VStack { - Text(palette.colorText) + Text(palette.colorText ?? "") .navigationBarTitle(Text("Color")) Slider(value: $hue, in: 0.0 ... 359.0 as ClosedRange) Slider(value: $saturation, in: 0.0 ... 1.0 as ClosedRange) diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 2f8d94f..2b40a97 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -292,22 +292,32 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(person.name.value, "John") XCTAssertEqual(person.displayName.value, "Mr. John") // Custom getter - let personSnapshot1 = person.asSnapshot(in: transaction) - XCTAssertEqual(person.name.value, personSnapshot1?.name) - XCTAssertEqual(person.title.value, personSnapshot1?.title) - XCTAssertEqual(person.displayName.value, personSnapshot1?.displayName) + let personSnapshot1 = person.asSnapshot(in: transaction)! + XCTAssertEqual(person.name.value, personSnapshot1.name) + XCTAssertEqual(person.title.value, personSnapshot1.title) + XCTAssertEqual(person.displayName.value, personSnapshot1.displayName) person.title .= "Sir" XCTAssertEqual(person.displayName.value, "Sir John") - XCTAssertEqual(personSnapshot1?.name, "John") - XCTAssertEqual(personSnapshot1?.title, "Mr.") - XCTAssertEqual(personSnapshot1?.displayName, "Mr. John") + XCTAssertEqual(personSnapshot1.name, "John") + XCTAssertEqual(personSnapshot1.title, "Mr.") + XCTAssertEqual(personSnapshot1.displayName, "Mr. John") - let personSnapshot2 = person.asSnapshot(in: transaction) - XCTAssertEqual(person.name.value, personSnapshot2?.name) - XCTAssertEqual(person.title.value, personSnapshot2?.title) - XCTAssertEqual(person.displayName.value, personSnapshot2?.displayName) + let personSnapshot2 = person.asSnapshot(in: transaction)! + XCTAssertEqual(person.name.value, personSnapshot2.name) + XCTAssertEqual(person.title.value, personSnapshot2.title) + XCTAssertEqual(person.displayName.value, personSnapshot2.displayName) + + var personSnapshot3 = personSnapshot2 + personSnapshot3.name = "James" + XCTAssertEqual(personSnapshot1.name, "John") + XCTAssertEqual(personSnapshot1.displayName, "Mr. John") + XCTAssertEqual(personSnapshot2.name, "John") + XCTAssertEqual(personSnapshot2.displayName, "Sir John") + XCTAssertEqual(personSnapshot3.name, "James") + XCTAssertEqual(personSnapshot3.displayName, "Sir John") + person.pets.value.insert(dog) XCTAssertEqual(person.pets.count, 1) diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index c4231f6..f3fb9ef 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -46,7 +46,7 @@ public protocol DynamicObject: AnyObject { /** 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. @@ -97,11 +97,19 @@ extension NSManagedObject: DynamicObject { return object } - public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any] { + public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]? { - let object = context.fetchExisting(id)! as Self + guard let object = context.fetchExisting(id) as NSManagedObject? else { + + return nil + } let rawObject = object.cs_toRaw() - return rawObject.dictionaryWithValues(forKeys: rawObject.entity.properties.map({ $0.name })) + var dictionary = rawObject.dictionaryWithValues(forKeys: Array(rawObject.entity.attributesByName.keys)) + for case (let key, let target as NSManagedObject) in rawObject.dictionaryWithValues(forKeys: Array(rawObject.entity.relationshipsByName.keys)) { + + dictionary[key] = target.objectID + } + return dictionary } public class func cs_fromRaw(object: NSManagedObject) -> Self { @@ -143,7 +151,7 @@ extension CoreStoreObject { return self.cs_fromRaw(object: object) } - public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any] { + public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]? { func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) { @@ -170,11 +178,14 @@ extension CoreStoreObject { } } } - let object = context.fetchExisting(id)! as Self + guard let object = context.fetchExisting(id) as CoreStoreObject? else { + + return nil + } var values: [KeyPathString: Any] = [:] initializeAttributes( mirror: Mirror(reflecting: object), - object: object, + object: object as! Self, into: &values ) return values diff --git a/Sources/LiveObject.swift b/Sources/LiveObject.swift index 99034ae..4ca4f2e 100644 --- a/Sources/LiveObject.swift +++ b/Sources/LiveObject.swift @@ -46,10 +46,12 @@ public final class LiveObject: ObjectRepresentation, Hashable public typealias SectionID = String public typealias ItemID = O.ObjectID - public var snapshot: SnapshotType { + public var snapshot: ObjectSnapshot? { return self.lazySnapshot } + + public lazy var object: O? = self.context.fetchExisting(self.id) public func addObserver(_ observer: T, _ callback: @escaping (LiveObject) -> Void) { @@ -79,7 +81,7 @@ public final class LiveObject: ObjectRepresentation, Hashable return self.id } - public func asLiveObject(in dataStack: DataStack) -> LiveObject? { + public func asLiveObject(in dataStack: DataStack) -> LiveObject { let context = dataStack.unsafeContext() if self.context == context { @@ -106,7 +108,7 @@ public final class LiveObject: ObjectRepresentation, Hashable return self.lazySnapshot } - return .init(objectID: self.id, context: context) + return ObjectSnapshot(objectID: self.id, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { @@ -116,7 +118,7 @@ public final class LiveObject: ObjectRepresentation, Hashable return self.lazySnapshot } - return .init(objectID: self.id, context: context) + return ObjectSnapshot(objectID: self.id, context: context) } @@ -159,7 +161,7 @@ public final class LiveObject: ObjectRepresentation, Hashable fileprivate let rawObjectWillChange: Any? - fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot) { + fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot?) { self.id = objectID self.context = context @@ -179,24 +181,31 @@ public final class LiveObject: ObjectRepresentation, Hashable } self.$lazySnapshot.initialize({ initializer(objectID, context) }) - context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (objectIDs) in + context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (updatedIDs, deletedIDs) in guard let self = self else { return } - self.willChange() - self.$lazySnapshot.reset({ initializer(objectID, context) }) - self.notifyObservers() - self.didChange() + if deletedIDs.contains(objectID) { + + self.object = nil + + self.willChange() + self.$lazySnapshot.reset({ nil }) + self.notifyObservers() + self.didChange() + } + else if updatedIDs.contains(objectID) { + + self.willChange() + self.$lazySnapshot.reset({ initializer(objectID, context) }) + self.notifyObservers() + self.didChange() + } } } - fileprivate var object: O { - - return self.context.fetchExisting(self.id)! - } - // MARK: Private @@ -204,7 +213,7 @@ public final class LiveObject: ObjectRepresentation, Hashable private let context: NSManagedObjectContext @Internals.LazyNonmutating(uninitialized: ()) - private var lazySnapshot: ObjectSnapshot + private var lazySnapshot: ObjectSnapshot? private lazy var observers: NSMapTable, Void>> = .weakToStrongObjects() @@ -302,9 +311,9 @@ extension LiveObject where O: CoreStoreObject { /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.Required>) -> V { + public subscript(dynamicMember member: KeyPath.Required>) -> V? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** @@ -312,15 +321,15 @@ extension LiveObject where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Optional>) -> V? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.Required>) -> V { + public subscript(dynamicMember member: KeyPath.Required>) -> V? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** @@ -328,7 +337,7 @@ extension LiveObject where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Optional>) -> V? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** @@ -336,22 +345,30 @@ extension LiveObject where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.ToOne>) -> D? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.ToManyOrdered>) -> [D] { + public subscript(dynamicMember member: KeyPath.ToManyOrdered>) -> [D]? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.ToManyUnordered>) -> Set { + public subscript(dynamicMember member: KeyPath.ToManyUnordered>) -> Set? { - return self.snapshot[dynamicMember: member] + return self.object?[keyPath: member].value + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath) -> V? { + + return self.object?[keyPath: member] } } diff --git a/Sources/NSManagedObjectContext+CoreStore.swift b/Sources/NSManagedObjectContext+CoreStore.swift index 28a54e9..589e99b 100644 --- a/Sources/NSManagedObjectContext+CoreStore.swift +++ b/Sources/NSManagedObjectContext+CoreStore.swift @@ -106,7 +106,7 @@ extension NSManagedObjectContext { } @nonobjc - internal func objectsDidChangeObserver(for observer: U) -> Internals.SharedNotificationObserver> { + internal func objectsDidChangeObserver(for observer: U) -> Internals.SharedNotificationObserver<(updated: Set, deleted: Set)> { return self.userInfo(for: .objectsChangeObserver(U.self)) { [unowned self] in @@ -114,11 +114,11 @@ extension NSManagedObjectContext { notificationName: .NSManagedObjectContextObjectsDidChange, object: self, queue: .main, - sharedValue: { (notification) -> Set in + sharedValue: { (notification) -> (updated: Set, deleted: Set) in guard let userInfo = notification.userInfo else { - return [] + return (updated: [], deleted: []) } var updatedObjectIDs: Set = [] if let updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set { @@ -129,7 +129,8 @@ extension NSManagedObjectContext { updatedObjectIDs.formUnion(mergedObjects) } - return updatedObjectIDs + let deletedObjectIDs: Set = (userInfo[NSDeletedObjectsKey] as? Set) ?? [] + return (updated: updatedObjectIDs, deleted: deletedObjectIDs) } ) } diff --git a/Sources/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index 85fba5d..8f88c1f 100644 --- a/Sources/ObjectRepresentation.swift +++ b/Sources/ObjectRepresentation.swift @@ -46,7 +46,7 @@ public protocol ObjectRepresentation { /** An instance that may be observed for object changes. */ - func asLiveObject(in dataStack: DataStack) -> LiveObject? + func asLiveObject(in dataStack: DataStack) -> LiveObject /** A read-only instance in the `DataStack`. @@ -82,10 +82,10 @@ extension DynamicObject where Self: ObjectRepresentation { return self.cs_id() } - public func asLiveObject(in dataStack: DataStack) -> LiveObject? { + public func asLiveObject(in dataStack: DataStack) -> LiveObject { let context = dataStack.unsafeContext() - return .init(objectID: self.cs_id(), context: context) + return LiveObject(objectID: self.cs_id(), context: context) } public func asReadOnly(in dataStack: DataStack) -> Self? { @@ -111,12 +111,12 @@ extension DynamicObject where Self: ObjectRepresentation { public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return .init(objectID: self.cs_id(), context: context) + return ObjectSnapshot(objectID: self.cs_id(), context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return .init(objectID: self.cs_id(), context: context) + return ObjectSnapshot(objectID: self.cs_id(), context: context) } } diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index a6be1be..5c2dd3d 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -51,10 +51,10 @@ public struct ObjectSnapshot: SnapshotResult, ObjectRepresenta return self.id } - public func asLiveObject(in dataStack: DataStack) -> LiveObject? { + public func asLiveObject(in dataStack: DataStack) -> LiveObject { let context = dataStack.unsafeContext() - return .init(objectID: self.id, context: context) + return LiveObject(objectID: self.id, context: context) } public func asReadOnly(in dataStack: DataStack) -> O? { @@ -70,21 +70,13 @@ public struct ObjectSnapshot: SnapshotResult, ObjectRepresenta public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - if self.context == context { - - return self - } - return .init(objectID: self.id, context: context) + return ObjectSnapshot(objectID: self.id, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - if self.context == context { - - return self - } - return .init(objectID: self.id, context: context) + return ObjectSnapshot(objectID: self.id, context: context) } @@ -93,7 +85,7 @@ public struct ObjectSnapshot: SnapshotResult, ObjectRepresenta public static func == (_ lhs: Self, _ rhs: Self) -> Bool { return lhs.id == rhs.id - && lhs.values == rhs.values + && lhs.valuesRef == rhs.valuesRef } @@ -102,25 +94,32 @@ public struct ObjectSnapshot: SnapshotResult, ObjectRepresenta public func hash(into hasher: inout Hasher) { hasher.combine(self.id) - hasher.combine(self.values) + hasher.combine(self.valuesRef) } // MARK: Internal - internal init(objectID: O.ObjectID, context: NSManagedObjectContext) { + internal init?(objectID: O.ObjectID, context: NSManagedObjectContext) { + guard let values = O.cs_snapshotDictionary(id: objectID, context: context) else { + + return nil + } self.id = objectID - self.context = context - self.values = O.cs_snapshotDictionary(id: objectID, context: context) as NSDictionary + self.values = values } // MARK: Private private let id: O.ObjectID - private let context: NSManagedObjectContext - private let values: NSDictionary + private var values: [String: Any] + + private var valuesRef: NSDictionary { + + return self.values as NSDictionary + } } @@ -149,8 +148,16 @@ extension ObjectSnapshot where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Required>) -> V { - let key = String(keyPath: member) - return self.values[key] as! V + get { + + let key = String(keyPath: member) + return self.values[key] as! V + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** @@ -158,8 +165,16 @@ extension ObjectSnapshot where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Optional>) -> V? { - let key = String(keyPath: member) - return self.values[key] as! V? + get { + + let key = String(keyPath: member) + return self.values[key] as! V? + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** @@ -167,8 +182,16 @@ extension ObjectSnapshot where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Required>) -> V { - let key = String(keyPath: member) - return self.values[key] as! V + get { + + let key = String(keyPath: member) + return self.values[key] as! V + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** @@ -176,34 +199,66 @@ extension ObjectSnapshot where O: CoreStoreObject { */ public subscript(dynamicMember member: KeyPath.Optional>) -> V? { - let key = String(keyPath: member) - return self.values[key] as! V? + get { + + let key = String(keyPath: member) + return self.values[key] as! V? + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.ToOne>) -> D? { + public subscript(dynamicMember member: KeyPath.ToOne>) -> D.ObjectID? { - let key = String(keyPath: member) - return self.values[key] as! D? + get { + + let key = String(keyPath: member) + return self.values[key] as! D.ObjectID? + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.ToManyOrdered>) -> [D] { + public subscript(dynamicMember member: KeyPath.ToManyOrdered>) -> [D.ObjectID] { - let key = String(keyPath: member) - return self.values[key] as! [D] + get { + + let key = String(keyPath: member) + return self.values[key] as! [D.ObjectID] + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } /** Returns the value for the property identified by a given key. */ - public subscript(dynamicMember member: KeyPath.ToManyUnordered>) -> Set { + public subscript(dynamicMember member: KeyPath.ToManyUnordered>) -> Set { - let key = String(keyPath: member) - return self.values[key] as! Set + get { + + let key = String(keyPath: member) + return self.values[key] as! Set + } + set { + + let key = String(keyPath: member) + self.values[key] = newValue + } } } diff --git a/Sources/Relationship.swift b/Sources/Relationship.swift index 4243db4..120a799 100644 --- a/Sources/Relationship.swift +++ b/Sources/Relationship.swift @@ -315,7 +315,7 @@ public enum RelationshipContainer { internal var valueForSnapshot: Any { - return self.value as Any + return self.value?.objectID() as Any } @@ -611,7 +611,7 @@ public enum RelationshipContainer { internal var valueForSnapshot: Any { - return self.value as Any + return self.value.map({ $0.objectID() }) as Any } @@ -912,7 +912,7 @@ public enum RelationshipContainer { internal var valueForSnapshot: Any { - return self.value as Any + return Set(self.value.map({ $0.objectID() })) as Any }