From 10cd18dbf058ad71e7e87e5a54101c8aff14b804 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 14 Dec 2018 18:20:42 +0900 Subject: [PATCH] prototype for CoreStoreObject property observers (a.k.a. KVO) --- CoreStore.xcodeproj/project.pbxproj | 10 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + .../AppIcon.appiconset/Contents.json | 55 ++ CoreStoreTests/DynamicModelTests.swift | 36 ++ Sources/CoreStoreObject+Observing.swift | 507 ++++++++++++++++++ 5 files changed, 613 insertions(+) create mode 100644 CoreStore.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Sources/CoreStoreObject+Observing.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index b319d93..f129a82 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -659,6 +659,10 @@ B5E84F371AFF85470064E85B /* NSManagedObjectContext+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */; }; B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */; }; B5E84F411AFF8CCD0064E85B /* TypeErasedClauses.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F401AFF8CCD0064E85B /* TypeErasedClauses.swift */; }; + B5E8A72021C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */; }; + B5E8A72121C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */; }; + B5E8A72221C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */; }; + B5E8A72321C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */; }; B5ECDBDF1CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECDBDE1CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift */; }; B5ECDBE11CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECDBDE1CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift */; }; B5ECDBE21CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5ECDBDE1CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift */; }; @@ -950,6 +954,7 @@ B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Transaction.swift"; sourceTree = ""; }; B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = ""; }; B5E84F401AFF8CCD0064E85B /* TypeErasedClauses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeErasedClauses.swift; sourceTree = ""; }; + B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreStoreObject+Observing.swift"; sourceTree = ""; }; B5ECDBDE1CA6BB2B00C7F112 /* CSBaseDataTransaction+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSBaseDataTransaction+Querying.swift"; sourceTree = ""; }; B5ECDBE41CA6BEA300C7F112 /* CSClauseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSClauseTypes.swift; sourceTree = ""; }; B5ECDBEB1CA6BF2000C7F112 /* CSFrom.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSFrom.swift; sourceTree = ""; }; @@ -1493,6 +1498,7 @@ B56007131B3F6C2800A9A8F9 /* SectionBy.swift */, B5E84F1A1AFF84860064E85B /* DataStack+Observing.swift */, B5E84F1B1AFF84860064E85B /* CoreStore+Observing.swift */, + B5E8A71F21C1015300EF006A /* CoreStoreObject+Observing.swift */, B5C976E21C6C9F6A00B1AF90 /* UnsafeDataTransaction+Observing.swift */, B5E84F1C1AFF84860064E85B /* ObjectMonitor.swift */, B5E84F1F1AFF84860064E85B /* ObjectObserver.swift */, @@ -2008,6 +2014,7 @@ B5A991EC1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5FE4DA71C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, B52F743D1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, + B5E8A72021C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B56923FF1EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CAE1FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, B5ECDBEC1CA6BF2000C7F112 /* CSFrom.swift in Sources */, @@ -2204,6 +2211,7 @@ B5A991ED1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5ECDBEE1CA6BF2000C7F112 /* CSFrom.swift in Sources */, B52F743E1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, + B5E8A72121C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B56924001EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CAF1FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, 82BA18D61C4BBD7100A0916E /* NSManagedObjectContext+Transaction.swift in Sources */, @@ -2400,6 +2408,7 @@ B5A991EF1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5220E201D130813009BC71E /* CSObjectMonitor.swift in Sources */, B52F74401E9B8724005F3DAC /* DynamicSchema.swift in Sources */, + B5E8A72321C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B56924021EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CB11FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, B5220E171D1306DF009BC71E /* UnsafeDataTransaction+Observing.swift in Sources */, @@ -2596,6 +2605,7 @@ B5A991EE1E9DC2CE0091A2E3 /* VersionLock.swift in Sources */, B5ECDBEF1CA6BF2000C7F112 /* CSFrom.swift in Sources */, B52F743F1E9B8724005F3DAC /* DynamicSchema.swift in Sources */, + B5E8A72221C1015300EF006A /* CoreStoreObject+Observing.swift in Sources */, B56924011EB82976007C4DC9 /* CSUnsafeDataModelSchema.swift in Sources */, B5215CB01FA4812500139E3A /* SectionMonitorBuilder.swift in Sources */, B56321B41BD6521C006C9394 /* NSManagedObjectContext+Transaction.swift in Sources */, diff --git a/CoreStore.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/CoreStore.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/CoreStore.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/CoreStoreDemo/CoreStoreDemo/Images.xcassets/AppIcon.appiconset/Contents.json b/CoreStoreDemo/CoreStoreDemo/Images.xcassets/AppIcon.appiconset/Contents.json index 83cfa32..e627b09 100644 --- a/CoreStoreDemo/CoreStoreDemo/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/CoreStoreDemo/CoreStoreDemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -32,6 +42,36 @@ "filename" : "Icon-60@3x-1.png", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, { "size" : "76x76", "idiom" : "ipad", @@ -44,6 +84,21 @@ "filename" : "Icon-76@2x.png", "scale" : "2x" }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + }, + { + "idiom" : "car", + "size" : "60x60", + "scale" : "2x" + }, { "size" : "60x60", "idiom" : "car", diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 647955a..bdc877a 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -143,6 +143,9 @@ class DynamicModelTests: BaseTestDataTestCase { let updateDone = self.expectation(description: "update-done") let fetchDone = self.expectation(description: "fetch-done") + let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done") + let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done") + let didSetObserverDone = self.expectation(description: "didSet-observe-done") stack.perform( asynchronous: { (transaction) in @@ -160,9 +163,42 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(dog.species.value, "Swift") XCTAssertEqual(dog.nickname.value, nil) XCTAssertEqual(dog.age.value, 1) + + let didSetObserver = dog.species.observe(options: [.new, .old]) { (object, change) in + + XCTAssertEqual(object, dog) + XCTAssertEqual(change.kind, .setting) + XCTAssertEqual(change.newValue, "Dog") + XCTAssertEqual(change.oldValue, "Swift") + XCTAssertFalse(change.isPrior) + XCTAssertEqual(object.species.value, "Dog") + didSetObserverDone.fulfill() + } + let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in + + XCTAssertEqual(object, dog) + XCTAssertEqual(change.kind, .setting) + XCTAssertEqual(change.oldValue, "Swift") + + if change.isPrior { + + XCTAssertNil(change.newValue) + XCTAssertEqual(object.species.value, "Swift") + willSetPriorObserverDone.fulfill() + } + else { + + XCTAssertEqual(change.newValue, "Dog") + XCTAssertEqual(object.species.value, "Dog") + willSetNotPriorObserverDone.fulfill() + } + } dog.species .= "Dog" XCTAssertEqual(dog.species.value, "Dog") + + didSetObserver.invalidate() + willSetObserver.invalidate() dog.nickname .= "Spot" XCTAssertEqual(dog.nickname.value, "Spot") diff --git a/Sources/CoreStoreObject+Observing.swift b/Sources/CoreStoreObject+Observing.swift new file mode 100644 index 0000000..0c63c75 --- /dev/null +++ b/Sources/CoreStoreObject+Observing.swift @@ -0,0 +1,507 @@ +// +// CoreStoreObject+Observing.swift +// CoreStore +// +// Created by John Estropia on 2018/12/12. +// Copyright © 2018 John Rommel Estropia. All rights reserved. +// + +import Foundation +import CoreData + + +// MARK: CoreStoreObjectKeyValueObservation + +/** + Observation token for `CoreStoreObject` properties. Make sure to retain this instance to keep observing notifications. + + `invalidate()` will be called automatically when an `CoreStoreObjectKeyValueObservation` is deinited. + */ +public protocol CoreStoreObjectKeyValueObservation: class { + + /** + `invalidate()` will be called automatically when an `CoreStoreObjectKeyValueObservation` is deinited. + */ + func invalidate() +} + + +// MARK: - ValueContainer.Required + +extension ValueContainer.Required { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectValueDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + return self.observe(with: options, changeHandler: changeHandler) + } +} + + +// MARK: - ValueContainer.Optional + +extension ValueContainer.Optional { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectValueDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + return self.observe(with: options, changeHandler: changeHandler) + } +} + + +// MARK: - TransformableContainer.Required + +extension TransformableContainer.Required { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectTransformableDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + return self.observe(with: options, changeHandler: changeHandler) + } +} + + +// MARK: - TransformableContainer.Optional + +extension TransformableContainer.Optional { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectTransformableDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + return self.observe(with: options, changeHandler: changeHandler) + } +} + + +// MARK: - RelationshipContainer.ToOne + +extension RelationshipContainer.ToOne { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectObjectDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self.keyPath, + callback: { (object, kind, newValue, oldValue, _, isPrior) in + + let notification = CoreStoreObjectObjectDiff( + kind: kind, + newNativeValue: newValue as! CoreStoreManagedObject?, + oldNativeValue: oldValue as! CoreStoreManagedObject?, + isPrior: isPrior + ) + changeHandler( + O.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } +} + + +// MARK: - RelationshipContainer.ToManyUnordered + +extension RelationshipContainer.ToManyUnordered { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectUnorderedDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self.keyPath, + callback: { (object, kind, newValue, oldValue, _, isPrior) in + + let notification = CoreStoreObjectUnorderedDiff( + kind: kind, + newNativeValue: newValue as! NSOrderedSet?, + oldNativeValue: oldValue as! NSOrderedSet?, + isPrior: isPrior + ) + changeHandler( + O.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } +} + + +// MARK: - RelationshipContainer.ToManyOrdered + +extension RelationshipContainer.ToManyOrdered { + + public func observe(options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectOrderedDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self.keyPath, + callback: { (object, kind, newValue, oldValue, indexes, isPrior) in + + let notification = CoreStoreObjectOrderedDiff( + kind: kind, + newNativeValue: newValue as! NSArray?, + oldNativeValue: oldValue as! NSArray?, + indexes: indexes ?? IndexSet(), + isPrior: isPrior + ) + changeHandler( + O.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } +} + + +// MARK: - CoreStoreObjectValueDiff + +public final class CoreStoreObjectValueDiff { + + /** + Indicates the kind of change. See the comments for `NSObject.observeValue(forKeyPath:of:change:context:)` for more information. + */ + public let kind: NSKeyValueChange + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var newValue: V? = self.newNativeValue.flatMap(V.cs_fromQueryableNativeType) + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var oldValue: V? = self.oldNativeValue.flatMap(V.cs_fromQueryableNativeType) + + /** + 'isPrior' will be `true` if this change observation is being sent before the change happens, due to `.prior` being passed to `observe()` + */ + public let isPrior: Bool + + + // MARK: FilePrivate + + fileprivate init(kind: NSKeyValueChange, newNativeValue: V.QueryableNativeType?, oldNativeValue: V.QueryableNativeType?, isPrior: Bool) { + + self.kind = kind + self.newNativeValue = newNativeValue + self.oldNativeValue = oldNativeValue + self.isPrior = isPrior + } + + + // MARK: Private + + private let newNativeValue: V.QueryableNativeType? + private let oldNativeValue: V.QueryableNativeType? +} + + +// MARK: - CoreStoreObjectValueDiff + +public final class CoreStoreObjectTransformableDiff { + + /** + Indicates the kind of change. See the comments for `NSObject.observeValue(forKeyPath:of:change:context:)` for more information. + */ + public let kind: NSKeyValueChange + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public let newValue: V? + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public let oldValue: V? + + /** + 'isPrior' will be `true` if this change observation is being sent before the change happens, due to `.prior` being passed to `observe()` + */ + public let isPrior: Bool + + + // MARK: FilePrivate + + fileprivate init(kind: NSKeyValueChange, newValue: V?, oldValue: V?, isPrior: Bool) { + + self.kind = kind + self.newValue = newValue + self.oldValue = oldValue + self.isPrior = isPrior + } +} + + +// MARK: - CoreStoreObjectObjectDiff + +public final class CoreStoreObjectObjectDiff { + + /** + Indicates the kind of change. See the comments for `NSObject.observeValue(forKeyPath:of:change:context:)` for more information. + */ + public let kind: NSKeyValueChange + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var newValue: D? = self.newNativeValue.flatMap(D.cs_fromRaw(object:)) + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var oldValue: D? = self.oldNativeValue.flatMap(D.cs_fromRaw(object:)) + + /** + 'isPrior' will be `true` if this change observation is being sent before the change happens, due to `.prior` being passed to `observe()` + */ + public let isPrior: Bool + + + // MARK: FilePrivate + + fileprivate init(kind: NSKeyValueChange, newNativeValue: CoreStoreManagedObject?, oldNativeValue: CoreStoreManagedObject?, isPrior: Bool) { + + self.kind = kind + self.newNativeValue = newNativeValue + self.oldNativeValue = oldNativeValue + self.isPrior = isPrior + } + + + // MARK: Private + + private let newNativeValue: CoreStoreManagedObject? + private let oldNativeValue: CoreStoreManagedObject? +} + + +// MARK: - CoreStoreObjectUnorderedDiff + +public final class CoreStoreObjectUnorderedDiff { + + /** + Indicates the kind of change. See the comments for `NSObject.observeValue(forKeyPath:of:change:context:)` for more information. + */ + public let kind: NSKeyValueChange + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var newValue: Set = Set(self.newNativeValue.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) })) + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var oldValue: Set = Set(self.oldNativeValue.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) })) + + /** + 'isPrior' will be `true` if this change observation is being sent before the change happens, due to `.prior` being passed to `observe()` + */ + public let isPrior: Bool + + + // MARK: FilePrivate + + fileprivate init(kind: NSKeyValueChange, newNativeValue: NSOrderedSet?, oldNativeValue: NSOrderedSet?, isPrior: Bool) { + + self.kind = kind + self.newNativeValue = newNativeValue ?? [] + self.oldNativeValue = oldNativeValue ?? [] + self.isPrior = isPrior + } + + + // MARK: Private + + private let newNativeValue: NSOrderedSet + private let oldNativeValue: NSOrderedSet +} + + +// MARK: - CoreStoreObjectOrderedDiff + +public final class CoreStoreObjectOrderedDiff { + + /** + Indicates the kind of change. See the comments for `NSObject.observeValue(forKeyPath:of:change:context:)` for more information. + */ + public let kind: NSKeyValueChange + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var newValue: [D] = self.newNativeValue.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) }) + + /** + `newValue` and `oldValue` will only be non-nil if `.new`/`.old` is passed to `observe()`. In general, get the most up to date value by accessing it directly on the observed object instead. + */ + public private(set) lazy var oldValue: [D] = self.oldNativeValue.map({ D.cs_fromRaw(object: $0 as! NSManagedObject) }) + + /** + `indexes` will be `nil` unless the observed KeyPath refers to an ordered to-many property + */ + public let indexes: IndexSet + + /** + 'isPrior' will be `true` if this change observation is being sent before the change happens, due to `.prior` being passed to `observe()` + */ + public let isPrior: Bool + + + // MARK: FilePrivate + + fileprivate init(kind: NSKeyValueChange, newNativeValue: NSArray?, oldNativeValue: NSArray?, indexes: IndexSet, isPrior: Bool) { + + self.kind = kind + self.newNativeValue = newNativeValue ?? [] + self.oldNativeValue = oldNativeValue ?? [] + self.indexes = indexes + self.isPrior = isPrior + } + + + // MARK: Private + + private let newNativeValue: NSArray + private let oldNativeValue: NSArray +} + + +// MARK: - AttributeProtocol + +extension AttributeProtocol { + + // MARK: FilePrivate + + fileprivate func observe(with options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectValueDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self.keyPath, + callback: { (object, kind, newValue, oldValue, _, isPrior) in + + let notification = CoreStoreObjectValueDiff( + kind: kind, + newNativeValue: newValue as! V.QueryableNativeType?, + oldNativeValue: oldValue as! V.QueryableNativeType?, + isPrior: isPrior + ) + changeHandler( + O.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } + + fileprivate func observe(with options: NSKeyValueObservingOptions = [], changeHandler: @escaping (O, CoreStoreObjectTransformableDiff) -> Void) -> CoreStoreObjectKeyValueObservation { + + let result = _CoreStoreObjectKeyValueObservation( + object: self.rawObject!, + keyPath: self.keyPath, + callback: { (object, kind, newValue, oldValue, _, isPrior) in + + let notification = CoreStoreObjectTransformableDiff( + kind: kind, + newValue: newValue as! V?, + oldValue: oldValue as! V?, + isPrior: isPrior + ) + changeHandler( + O.cs_fromRaw(object: object), + notification + ) + } + ) + result.start(options) + return result + } +} + + +// MARK: - _CoreStoreObjectKeyValueObservation + +// Mirrored implementation from https://github.com/apple/swift/blob/6e7051eb1e38e743a514555d09256d12d3fec750/stdlib/public/Darwin/Foundation/NSObject.swift#L141 +fileprivate final class _CoreStoreObjectKeyValueObservation: NSObject, CoreStoreObjectKeyValueObservation { + + // MARK: FilePrivate + + fileprivate init(object: CoreStoreManagedObject, keyPath: KeyPathString, callback: @escaping (_ object: CoreStoreManagedObject, _ kind: NSKeyValueChange, _ newValue: Any?, _ oldValue: Any?, _ indexes: IndexSet?, _ isPrior: Bool) -> Void) { + + let _ = _CoreStoreObjectKeyValueObservation.swizzler + self.keyPath = keyPath + self.object = object + self.callback = callback + } + + fileprivate func start(_ options: NSKeyValueObservingOptions) { + + self.object?.addObserver(self, forKeyPath: self.keyPath, options: options, context: nil) + } + + deinit { + + self.object?.removeObserver(self, forKeyPath: self.keyPath, context: nil) + } + + + // MARK: DynamicObjectKeyValueObservation + + public func invalidate() { + + self.object?.removeObserver(self, forKeyPath: self.keyPath, context: nil) + self.object = nil + } + + + // MARK: Private + + // workaround for Erroneous (?) error when using bridging in the Foundation overlay + @nonobjc static var swizzler: Any? = cs_lazy { + + let bridgeClass: AnyClass = _CoreStoreObjectKeyValueObservation.self + let rootObserveImpl = class_getInstanceMethod( + bridgeClass, + #selector(_CoreStoreObjectKeyValueObservation.observeValue(forKeyPath:of:change:context:)) + )! + let swapObserveImpl = class_getInstanceMethod( + bridgeClass, + #selector(_CoreStoreObjectKeyValueObservation._cs_swizzle_me_observeValue(forKeyPath:of:change:context:)) + )! + method_exchangeImplementations(rootObserveImpl, swapObserveImpl) + return nil + } + + @nonobjc private weak var object: CoreStoreManagedObject? + @nonobjc private let callback: (_ object: CoreStoreManagedObject, _ kind: NSKeyValueChange, _ newValue: Any?, _ oldValue: Any?, _ indexes: IndexSet?, _ isPrior: Bool) -> Void + @nonobjc private let keyPath: KeyPathString + + @objc private dynamic func _cs_swizzle_me_observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSString: Any]?, context: UnsafeMutableRawPointer?) { + + guard + let object = object as? CoreStoreManagedObject, + object == self.object, + let change = change + else { + + return + } + let rawKind: UInt = change[NSKeyValueChangeKey.kindKey.rawValue as NSString] as! UInt + self.callback( + object, + NSKeyValueChange(rawValue: rawKind)!, + change[NSKeyValueChangeKey.newKey.rawValue as NSString], + change[NSKeyValueChangeKey.oldKey.rawValue as NSString], + change[NSKeyValueChangeKey.indexesKey.rawValue as NSString] as! IndexSet?, + change[NSKeyValueChangeKey.notificationIsPriorKey.rawValue as NSString] as! Bool? ?? false + ) + } +}