From 843adf21f7aa7eebe7120499312fe464071d2eda Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 18 Feb 2020 18:17:52 +0900 Subject: [PATCH] improved API for custom getters and setters in Field properties --- CoreStore.xcodeproj/project.pbxproj | 10 ++ CoreStoreTests/DynamicModelTests.swift | 68 ++++--- Sources/Field.Coded.swift | 93 +++++----- Sources/Field.Stored.swift | 77 ++++---- Sources/Field.Virtual.swift | 56 +++--- Sources/ObjectProxy.swift | 235 +++++++++++++++++++++++++ 6 files changed, 408 insertions(+), 131 deletions(-) create mode 100644 Sources/ObjectProxy.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 3e52e5a..83eff01 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -189,6 +189,10 @@ B50E17622351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; B50E17632351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; B50E17642351FA66004F033C /* Internals.Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E17602351FA66004F033C /* Internals.Closure.swift */; }; + B50E42F723FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42F823FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42F923FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; + B50E42FA23FBB91800ED476E /* ObjectProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50E42F623FBB91800ED476E /* ObjectProxy.swift */; }; B50EE14223473C92009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; B50EE14323473C96009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; B50EE14423473C97009B8C47 /* CoreStoreObject+DataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */; }; @@ -1025,6 +1029,7 @@ B50E175623517DE4004F033C /* Differentiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Differentiable.swift; sourceTree = ""; }; B50E175B2351848E004F033C /* Internals.DiffableDataUIDispatcher.DiffResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.DiffableDataUIDispatcher.DiffResult.swift; sourceTree = ""; }; B50E17602351FA66004F033C /* Internals.Closure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.Closure.swift; sourceTree = ""; }; + B50E42F623FBB91800ED476E /* ObjectProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectProxy.swift; sourceTree = ""; }; B50EE14123473C92009B8C47 /* CoreStoreObject+DataSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreStoreObject+DataSources.swift"; sourceTree = ""; }; B512607E1E97A18000402229 /* CoreStoreObject+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CoreStoreObject+Convenience.swift"; sourceTree = ""; }; B51260881E9B252B00402229 /* NSEntityDescription+DynamicModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSEntityDescription+DynamicModel.swift"; sourceTree = ""; }; @@ -1646,6 +1651,7 @@ children = ( B5D339D71E9489AB00C880DE /* CoreStoreObject.swift */, B53CA9A11EF1EF1600E0F440 /* PartialObject.swift */, + B50E42F623FBB91800ED476E /* ObjectProxy.swift */, B56E4EC823CD9B2E00E1708C /* Field Properties */, B5831B6E1F3355C300A9F647 /* Legacy Properties */, B52F74391E9B8724005F3DAC /* Dynamic Schema */, @@ -2381,6 +2387,7 @@ B5E84F411AFF8CCD0064E85B /* TypeErasedClauses.swift in Sources */, B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */, B52F74451E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, + B50E42F723FBB91800ED476E /* ObjectProxy.swift in Sources */, B5FAD6AC1B51285300714891 /* Internals.MigrationManager.swift in Sources */, B50EE14223473C92009B8C47 /* CoreStoreObject+DataSources.swift in Sources */, B50C3EE023D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -2632,6 +2639,7 @@ B59851491C90289D00C99590 /* NSPersistentStoreCoordinator+Setup.swift in Sources */, B5E1B5A41CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B596BBB71DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, + B50E42F823FBB91800ED476E /* ObjectProxy.swift in Sources */, B5FEC18F1C9166E600532541 /* NSPersistentStore+Setup.swift in Sources */, 82BA18B71C4BBD3F00A0916E /* CoreStore+Querying.swift in Sources */, B50C3EE123D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -2883,6 +2891,7 @@ B52DD1A51BE1F92F00949AFE /* ImportableUniqueObject.swift in Sources */, B5E222271CA4E12600BA2E95 /* CSSynchronousDataTransaction.swift in Sources */, B52F74481E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, + B50E42FA23FBB91800ED476E /* ObjectProxy.swift in Sources */, B5519A621CA21954002BEF78 /* CSAsynchronousDataTransaction.swift in Sources */, B52DD19C1BE1F92C00949AFE /* Into.swift in Sources */, B50C3EE323D062C300B29880 /* FieldCoderType.swift in Sources */, @@ -3134,6 +3143,7 @@ B52F74471E9B8724005F3DAC /* XcodeDataModelSchema.swift in Sources */, B5FEC1901C9166E700532541 /* NSPersistentStore+Setup.swift in Sources */, B56321A11BD65216006C9394 /* ListMonitor.swift in Sources */, + B50E42F923FBB91800ED476E /* ObjectProxy.swift in Sources */, B5E1B5A51CAA4365007FD580 /* CSCoreStore+Observing.swift in Sources */, B596BBB81DD5BC67001DCDD9 /* FetchableSource.swift in Sources */, B50C3EE223D062C300B29880 /* FieldCoderType.swift in Sources */, diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 5d72143..b98dec5 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -94,22 +94,43 @@ enum Job: String { class Person: CoreStoreObject { - @Field.Stored("title", customSetter: Person.setTitle(_:_:)) + @Field.Stored( + "title", + customSetter: { (object, field, newValue) in + field.primitiveValue = newValue + object.$displayName.primitiveValue = nil + } + ) var title: String = "Mr." - @Field.Stored("name", customSetter: Person.setName(_:_:)) + @Field.Stored( + "name", + customSetter: { (object, field, newValue) in + field.primitiveValue = newValue + object.$displayName.primitiveValue = nil + } + ) var name: String = "" @Field.Virtual( "displayName", - customGetter: Person.getDisplayName(_:), + customGetter: Person.getDisplayName(_:_:), affectedByKeyPaths: Person.keyPathsAffectingDisplayName() ) var displayName: String? @Field.Virtual( "customType", - customGetter: Person.getCustomField(_:) + customGetter: { (object, field) in + + if let value = field.primitiveValue { + + return value + } + let value = CustomType() + field.primitiveValue = value + return value + } ) var customField: CustomType @@ -131,40 +152,17 @@ class Person: CoreStoreObject { @Field.Relationship("_spouseInverse", inverse: \.$spouse) private var spouseInverse: Person? - private static func setTitle(_ partialObject: PartialObject, _ newValue: String) { - - partialObject.setPrimitiveValue(newValue, for: \.$title) - partialObject.setPrimitiveValue(nil, for: \.$displayName) - } - - private static func setName(_ partialObject: PartialObject, _ newValue: String) { - - partialObject.setPrimitiveValue(newValue, for: \.$name) - partialObject.setPrimitiveValue(nil, for: \.$displayName) - } + static func getDisplayName(_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> String? { - static func getCustomField(_ partialObject: PartialObject) -> CustomType { + if let value = field.primitiveValue { - if let customField = partialObject.primitiveValue(for: \.$customField) { - - return customField + return value } - let customField = CustomType() - partialObject.setPrimitiveValue(customField, for: \.$customField) - return customField - } - - static func getDisplayName(_ partialObject: PartialObject) -> String? { - - if let displayName = partialObject.primitiveValue(for: \.$displayName) { - - return displayName - } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) - let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: \.$displayName) - return displayName + let title = object.$title.value + let name = object.$name.value + let value = "\(title) \(name)" + field.primitiveValue = value + return value } static func keyPathsAffectingDisplayName() -> Set { diff --git a/Sources/Field.Coded.swift b/Sources/Field.Coded.swift index f014a11..8b9df88 100644 --- a/Sources/Field.Coded.swift +++ b/Sources/Field.Coded.swift @@ -65,15 +65,15 @@ extension FieldContainer { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -82,8 +82,8 @@ extension FieldContainer { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -92,8 +92,8 @@ extension FieldContainer { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder fieldCoderType: Coder.Type, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) where Coder.FieldStoredValue == V { @@ -116,8 +116,8 @@ extension FieldContainer { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: (encode: (V) -> Data?, decode: (Data?) -> V), - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -209,7 +209,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -237,7 +240,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue(newValue, forKey: keyPath) } @@ -279,7 +286,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - let value = customGetter(PartialObject(rawObject)) + let value = customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) return value } } @@ -300,7 +310,8 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } customSetter( - PartialObject(rawObject), + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), newValue as! V ) } @@ -316,8 +327,8 @@ extension FieldContainer { versionHashModifier: @escaping () -> String?, renamingIdentifier: @escaping () -> String?, valueTransformer: @escaping () -> Internals.AnyFieldCoder?, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -346,8 +357,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -369,15 +380,15 @@ extension FieldContainer.Coded where V: FieldOptionalType { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -386,8 +397,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -396,8 +407,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: Coder.Type, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) where Coder.FieldStoredValue == V.Wrapped { @@ -420,8 +431,8 @@ extension FieldContainer.Coded where V: FieldOptionalType { versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, coder: (encode: (V) -> Data?, decode: (Data?) -> V), - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -457,15 +468,15 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -474,8 +485,8 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -483,8 +494,8 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -512,8 +523,8 @@ extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSS _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { diff --git a/Sources/Field.Stored.swift b/Sources/Field.Stored.swift index ce7a914..09bd681 100644 --- a/Sources/Field.Stored.swift +++ b/Sources/Field.Stored.swift @@ -62,28 +62,26 @@ extension FieldContainer { @Field.Stored("name") var name: String = "" - @Field.Virtual("displayName", customGetter: Person.getName(_:)) - var displayName: String = "" - - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) - if !cachedDisplayName.isEmpty { - return cachedDisplayName + @Field.Virtual( + "displayName", + customGetter: { (object, field) in + if let cached = field.primitiveValue, !cached.isEmpty { + return cached + } + let value = "\(object.$title.value) \(object.$name.value)" + field.primitiveValue = value + return value } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) - let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) - return displayName - } + ) + var displayName: String = "" } ``` - parameter initial: the initial value for the property when the object is first create - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -91,8 +89,8 @@ extension FieldContainer { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = [] ) { @@ -183,7 +181,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -211,7 +212,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue( newValue.cs_toFieldStoredNativeType(), @@ -239,7 +244,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - let value = customGetter(PartialObject(rawObject)) + let value = customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) return value.cs_toFieldStoredNativeType() } } @@ -260,7 +268,8 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } customSetter( - PartialObject(rawObject), + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), V.cs_fromFieldStoredNativeType(newValue as! V.FieldStoredNativeType) ) } @@ -275,8 +284,8 @@ extension FieldContainer { isOptional: Bool, versionHashModifier: @escaping () -> String?, renamingIdentifier: @escaping () -> String?, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -300,8 +309,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -323,15 +332,15 @@ extension FieldContainer.Stored where V: FieldOptionalType { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } @@ -340,8 +349,8 @@ extension FieldContainer.Stored where V: FieldOptionalType { - parameter keyPath: the permanent attribute name for this property. - parameter versionHashModifier: used to mark or denote a property as being a different "version" than another even if all of the values which affect persistence are equal. (Such a difference is important in cases where the properties are unchanged but the format or content of its data are changed.) - parameter previousVersionKeyPath: used to resolve naming conflicts between models. When creating an entity mapping between entities in two managed object models, a source entity property's `keyPath` with a matching destination entity property's `previousVersionKeyPath` indicate that a property mapping should be configured to migrate from the source to the destination. If unset, the identifier will be the property's `keyPath`. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( @@ -349,8 +358,8 @@ extension FieldContainer.Stored where V: FieldOptionalType { _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, previousVersionKeyPath: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( diff --git a/Sources/Field.Virtual.swift b/Sources/Field.Virtual.swift index c4fe361..67f6aa8 100644 --- a/Sources/Field.Virtual.swift +++ b/Sources/Field.Virtual.swift @@ -65,28 +65,28 @@ extension FieldContainer { @Field.Virtual("displayName", customGetter: Person.getName(_:)) var displayName: String = "" - private static func getName(_ partialObject: PartialObject) -> String { - let cachedDisplayName = partialObject.primitiveValue(for: \.$displayName) + private static func getName(_ object: ObjectProxy) -> String { + let cachedDisplayName = object.primitiveValue(for: \.$displayName) if !cachedDisplayName.isEmpty { return cachedDisplayName } - let title = partialObject.value(for: \.$title) - let name = partialObject.value(for: \.$name) + let title = object.value(for: \.$title) + let name = object.value(for: \.$name) let displayName = "\(title) \(name)" - partialObject.setPrimitiveValue(displayName, for: { $0.displayName }) + object.setPrimitiveValue(displayName, for: { $0.displayName }) return displayName } } ``` - parameter keyPath: the permanent attribute name for this property. - - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.primitiveValue(for:)` instead of `PartialObject.value(for:)`, which would unintentionally execute the same closure again recursively. - - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `PartialObject`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `PartialObject`, make sure to use `PartialObject.setPrimitiveValue(_:for:)` instead of `PartialObject.setValue(_:for:)`, which would unintentionally execute the same closure again recursively. + - parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.primitiveValue(for:)` instead of `ObjectProxy.value(for:)`, which would unintentionally execute the same closure again recursively. + - parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info everytime KVO invokes this accessor method incurs a cumulative performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would unintentionally execute the same closure again recursively. - parameter affectedByKeyPaths: a set of key paths for properties whose values affect the value of the receiver. This is similar to `NSManagedObject.keyPathsForValuesAffectingValue(forKey:)`. */ public init( _ keyPath: KeyPathString, - customGetter: @escaping (_ partialObject: PartialObject) -> V, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: @escaping (_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( @@ -106,8 +106,8 @@ extension FieldContainer { wrappedValue initial: @autoclosure @escaping () -> V, _ keyPath: KeyPathString, versionHashModifier: @autoclosure @escaping () -> String? = nil, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { fatalError() @@ -188,7 +188,10 @@ extension FieldContainer { let field = field as! Self if let customGetter = field.customGetter { - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field) + ) } let keyPath = field.keyPath switch rawObject.value(forKey: keyPath) { @@ -216,7 +219,11 @@ extension FieldContainer { let keyPath = field.keyPath if let customSetter = field.customSetter { - return customSetter(PartialObject(rawObject), newValue) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: field), + newValue + ) } return rawObject.setValue(newValue, forKey: keyPath) } @@ -241,7 +248,10 @@ extension FieldContainer { rawObject.didAccessValue(forKey: keyPath) } - return customGetter(PartialObject(rawObject)) + return customGetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self) + ) } } @@ -260,7 +270,11 @@ extension FieldContainer { rawObject.didChangeValue(forKey: keyPath) } - return customSetter(PartialObject(rawObject), newValue as! V) + return customSetter( + ObjectProxy(rawObject), + ObjectProxy.FieldProxy(rawObject: rawObject, field: self), + newValue as! V + ) } } @@ -270,8 +284,8 @@ extension FieldContainer { fileprivate init( keyPath: KeyPathString, isOptional: Bool, - customGetter: ((_ partialObject: PartialObject) -> V)?, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? , + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)?, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? , affectedByKeyPaths: @escaping () -> Set) { self.keyPath = keyPath @@ -295,8 +309,8 @@ extension FieldContainer { // MARK: Private - private let customGetter: ((_ partialObject: PartialObject) -> V)? - private let customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? + private let customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? + private let customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? } } @@ -305,8 +319,8 @@ extension FieldContainer.Virtual where V: FieldOptionalType { public init( _ keyPath: KeyPathString, - customGetter: ((_ partialObject: PartialObject) -> V)? = nil, - customSetter: ((_ partialObject: PartialObject, _ newValue: V) -> Void)? = nil, + customGetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> V)? = nil, + customSetter: ((_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy, _ newValue: V) -> Void)? = nil, affectedByKeyPaths: @autoclosure @escaping () -> Set = []) { self.init( diff --git a/Sources/ObjectProxy.swift b/Sources/ObjectProxy.swift new file mode 100644 index 0000000..7f54b22 --- /dev/null +++ b/Sources/ObjectProxy.swift @@ -0,0 +1,235 @@ +// +// ObjectProxy.swift +// CoreStore +// +// Copyright © 2020 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import CoreData +import Foundation + + +// MARK: - ObjectProxy + +/** + An `ObjectProxy` is only used when overriding getters and setters for `CoreStoreObject` properties. Custom getters and setters are implemented as a closure that "overrides" the default property getter/setter. The closure receives an `ObjectProxy`, which acts as a fast, type-safe KVC interface for `CoreStoreObject`. The reason a `CoreStoreObject` instance is not passed directly is because the Core Data runtime is not aware of `CoreStoreObject` properties' static typing, and so loading those info every time KVO invokes this accessor method incurs a heavy performance hit (especially in KVO-heavy operations such as `ListMonitor` observing.) When accessing the property value from `ObjectProxy`, make sure to use `ObjectProxy.$property.primitiveValue` instead of `ObjectProxy.$property.value`, which would execute the same accessor again recursively. + */ +@dynamicMemberLookup +public struct ObjectProxy { + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Stored>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Virtual>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + /** + Returns the value for the property identified by a given key. + */ + public subscript(dynamicMember member: KeyPath.Coded>) -> FieldProxy { + + return .init(rawObject: self.rawObject, keyPath: member) + } + + + // MARK: Internal + + internal let rawObject: CoreStoreManagedObject + + internal init(_ rawObject: CoreStoreManagedObject) { + + self.rawObject = rawObject + } + + + // MARK: - FieldProxy + + public struct FieldProxy { + + // MARK: Public + + /** + Returns the value for the specified property from the object’s private internal storage. + + Accessing this property does not invoke the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). This method is used primarily to implement custom accessor methods that need direct access to the object's private storage. + */ + public var primitiveValue: V? { + + get { + + return self.getPrimitiveValue() + } + nonmutating set { + + self.setPrimitiveValue(newValue) + } + } + + /** + Returns the value for the property identified by a given key. + + Accessing this property triggers the access notification methods (`willAccessValue(forKey:)` and `didAccessValue(forKey:)`). + */ + public var value: V { + + get { + + return self.getValue() + } + nonmutating set { + + self.setValue(newValue) + } + } + + + // MARK: Internal + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Stored>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Virtual>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, keyPath: KeyPath.Coded>) { + + self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath]) + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Stored) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Stored.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Stored.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + return V.cs_fromFieldStoredNativeType( + rawObject.primitiveValue(forKey: keyPathString) as! V.FieldStoredNativeType + ) + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0.cs_toFieldStoredNativeType(), + forKey: keyPathString + ) + } + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Virtual) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Virtual.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Virtual.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + switch rawObject.primitiveValue(forKey: keyPathString) { + + case let value as V: + return value + + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil + } + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0, + forKey: keyPathString + ) + } + } + + internal init(rawObject: CoreStoreManagedObject, field: FieldContainer.Coded) { + + let keyPathString = field.keyPath + self.getValue = { + + return FieldContainer.Coded.read(field: field, for: rawObject) as! V + } + self.setValue = { + + FieldContainer.Coded.modify(field: field, for: rawObject, newValue: $0) + } + self.getPrimitiveValue = { + + switch rawObject.primitiveValue(forKey: keyPathString) { + + case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: + rawObject.setPrimitiveValue(valueBox.value, forKey: keyPathString) + return valueBox.value as? V + + case let value as V: + return value + + case nil, + is NSNull, + _? /* any other unrelated type */ : + return nil + } + } + self.setPrimitiveValue = { + + rawObject.setPrimitiveValue( + $0, + forKey: keyPathString + ) + } + } + + + // MARK: Private + + private let getValue: () -> V + private let setValue: (V) -> Void + private let getPrimitiveValue: () -> V? + private let setPrimitiveValue: (V?) -> Void + } +}