Improve handling in LiveObject and ObjectSnapshot when objects are deleted

This commit is contained in:
John Estropia
2019-10-17 19:27:03 +09:00
parent bd066f0cef
commit 6e3e540d0a
9 changed files with 194 additions and 118 deletions

View File

@@ -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) {

View File

@@ -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 ?? "<Deleted>")
}
}
}
@@ -140,18 +140,18 @@ struct DetailView: View {
init(palette: LiveObject<Palette>) {
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 ?? "<Deleted>")
.navigationBarTitle(Text("Color"))
Slider(value: $hue, in: 0.0 ... 359.0 as ClosedRange<Float>)
Slider(value: $saturation, in: 0.0 ... 1.0 as ClosedRange<Float>)

View File

@@ -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)

View File

@@ -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

View File

@@ -46,10 +46,12 @@ public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable
public typealias SectionID = String
public typealias ItemID = O.ObjectID
public var snapshot: SnapshotType {
public var snapshot: ObjectSnapshot<O>? {
return self.lazySnapshot
}
public lazy var object: O? = self.context.fetchExisting(self.id)
public func addObserver<T: AnyObject>(_ observer: T, _ callback: @escaping (LiveObject<O>) -> Void) {
@@ -79,7 +81,7 @@ public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable
return self.id
}
public func asLiveObject(in dataStack: DataStack) -> LiveObject<O>? {
public func asLiveObject(in dataStack: DataStack) -> LiveObject<O> {
let context = dataStack.unsafeContext()
if self.context == context {
@@ -106,7 +108,7 @@ public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable
return self.lazySnapshot
}
return .init(objectID: self.id, context: context)
return ObjectSnapshot<O>(objectID: self.id, context: context)
}
public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<O>? {
@@ -116,7 +118,7 @@ public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable
return self.lazySnapshot
}
return .init(objectID: self.id, context: context)
return ObjectSnapshot<O>(objectID: self.id, context: context)
}
@@ -159,7 +161,7 @@ public final class LiveObject<O: DynamicObject>: ObjectRepresentation, Hashable
fileprivate let rawObjectWillChange: Any?
fileprivate init(objectID: 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 = objectID
self.context = context
@@ -179,24 +181,31 @@ public final class LiveObject<O: DynamicObject>: 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<O: DynamicObject>: ObjectRepresentation, Hashable
private let context: NSManagedObjectContext
@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()
@@ -302,9 +311,9 @@ extension LiveObject where O: CoreStoreObject {
/**
Returns the value for the property identified by a given key.
*/
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Required<V>>) -> V {
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Required<V>>) -> V? {
return self.snapshot[dynamicMember: member]
return self.object?[keyPath: member].value
}
/**
@@ -312,15 +321,15 @@ extension LiveObject where O: CoreStoreObject {
*/
public subscript<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Optional<V>>) -> 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<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Required<V>>) -> V {
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Required<V>>) -> V? {
return self.snapshot[dynamicMember: member]
return self.object?[keyPath: member].value
}
/**
@@ -328,7 +337,7 @@ extension LiveObject where O: CoreStoreObject {
*/
public subscript<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Optional<V>>) -> V? {
return self.snapshot[dynamicMember: member]
return self.object?[keyPath: member].value
}
/**
@@ -336,22 +345,30 @@ extension LiveObject where O: CoreStoreObject {
*/
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToOne<D>>) -> 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<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [D] {
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [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<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<D> {
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<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<V>(dynamicMember member: KeyPath<O, V>) -> V? {
return self.object?[keyPath: member]
}
}

View File

@@ -106,7 +106,7 @@ extension NSManagedObjectContext {
}
@nonobjc
internal func objectsDidChangeObserver<U: AnyObject>(for observer: U) -> Internals.SharedNotificationObserver<Set<NSManagedObjectID>> {
internal func objectsDidChangeObserver<U: AnyObject>(for observer: U) -> Internals.SharedNotificationObserver<(updated: Set<NSManagedObjectID>, deleted: Set<NSManagedObjectID>)> {
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<NSManagedObjectID> in
sharedValue: { (notification) -> (updated: Set<NSManagedObjectID>, deleted: Set<NSManagedObjectID>) in
guard let userInfo = notification.userInfo else {
return []
return (updated: [], deleted: [])
}
var updatedObjectIDs: Set<NSManagedObjectID> = []
if let updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObjectID> {
@@ -129,7 +129,8 @@ extension NSManagedObjectContext {
updatedObjectIDs.formUnion(mergedObjects)
}
return updatedObjectIDs
let deletedObjectIDs: Set<NSManagedObjectID> = (userInfo[NSDeletedObjectsKey] as? Set<NSManagedObjectID>) ?? []
return (updated: updatedObjectIDs, deleted: deletedObjectIDs)
}
)
}

View File

@@ -46,7 +46,7 @@ public protocol ObjectRepresentation {
/**
An instance that may be observed for object changes.
*/
func asLiveObject(in dataStack: DataStack) -> LiveObject<ObjectType>?
func asLiveObject(in dataStack: DataStack) -> LiveObject<ObjectType>
/**
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<Self>? {
public func asLiveObject(in dataStack: DataStack) -> LiveObject<Self> {
let context = dataStack.unsafeContext()
return .init(objectID: self.cs_id(), context: context)
return LiveObject<Self>(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<Self>? {
let context = dataStack.unsafeContext()
return .init(objectID: self.cs_id(), context: context)
return ObjectSnapshot<Self>(objectID: self.cs_id(), context: context)
}
public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<Self>? {
let context = transaction.unsafeContext()
return .init(objectID: self.cs_id(), context: context)
return ObjectSnapshot<Self>(objectID: self.cs_id(), context: context)
}
}

View File

@@ -51,10 +51,10 @@ public struct ObjectSnapshot<O: DynamicObject>: SnapshotResult, ObjectRepresenta
return self.id
}
public func asLiveObject(in dataStack: DataStack) -> LiveObject<O>? {
public func asLiveObject(in dataStack: DataStack) -> LiveObject<O> {
let context = dataStack.unsafeContext()
return .init(objectID: self.id, context: context)
return LiveObject<O>(objectID: self.id, context: context)
}
public func asReadOnly(in dataStack: DataStack) -> O? {
@@ -70,21 +70,13 @@ public struct ObjectSnapshot<O: DynamicObject>: SnapshotResult, ObjectRepresenta
public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot<O>? {
let context = dataStack.unsafeContext()
if self.context == context {
return self
}
return .init(objectID: self.id, context: context)
return ObjectSnapshot<O>(objectID: self.id, context: context)
}
public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<O>? {
let context = transaction.unsafeContext()
if self.context == context {
return self
}
return .init(objectID: self.id, context: context)
return ObjectSnapshot<O>(objectID: self.id, context: context)
}
@@ -93,7 +85,7 @@ public struct ObjectSnapshot<O: DynamicObject>: 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<O: DynamicObject>: 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<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Required<V>>) -> 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<V>(dynamicMember member: KeyPath<O, ValueContainer<O>.Optional<V>>) -> 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<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Required<V>>) -> 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<V>(dynamicMember member: KeyPath<O, TransformableContainer<O>.Optional<V>>) -> 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<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToOne<D>>) -> D? {
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToOne<D>>) -> 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<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [D] {
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyOrdered<D>>) -> [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<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<D> {
public subscript<D>(dynamicMember member: KeyPath<O, RelationshipContainer<O>.ToManyUnordered<D>>) -> Set<D.ObjectID> {
let key = String(keyPath: member)
return self.values[key] as! Set<D>
get {
let key = String(keyPath: member)
return self.values[key] as! Set<D.ObjectID>
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
}
}
}

View File

@@ -315,7 +315,7 @@ public enum RelationshipContainer<O: CoreStoreObject> {
internal var valueForSnapshot: Any {
return self.value as Any
return self.value?.objectID() as Any
}
@@ -611,7 +611,7 @@ public enum RelationshipContainer<O: CoreStoreObject> {
internal var valueForSnapshot: Any {
return self.value as Any
return self.value.map({ $0.objectID() }) as Any
}
@@ -912,7 +912,7 @@ public enum RelationshipContainer<O: CoreStoreObject> {
internal var valueForSnapshot: Any {
return self.value as Any
return Set(self.value.map({ $0.objectID() })) as Any
}