Compare commits

...

10 Commits
7.1.0 ... 7.2.0

Author SHA1 Message Date
John Estropia
e720504855 Update README.md 2020-06-20 17:37:12 +09:00
John Estropia
ee51c5ad9c fix readme indentations 2020-06-20 16:47:52 +09:00
John Estropia
4ac7df7364 version bump 2020-06-20 13:16:14 +09:00
John Estropia
56f873ccb3 fix readme table of contents 2020-06-20 13:12:11 +09:00
John Estropia
1f90f846f3 readme update 2020-06-20 13:07:10 +09:00
John Estropia
4a97d5a8dc remove persistentstores during DataStack deinitialization to prevent warnings in Unit tests during teardown 2020-05-26 09:43:39 +09:00
John Estropia
0eb9b6393d add unit testst for Field dynamic initializers 2020-05-24 10:54:16 +09:00
John Estropia
56d0ea46ea Implement dynamic initializers for Field properties (fixes #382) 2020-05-23 12:07:16 +09:00
John Estropia
a7568eebdb fix comments 2020-04-15 16:49:45 +09:00
John Estropia
4049e1944a trigger lazy initialization of ObjectPublisher observation after addObserver() (fixes #383) 2020-04-15 16:48:42 +09:00
27 changed files with 833 additions and 225 deletions

View File

@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "CoreStore"
s.version = "7.1.0"
s.version = "7.2.0"
s.swift_version = "5.2"
s.license = "MIT"
s.homepage = "https://github.com/JohnEstropia/CoreStore"

View File

@@ -3413,7 +3413,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 7.0.4;
MARKETING_VERSION = 7.2.0;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,
@@ -3436,7 +3436,7 @@
DYLIB_INSTALL_NAME_BASE = "@rpath";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 7.0.4;
MARKETING_VERSION = 7.2.0;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,
@@ -3496,6 +3496,7 @@
GCC_NO_COMMON_BLOCKS = YES;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 7.2.0;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,
@@ -3521,6 +3522,7 @@
GCC_NO_COMMON_BLOCKS = YES;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 7.2.0;
OTHER_LDFLAGS = (
"-weak_framework",
Combine,

View File

@@ -19,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
return true
}
}

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Ni8-QF-XHB">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Ni8-QF-XHB">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -705,7 +705,7 @@
<color key="barTintColor" red="0.15542715787887573" green="0.2203737199306488" blue="0.2959403395652771" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<textAttributes key="titleTextAttributes">
<fontDescription key="fontDescription" name="HelveticaNeue-UltraLight" family="Helvetica Neue" pointSize="24"/>
<color key="textColor" red="0.90744441747665405" green="0.9265514612197876" blue="0.93116652965545654" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="textColor" red="0.90588235294117647" green="0.92549019607843142" blue="0.92941176470588238" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</textAttributes>
</navigationBar>
<nil name="viewControllers"/>
@@ -1173,10 +1173,10 @@
<point key="canvasLocation" x="4404" y="1484"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="hyN-De-zte"/>
</inferredMetricsTieBreakers>
<resources>
<image name="second" width="30" height="30"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="k4E-dJ-1lz"/>
</inferredMetricsTieBreakers>
</document>

View File

@@ -146,8 +146,7 @@ final class CollectionViewDemoViewController: UICollectionViewController {
ColorsDemo.stack.perform(
asynchronous: { (transaction) in
let palette = transaction.create(Into<Palette>())
palette.setInitialValues(in: transaction)
_ = transaction.create(Into<Palette>())
},
completion: { _ in }
)
@@ -159,8 +158,8 @@ final class CollectionViewDemoViewController: UICollectionViewController {
for palette in try transaction.fetchAll(From<Palette>()) {
palette.hue .= Palette.randomHue()
palette.colorName .= nil
palette.hue = Palette.randomHue()
palette.colorName = nil
}
},
completion: { _ in }

View File

@@ -35,8 +35,8 @@ struct ColorsDemo {
switch self {
case .all: return .init()
case .light: return (\Palette.brightness >= 0.9)
case .dark: return (\Palette.brightness <= 0.4)
case .light: return (\Palette.$brightness >= 0.9)
case .dark: return (\Palette.$brightness <= 0.4)
}
}
}
@@ -47,9 +47,9 @@ struct ColorsDemo {
try! self.palettes.refetch(
From<Palette>()
.sectionBy(\.colorName)
.sectionBy(\.$colorName)
.where(self.filter.whereClause())
.orderBy(.ascending(\.hue))
.orderBy(.ascending(\.$hue))
)
}
}
@@ -81,8 +81,8 @@ struct ColorsDemo {
return ColorsDemo.stack.publishList(
From<Palette>()
.sectionBy(\.colorName)
.orderBy(.ascending(\.hue))
.sectionBy(\.$colorName)
.orderBy(.ascending(\.$hue))
)
}()
}

View File

@@ -158,8 +158,7 @@ final class ListObserverDemoViewController: UITableViewController {
ColorsDemo.stack.perform(
asynchronous: { (transaction) in
let palette = transaction.create(Into<Palette>())
palette.setInitialValues(in: transaction)
_ = transaction.create(Into<Palette>())
},
completion: { _ in }
)
@@ -171,8 +170,8 @@ final class ListObserverDemoViewController: UITableViewController {
for palette in try transaction.fetchAll(From<Palette>()) {
palette.hue .= Palette.randomHue()
palette.colorName .= nil
palette.hue = Palette.randomHue()
palette.colorName = nil
}
},
completion: { _ in }

View File

@@ -43,7 +43,7 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
required init?(coder aDecoder: NSCoder) {
if let palette = try! ColorsDemo.stack.fetchOne(From<Palette>().orderBy(.ascending(\.hue))) {
if let palette = try! ColorsDemo.stack.fetchOne(From<Palette>().orderBy(.ascending(\.$hue))) {
self.monitor = ColorsDemo.stack.monitorObject(palette)
}
@@ -52,12 +52,11 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
_ = try? ColorsDemo.stack.perform(
synchronous: { (transaction) in
let palette = transaction.create(Into<Palette>())
palette.setInitialValues(in: transaction)
_ = transaction.create(Into<Palette>())
}
)
let palette = try! ColorsDemo.stack.fetchOne(From<Palette>().orderBy(.ascending(\.hue)))!
let palette = try! ColorsDemo.stack.fetchOne(From<Palette>().orderBy(.ascending(\.$hue)))!
self.monitor = ColorsDemo.stack.monitorObject(palette)
}
@@ -119,7 +118,7 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
if let palette = transaction.edit(self?.monitor?.object) {
palette.hue .= Int(hue)
palette.hue = Int(hue)
}
},
completion: { _ in }
@@ -134,7 +133,7 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
if let palette = transaction.edit(self?.monitor?.object) {
palette.saturation .= saturation
palette.saturation = saturation
}
},
completion: { _ in }
@@ -149,7 +148,7 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
if let palette = transaction.edit(self?.monitor?.object) {
palette.brightness .= brightness
palette.brightness = brightness
}
},
completion: { _ in }
@@ -169,7 +168,7 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
func reloadPaletteInfo(_ palette: Palette, changedKeys: Set<String>?) {
self.colorNameLabel?.text = palette.colorName.value
self.colorNameLabel?.text = palette.colorName
let color = palette.color
self.colorNameLabel?.textColor = color
@@ -177,17 +176,17 @@ class ObjectObserverDemoViewController: UIViewController, ObjectObserver {
self.hsbLabel?.text = palette.colorText
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.hue)) == true {
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.$hue)) == true {
self.hueSlider?.value = Float(palette.hue.value)
self.hueSlider?.value = Float(palette.hue)
}
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.saturation)) == true {
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.$saturation)) == true {
self.saturationSlider?.value = palette.saturation.value
self.saturationSlider?.value = palette.saturation
}
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.brightness)) == true {
if changedKeys == nil || changedKeys?.contains(String(keyPath: \Palette.$brightness)) == true {
self.brightnessSlider?.value = palette.brightness.value
self.brightnessSlider?.value = palette.brightness
}
}
}

View File

@@ -16,44 +16,53 @@ import CoreStore
final class Palette: CoreStoreObject {
let hue = Value.Required<Int>("hue", initial: 0)
let saturation = Value.Required<Float>("saturation", initial: 0)
let brightness = Value.Required<Float>("brightness", initial: 0)
let colorName = Value.Optional<String>(
"colorName",
isTransient: true,
customGetter: Palette.getColorName
@Field.Stored(
"hue",
dynamicInitialValue: { Palette.randomHue() }
)
var hue: Int
static func randomHue() -> Int {
return Int(arc4random_uniform(360))
}
@Field.Stored("saturation")
var saturation: Float = 1
private static func getColorName(_ partialObject: PartialObject<Palette>) -> String? {
if let colorName = partialObject.primitiveValue(for: { $0.colorName }) {
@Field.Stored(
"brightness",
dynamicInitialValue: { Palette.randomBrightness() }
)
var brightness: Float
@Field.Virtual(
"colorName",
customGetter: { object, field in
if let colorName = field.primitiveValue {
return colorName
}
let colorName: String
switch object.$hue.value % 360 {
case 0 ..< 20: colorName = "Lower Reds"
case 20 ..< 57: colorName = "Oranges and Browns"
case 57 ..< 90: colorName = "Yellow-Greens"
case 90 ..< 159: colorName = "Greens"
case 159 ..< 197: colorName = "Blue-Greens"
case 197 ..< 241: colorName = "Blues"
case 241 ..< 297: colorName = "Violets"
case 297 ..< 331: colorName = "Magentas"
default: colorName = "Upper Reds"
}
field.primitiveValue = colorName
return colorName
}
)
var colorName: String!
static func randomHue() -> Int {
let colorName: String
switch partialObject.value(for: { $0.hue }) % 360 {
case 0 ..< 20: colorName = "Lower Reds"
case 20 ..< 57: colorName = "Oranges and Browns"
case 57 ..< 90: colorName = "Yellow-Greens"
case 90 ..< 159: colorName = "Greens"
case 159 ..< 197: colorName = "Blue-Greens"
case 197 ..< 241: colorName = "Blues"
case 241 ..< 297: colorName = "Violets"
case 297 ..< 331: colorName = "Magentas"
default: colorName = "Upper Reds"
}
return Int.random(in: 0 ..< 360)
}
static func randomBrightness() -> Float {
partialObject.setPrimitiveValue(colorName, for: { $0.colorName })
return colorName
return (Float.random(in: 0 ..< 70) + 30) / 100.0
}
}
@@ -62,25 +71,15 @@ extension Palette {
var color: UIColor {
return UIColor(
hue: CGFloat(self.hue.value) / 360.0,
saturation: CGFloat(self.saturation.value),
brightness: CGFloat(self.brightness.value),
hue: CGFloat(self.hue) / 360.0,
saturation: CGFloat(self.saturation),
brightness: CGFloat(self.brightness),
alpha: 1.0
)
}
var colorText: String {
return "H: \(self.hue.value)˚, S: \(round(self.saturation.value * 100.0))%, B: \(round(self.brightness.value * 100.0))%"
}
}
extension Palette {
func setInitialValues(in transaction: BaseDataTransaction) {
self.hue .= Palette.randomHue()
self.saturation .= Float(1.0)
self.brightness .= Float(arc4random_uniform(70) + 30) / 100.0
return "H: \(self.hue)˚, S: \(round(self.saturation * 100.0))%, B: \(round(self.brightness * 100.0))%"
}
}

View File

@@ -33,8 +33,8 @@ final class SwiftUIContainerViewController: UIViewController {
rootView: SwiftUIView(
palettes: ColorsDemo.stack.publishList(
From<Palette>()
.sectionBy(\.colorName)
.orderBy(.ascending(\.hue))
.sectionBy(\.$colorName)
.orderBy(.ascending(\.$hue))
)
)
.environment(\.dataStack, ColorsDemo.stack)

View File

@@ -61,8 +61,8 @@ struct SwiftUIView: View {
for palette in try transaction.fetchAll(From<Palette>()) {
palette.hue .= Palette.randomHue()
palette.colorName .= nil
palette.hue = Palette.randomHue()
palette.colorName = nil
}
},
completion: { _ in }
@@ -79,8 +79,7 @@ struct SwiftUIView: View {
self.dataStack.perform(
asynchronous: { transaction in
let palette = transaction.create(Into<Palette>())
palette.setInitialValues(in: transaction)
_ = transaction.create(Into<Palette>())
},
completion: { _ in }
)
@@ -173,8 +172,8 @@ struct SwiftUIView_Previews: PreviewProvider {
SwiftUIView(
palettes: ColorsDemo.stack.publishList(
From<Palette>()
.sectionBy(\.colorName)
.orderBy(.ascending(\.hue))
.sectionBy(\.$colorName)
.orderBy(.ascending(\.$hue))
)
)
.environment(\.dataStack, ColorsDemo.stack)

View File

@@ -62,6 +62,9 @@ class BaseTestCase: XCTestCase {
XCTFail(error.coreStoreDumpString)
}
self.addTeardownBlock {
stack.unsafeRemoveAllPersistentStoresAndWait()
}
}
@nonobjc

View File

@@ -49,9 +49,16 @@ class Animal: CoreStoreObject {
}
class Dog: Animal {
static let commonNicknames = ["Spot", "Benjie", "Max", "Milo"]
@Field.Stored("nickname")
var nickname: String?
@Field.Stored(
"nickname",
dynamicInitialValue: {
commonNicknames[.random(in: commonNicknames.indices)]
}
)
var nickname: String
@Field.Stored("age")
var age: Int = 1
@@ -67,7 +74,7 @@ struct CustomType {
var string = "customString"
}
enum Job: String {
enum Job: String, CaseIterable {
case unemployed
case engineer
@@ -139,9 +146,12 @@ class Person: CoreStoreObject {
coder: (
encode: { $0.toData() },
decode: { $0.flatMap(Job.init(data:)) ?? .unemployed }
)
),
dynamicInitialValue: {
Job.allCases[.random(in: Job.allCases.indices)]
}
)
var job: Job = .unemployed
var job: Job
@Field.Relationship("spouse")
var spouse: Person?
@@ -152,7 +162,7 @@ class Person: CoreStoreObject {
@Field.Relationship("_spouseInverse", inverse: \.$spouse)
private var spouseInverse: Person?
static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
if let value = field.primitiveValue {
@@ -165,7 +175,7 @@ class Person: CoreStoreObject {
return value
}
static func keyPathsAffectingDisplayName() -> Set<String> {
private static func keyPathsAffectingDisplayName() -> Set<String> {
return [
String(keyPath: \Person.$title),
@@ -192,7 +202,7 @@ class DynamicModelTests: BaseTestDataTestCase {
],
versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
"Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992]
]
)
@@ -247,8 +257,8 @@ class DynamicModelTests: BaseTestDataTestCase {
let dog = transaction.create(Into<Dog>())
XCTAssertEqual(dog.species, "Swift")
XCTAssertEqual(dog.nickname, nil)
XCTAssertEqual(dog.age, 1)
XCTAssertTrue(Dog.commonNicknames.contains(dog.nickname))
for property in Dog.metaProperties(includeSuperclasses: true) {
@@ -264,7 +274,7 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
case String(keyPath: \Dog.$nickname):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String?>)
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String>)
case String(keyPath: \Dog.$age):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<Int>)
@@ -335,7 +345,8 @@ class DynamicModelTests: BaseTestDataTestCase {
let person = transaction.create(Into<Person>())
XCTAssertTrue(person.pets.isEmpty)
XCTAssertEqual(person.customField.string, "customString")
XCTAssertEqual(person.job, .unemployed)
let initialJob = person.job
XCTAssertTrue(Job.allCases.contains(initialJob))
XCTAssertEqual(
person.rawObject!
@@ -385,7 +396,7 @@ class DynamicModelTests: BaseTestDataTestCase {
personSnapshot3.$name = "James"
XCTAssertEqual(personSnapshot1.$name, "John")
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
XCTAssertEqual(personSnapshot1.$job, .unemployed)
XCTAssertEqual(personSnapshot1.$job, initialJob)
XCTAssertEqual(personSnapshot2.$name, "John")
XCTAssertEqual(personSnapshot2.$displayName, "Sir John")
XCTAssertEqual(personSnapshot2.$job, .engineer)
@@ -494,7 +505,12 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTFail()
}
)
self.waitAndCheckExpectations()
}
self.waitForExpectations(timeout: 10, handler: { _ in })
self.addTeardownBlock {
dataStack.unsafeRemoveAllPersistentStoresAndWait()
}
}

View File

@@ -132,8 +132,8 @@ final class FetchTests: BaseTestDataTestCase {
}
)
}
self.waitAndCheckExpectations()
}
self.waitAndCheckExpectations()
}
@objc
@@ -267,8 +267,8 @@ final class FetchTests: BaseTestDataTestCase {
}
)
}
self.waitAndCheckExpectations()
}
self.waitAndCheckExpectations()
}
@objc

View File

@@ -144,6 +144,7 @@ class ObjectPublisherTests: BaseTestDataTestCase {
XCTFail()
}
)
self.waitAndCheckExpectations()
withExtendedLifetime(objectPublisher, {})

View File

@@ -622,8 +622,8 @@ final class TransactionTests: BaseTestCase {
}
)
}
self.waitAndCheckExpectations()
}
self.waitAndCheckExpectations()
}
@objc
@@ -755,8 +755,8 @@ final class TransactionTests: BaseTestCase {
}
)
}
self.waitAndCheckExpectations()
}
self.waitAndCheckExpectations()
}
@objc
@@ -896,8 +896,8 @@ final class TransactionTests: BaseTestCase {
}
)
}
self.waitAndCheckExpectations()
}
self.waitAndCheckExpectations()
}
@objc

301
README.md
View File

@@ -33,7 +33,7 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep
### Features
- 🆕**Backwards-portable DiffableDataSources implementation!** `UITableViews` and `UICollectionViews` now have a new ally: `ListPublisher`s provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to `UITableViews` and `UICollectionViews` reload errors!
- **Backwards-portable DiffableDataSources implementation!** `UITableViews` and `UICollectionViews` now have a new ally: `ListPublisher`s provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to `UITableViews` and `UICollectionViews` reload errors!
- **💎Tight design around Swifts code elegance and type safety.** CoreStore fully utilizes Swift's community-driven language features.
- **🚦Safer concurrency architecture.** CoreStore makes it hard to fall into common concurrency mistakes. The main `NSManagedObjectContext` is strictly read-only, while all updates are done through serial *transactions*. *(See [Saving and processing transactions](#saving-and-processing-transactions))*
- **🔍Clean fetching and querying API.** Fetching objects is easy, but querying for raw aggregates (`min`, `max`, etc.) and raw property values is now just as convenient. *(See [Fetching and querying](#fetching-and-querying))*
@@ -88,12 +88,18 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep
- [Logging and error reporting](#logging-and-error-reporting)
- [Observing changes and notifications](#observing-changes-and-notifications)
- [Observe a single property](#observe-a-single-property)
- 🆕[Observe a single object's updates](#observe-a-single-objects-updates)
- [Observe a single object's updates](#observe-a-single-objects-updates)
- [Observe a single object's per-property updates](#observe-a-single-objects-per-property-updates)
- 🆕[Observe a diffable list](#observe-a-diffable-list)
- [Observe a diffable list](#observe-a-diffable-list)
- [Observe detailed list changes](#observe-detailed-list-changes)
- [Objective-C support](#objective-c-support)
- [Type-safe `CoreStoreObject`s](#type-safe-corestoreobjects)
- 🆕[New `@Field` Property Wrapper syntax](#new-field-property-wrapper-syntax)
- 🆕[`@Field.Stored` ](#fieldstored)
- 🆕[`@Field.Virtual` ](#fieldvirtual)
- 🆕[`@Field.Coded` ](#fieldcoded)
- 🆕[`@Field.Relationship` ](#fieldrelationship)
- 🆕[`@Field` usage notes](#field-usage-notes)
- [`VersionLock`s](#versionlocks)
- [Roadmap](#roadmap)
- [Installation](#installation)
@@ -1729,31 +1735,35 @@ To use these syntax sugars, include *CoreStoreBridge.h* in your Objective-C sour
Starting CoreStore 4.0, we can now create persisted objects without depending on *.xcdatamodeld* Core Data files. The new `CoreStoreObject` subclass replaces `NSManagedObject`, and specially-typed properties declared on these classes will be synthesized as Core Data attributes.
```swift
class Animal: CoreStoreObject {
let species = Value.Required<String>("species", initial: "")
@Field.Stored("species")
var species: String = ""
}
class Dog: Animal {
let nickname = Value.Optional<String>("nickname")
let master = Relationship.ToOne<Person>("master")
@Field.Stored("nickname")
var nickname: String?
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
let name = Value.Required<String>("name", initial: "")
let pets = Relationship.ToManyUnordered<Dog>("pets", inverse: { $0.master })
@Field.Stored("name")
var name: String = ""
@Field.Relationship("pets", inverse: \Dog.$master)
var pets: Set<Dog>
}
```
The property names to be saved to Core Data is specified as the `keyPath` argument. This lets us refactor our Swift code without affecting the underlying database. For example:
```swift
class Person: CoreStoreObject {
private let _name = Value.Required<String>("name", initial: "")
@Field.Stored("name")
private var internalName: String = ""
// note property name is independent of the storage key name
}
```
Here we added an underscore to the property name and made it `private`, but the underlying key-path `"name"` was unchanged so our model will not trigger a data migration.
> ⚠️**Important:** As a rule, CoreStore can only process *stored properties*. Computed, `static`, `weak`, or `lazy` properties will be ignored and will not be added to the store. It is also strictly advised use `let` instead of `var` to declare these properties, as any changes to the property value will break the schema.
Also note how `Relationship`s are linked statically with the `inverse:` argument. **All relationships are required to have an "inverse" relationship**. Unfortunately, due to Swift compiler limitation we can only declare the `inverse:` on one end of the relationship-pair.
Here we used the property name `internalName` and made it `private`, but the underlying key-path `"name"` was unchanged so our model will not trigger a data migration.
To tell the `DataStack` about these types, add all `CoreStoreObject`s' entities to a `CoreStoreSchema`:
```swift
@@ -1772,37 +1782,250 @@ CoreStoreDefaults.dataStack.addStorage(/* ... */)
```
And that's all CoreStore needs to build the model; **we don't need *.xcdatamodeld* files anymore.**
These special properties' values can be accessed or mutated using `.value`:
In addition, `@Field` properties can be used to create type-safe key-path strings
```swift
dataStack.perform(
asynchronous: { (transaction) in
let dog: Dog = transaction.fetchOne(From<Dog>())!
// ...
let nickname = dog.nickname.value // String?
let species = dog.species.value // String
let age = dog.age.value // Int
// ...
dog.age.value = age + 1
},
completion: { /* ... */ }
)
```
In addition, `Value` and `Relationship` properties can be used to create type-safe key-paths
```swift
let keyPath: String = Dog.keyPath { $0.nickname }
let keyPath = String(keyPath: \Dog.$nickname)
```
as well as `Where` and `OrderBy` clauses
```swift
let puppies = try dataStack.fetchAll(
From<Dog>()
.where(\.age < 1)
.orderBy(.ascending(\.age))
.where(\.$age < 5)
.orderBy(.ascending(\.$age))
)
```
All CoreStore APIs that are usable with `NSManagedObject`s are also available for `CoreStoreObject`s. These include `ListMonitor`s, `ImportableObject`s, fetching, etc.
### New `@Field` Property Wrapper syntax
> ⚠️**Important:** `@Field` properties are only supported for `CoreStoreObject` subclasses. If you are using `NSManagedObject`s, you need to keep using `@NSManaged` for your attributes.
Starting CoreStore 7.1.0, `CoreStoreObject` properties may be converted to `@Field` Property Wrappers.
> ‼️ Please take note of the warnings below before converting or else the model's hash might change.
**If conversion is too risky, the current `Value.Required`, `Value.Optional`, `Transformable.Required`, `Transformable.Optional`, `Relationship.ToOne`, `Relationship.ToManyOrdered`, and `Relationship.ToManyUnordered` will all be supported for while so you can opt to use them as is for now.**
> ‼️ This cannot be stressed enough, but please make sure to set your schema's [`VersionLock`](#versionlocks) before converting!
#### `@Field.Stored`
The `@Field.Stored` property wrapper is used for persisted value types. This is the replacement for "non-transient" `Value.Required` and `Value.Optional` properties.
<table>
<tr><th>Before</th><th>`@Field.Stored`</th></tr>
<tr>
<td><pre lang=swift>
class Person: CoreStoreObject {
<br />
let title = Value.Required<String>("title", initial: "Mr.")
let nickname = Value.Optional<String>("nickname")
}
</pre></td>
<td><pre lang=swift>
class Person: CoreStoreObject {
<br />
@Field.Stored("title")
var title: String = "Mr."
<br />
@Field.Stored("nickname")
var nickname: String?
}
</pre></td>
</tr>
</table>
> ⚠️ Only `Value.Required` and `Value.Optional` that are NOT transient values can be converted to `Field.Stored`. For transient/computed properties, refer to [`@Field.Virtual`](#fieldvirtual) properties in the next section.
> ⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
#### `@Field.Virtual`
The `@Field.Virtual` property wrapper is used for unsaved, computed value types. This is the replacement for "transient" `Value.Required` and `Value.Optional` properties.
<table>
<tr><th>Before</th><th>`@Field.Virtual`</th></tr>
<tr>
<td><pre lang=swift>
class Animal: CoreStoreObject {
<br />
let speciesPlural = Value.Required<String>(
"speciesPlural",
transient: true,
customGetter: Animal.getSpeciesPlural(_:)
)
<br />
let species = Value.Required<String>("species", initial: "")
<br />
static func getSpeciesPlural(_ partialObject: PartialObject<Animal>) -> String? {
let species = partialObject.value(for: { $0.species })
return species + "s"
}
}
</pre></td>
<td><pre lang=swift>
class Animal: CoreStoreObject {
<br />
@Field.Virtual(
"speciesPlural",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var speciesPlural: String
<br />
@Field.Stored("species")
var species: String = ""
}
</pre></td>
</tr>
</table>
> ⚠️ Only `Value.Required` and `Value.Optional` that ARE transient values can be converted to `Field.Virtual`. For non-transient properties, refer to [`@Field.Stored`](#fieldstored) properties in the previous section.
> ⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
#### `@Field.Coded`
The `@Field.Coded` property wrapper is used for binary-codable values. This is the new counterpart, **not replacement**, for `Transformable.Required` and `Transformable.Optional` properties. `@Field.Coded` also supports other encodings such as JSON and custom binary converters.
> ‼️ The current `Transformable.Required` and `Transformable.Optional` mechanism have no safe one-to-one conversion to `@Field.Coded`. Please use `@Field.Coded` only for newly added attributes.
<table>
<tr><th>Before</th><th>`@Field.Coded`</th></tr>
<tr>
<td><pre lang=swift>
class Vehicle: CoreStoreObject {
<br />
let color = Transformable.Optional<UIColor>("color", initial: .white)
}
</pre></td>
<td><pre lang=swift>
class Vehicle: CoreStoreObject {
<br />
@Field.Coded("color", coder: FieldCoders.NSCoding.self)
var color: UIColor? = .white
}
</pre></td>
</tr>
</table>
Built-in encoders such as `FieldCoders.NSCoding`, `FieldCoders.Json`, and `FieldCoders.Plist` are available, and custom encoding/decoding is also supported:
```swift
class Person: CoreStoreObject {
<br />
struct CustomInfo: Codable {
// ...
}
<br />
@Field.Coded("otherInfo", coder: FieldCoders.Json.self)
var otherInfo: CustomInfo?
<br />
@Field.Coded(
"photo",
coder: {
encode: { $0.toData() },
decode: { Photo(fromData: $0) }
}
)
var photo: Photo?
}
```
> ‼️**Important:** Any changes in the encoders/decoders are not reflected in the `VersionLock`, so make sure that the encoder and decoder logic is compatible for all versions of your persistent store.
#### `@Field.Relationship`
The `@Field.Relationship` property wrapper is used for link relationships with other `CoreStoreObject`s. This is the replacement for `Relationship.ToOne`, `Relationship.ToManyOrdered`, and `Relationship.ToManyUnordered` properties.
The type of relationship is determined by the `@Field.Relationship` generic type:
- `Optional<T>` : To-one relationship
- `Array<T>` : To-many ordered relationship
- `Set<T>` : To-many unordered relationship
<table>
<tr><th>Before</th><th>`@Field.Stored`</th></tr>
<tr>
<td><pre lang=swift>
class Pet: CoreStoreObject {
<br />
let master = Relationship.ToOne<Person>("master")
}
class Person: CoreStoreObject {
<br />
let pets: Relationship.ToManyUnordered<Pet>("pets", inverse: \.$master)
}
</pre></td>
<td><pre lang=swift>
class Pet: CoreStoreObject {
<br />
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
<br />
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Pet>
}
</pre></td>
</tr>
</table>
> ⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
Also note how `Relationship`s are linked statically with the `inverse:` argument. **All relationships are required to have an "inverse" relationship**. Unfortunately, due to Swift compiler limitation we can declare the `inverse:` on only one of the relationship-pair.
#### `@Field` usage notes
**Accessor syntax**
When using key-path utilities, properties using `@Field` property wrappers need to use the `$` syntax:
- Before: `From<Person>.where(\.title == "Mr.")`
- After: `From<Person>.where(\.$title == "Mr.")`
This applies to property access using `ObjectPublisher`s and `ObjectSnapshot`s.
- Before: `let name = personSnapshot.name`
- After: `let name = personSnapshot.$name`
**Default values vs. Initial values**
One common mistake when assigning default values to `CoreStoreObject` properties is to assign it a value and expect it to be evaluated whenever an object is created:
```swift
//
class Person: CoreStoreObject {
@Field.Stored("identifier")
var identifier: UUID = UUID() // Wrong!
@Field.Stored("createdDate")
var createdDate: Date = Date() // Wrong!
}
```
This default value will be evaluated only when the `DataStack` sets up the schema, and all instances will end up having the same values. This syntax for "default values" are usually used only for actual reasonable constant values, or sentinel values such as `""` or `0`.
For actual "initial values", `@Field.Stored` and `@Field.Coded` now supports dynamic evaluation during object creation via the `dynamicInitialValue:` argument:
```swift
//
class Person: CoreStoreObject {
@Field.Stored("identifier", dynamicInitialValue: { UUID() })
var identifier: UUID
@Field.Stored("createdDate", dynamicInitialValue: { Date() })
var createdDate: Date
}
```
When using this feature, a "default value" should not be assigned (i.e. no `=` expression).
### `VersionLock`s
While it is convenient to be able to declare entities only in code, it is worrying that we might accidentally change the `CoreStoreObject`'s properties and break our users' model version history. For this, the `CoreStoreSchema` allows us to "lock" our properties to a particular configuration. Any changes to that `VersionLock` will raise an assertion failure during the `CoreStoreSchema` initialization, so you can then look for the commit which changed the `VersionLock` hash.
@@ -1840,8 +2063,8 @@ Once the version lock is set, any changes in the properties or to the model will
# Installation
- Requires:
- iOS 10 SDK and above
- Swift 5.1 (Xcode 11+)
- For previous Swift versions: [Swift 3.2](https://github.com/JohnEstropia/CoreStore/tree/4.2.3), [Swift 4.2](https://github.com/JohnEstropia/CoreStore/tree/6.2.1), [Swift 5.0](https://github.com/JohnEstropia/CoreStore/tree/6.3.2)
- Swift 5.2 (Xcode 11.4+)
- For previous Swift versions: [Swift 3.2](https://github.com/JohnEstropia/CoreStore/tree/4.2.3), [Swift 4.2](https://github.com/JohnEstropia/CoreStore/tree/6.2.1), [Swift 5.0](https://github.com/JohnEstropia/CoreStore/tree/6.3.2), [Swift 5.1](https://github.com/JohnEstropia/CoreStore/tree/7.0.4)
- Dependencies:
- *None*
- Other notes:
@@ -1850,7 +2073,7 @@ Once the version lock is set, any changes in the properties or to the model will
### Install with CocoaPods
In your `Podfile`, add
```
pod 'CoreStore', '~> 7.0'
pod 'CoreStore', '~> 7.2'
```
and run
```
@@ -1861,7 +2084,7 @@ This installs CoreStore as a framework. Declare `import CoreStore` in your swift
### Install with Carthage
In your `Cartfile`, add
```
github "JohnEstropia/CoreStore" >= 7.0.0
github "JohnEstropia/CoreStore" >= 7.2.0
```
and run
```
@@ -1872,7 +2095,7 @@ This installs CoreStore as a framework. Declare `import CoreStore` in your swift
#### Install with Swift Package Manager:
```swift
dependencies: [
.package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "7.0.0"))
.package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "7.2.0"))
]
```
Declare `import CoreStore` in your swift file to use the library.

View File

@@ -33,6 +33,7 @@ import Foundation
internal typealias CustomGetter = @convention(block) (_ rawObject: Any) -> Any?
internal typealias CustomSetter = @convention(block) (_ rawObject: Any, _ newValue: Any?) -> Void
internal typealias CustomInitializer = @convention(block) (_ rawObject: Any) -> Void
internal typealias CustomGetterSetter = (getter: CustomGetter?, setter: CustomSetter?)
@nonobjc @inline(__always)

View File

@@ -209,15 +209,17 @@ public final class CoreStoreSchema: DynamicSchema {
let rawModel = NSManagedObjectModel()
var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:]
var allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:]
var allCustomInitializers: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomInitializer]] = [:]
var allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:]
for entity in self.allEntities {
let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = self.entityDescription(
let (entityDescription, customGetterSetterByKeyPaths, customInitializerByKeyPaths, fieldCoders) = self.entityDescription(
for: entity,
initializer: CoreStoreSchema.firstPassCreateEntityDescription(from:in:)
)
entityDescriptionsByEntity[entity] = (entityDescription.copy() as! NSEntityDescription)
allCustomGettersSetters[entity] = customGetterSetterByKeyPaths
allCustomInitializers[entity] = customInitializerByKeyPaths
allFieldCoders[entity] = fieldCoders
}
CoreStoreSchema.secondPassConnectRelationshipAttributes(for: entityDescriptionsByEntity)
@@ -225,6 +227,7 @@ public final class CoreStoreSchema: DynamicSchema {
CoreStoreSchema.fourthPassSynthesizeManagedObjectClasses(
for: entityDescriptionsByEntity,
allCustomGettersSetters: allCustomGettersSetters,
allCustomInitializers: allCustomInitializers,
allFieldCoders: allFieldCoders
)
@@ -257,6 +260,7 @@ public final class CoreStoreSchema: DynamicSchema {
private var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:]
private var customGettersSettersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:]
private var customInitializersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomInitializer]] = [:]
private var fieldCodersByEntity: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:]
private weak var cachedRawModel: NSManagedObjectModel?
@@ -265,11 +269,13 @@ public final class CoreStoreSchema: DynamicSchema {
initializer: (DynamicEntity, ModelVersion) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
customInitializersByEntity: [KeyPathString: CoreStoreManagedObject.CustomInitializer],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
)
) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
customInitializerByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomInitializer],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
) {
@@ -278,23 +284,31 @@ public final class CoreStoreSchema: DynamicSchema {
return (
cachedEntityDescription,
self.customGettersSettersByEntity[entity] ?? [:],
self.customInitializersByEntity[entity] ?? [:],
self.fieldCodersByEntity[entity] ?? [:]
)
}
let modelVersion = self.modelVersion
let (entityDescription, customGetterSetterByKeyPaths, fieldCoders) = withoutActuallyEscaping(
let (entityDescription, customGetterSetterByKeyPaths, customInitializerByKeyPaths, fieldCoders) = withoutActuallyEscaping(
initializer,
do: { $0(entity, modelVersion) }
)
self.entityDescriptionsByEntity[entity] = entityDescription
self.customGettersSettersByEntity[entity] = customGetterSetterByKeyPaths
self.customInitializersByEntity[entity] = customInitializerByKeyPaths
self.fieldCodersByEntity[entity] = fieldCoders
return (entityDescription, customGetterSetterByKeyPaths, fieldCoders)
return (
entityDescription,
customGetterSetterByKeyPaths,
customInitializerByKeyPaths,
fieldCoders
)
}
private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
customInitializerByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomInitializer],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
) {
@@ -306,6 +320,7 @@ public final class CoreStoreSchema: DynamicSchema {
entityDescription.managedObjectClassName = CoreStoreManagedObject.cs_subclassName(for: entity, in: modelVersion)
var keyPathsByAffectedKeyPaths: [KeyPathString: Set<KeyPathString>] = [:]
var customInitialValuesByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomInitializer] = [:]
var customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter] = [:]
var fieldCoders: [KeyPathString: Internals.AnyFieldCoder] = [:]
func createProperties(for type: CoreStoreObject.Type) -> [NSPropertyDescription] {
@@ -338,6 +353,7 @@ public final class CoreStoreSchema: DynamicSchema {
keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths
customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter)
customInitialValuesByKeyPaths[attribute.keyPath] = attribute.initializer
fieldCoders[attribute.keyPath] = valueTransformer
case let relationship as FieldRelationshipProtocol:
@@ -401,7 +417,12 @@ public final class CoreStoreSchema: DynamicSchema {
}
entityDescription.properties = createProperties(for: entity.type as! CoreStoreObject.Type)
entityDescription.keyPathsByAffectedKeyPaths = keyPathsByAffectedKeyPaths
return (entityDescription, customGetterSetterByKeyPaths, fieldCoders)
return (
entityDescription,
customGetterSetterByKeyPaths,
customInitialValuesByKeyPaths,
fieldCoders
)
}
private static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription]) {
@@ -597,10 +618,15 @@ public final class CoreStoreSchema: DynamicSchema {
private static func fourthPassSynthesizeManagedObjectClasses(
for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription],
allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]],
allCustomInitializers: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomInitializer]],
allFieldCoders: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]]
) {
func createManagedObjectSubclass(for entityDescription: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]?) {
func createManagedObjectSubclass(
for entityDescription: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]?,
customInitializers: [KeyPathString: CoreStoreManagedObject.CustomInitializer]?
) {
let superEntity = entityDescription.superentity
let className = entityDescription.managedObjectClassName!
@@ -612,7 +638,8 @@ public final class CoreStoreSchema: DynamicSchema {
createManagedObjectSubclass(
for: superEntity,
customGetterSetterByKeyPaths: superEntity.coreStoreEntity.flatMap({ allCustomGettersSetters[$0] })
customGetterSetterByKeyPaths: superEntity.coreStoreEntity.flatMap({ allCustomGettersSetters[$0] }),
customInitializers: superEntity.coreStoreEntity.flatMap({ allCustomInitializers[$0] })
)
}
let superClass = Internals.with { () -> CoreStoreManagedObject.Type in
@@ -675,40 +702,80 @@ public final class CoreStoreSchema: DynamicSchema {
}
}
}
let newSelector = NSSelectorFromString("cs_keyPathsForValuesAffectingValueForKey:")
let keyPathsByAffectedKeyPaths = entityDescription.keyPathsByAffectedKeyPaths
let keyPathsForValuesAffectingValue: @convention(block) (Any, String) -> Set<String> = { (instance, keyPath) in
if let keyPaths = keyPathsByAffectedKeyPaths[keyPath] {
swizzle_keyPathsForValuesAffectingValueForKey: do {
let newSelector = NSSelectorFromString("cs_keyPathsForValuesAffectingValueForKey:")
let keyPathsByAffectedKeyPaths = entityDescription.keyPathsByAffectedKeyPaths
let keyPathsForValuesAffectingValue: @convention(block) (Any, String) -> Set<String> = { (instance, keyPath) in
return keyPaths
if let keyPaths = keyPathsByAffectedKeyPaths[keyPath] {
return keyPaths
}
return []
}
return []
}
let origSelector = #selector(CoreStoreManagedObject.keyPathsForValuesAffectingValue(forKey:))
let metaClass: AnyClass = object_getClass(managedObjectClass)!
let origMethod = class_getClassMethod(managedObjectClass, origSelector)!
let origImp = method_getImplementation(origMethod)
let newImp = imp_implementationWithBlock(keyPathsForValuesAffectingValue)
if class_addMethod(metaClass, origSelector, newImp, method_getTypeEncoding(origMethod)) {
let origSelector = #selector(CoreStoreManagedObject.keyPathsForValuesAffectingValue(forKey:))
class_replaceMethod(metaClass, newSelector, origImp, method_getTypeEncoding(origMethod))
}
else {
let metaClass: AnyClass = object_getClass(managedObjectClass)!
let origMethod = class_getClassMethod(managedObjectClass, origSelector)!
let newMethod = class_getClassMethod(managedObjectClass, newSelector)!
method_exchangeImplementations(origMethod, newMethod)
let origImp = method_getImplementation(origMethod)
let newImp = imp_implementationWithBlock(keyPathsForValuesAffectingValue)
if class_addMethod(metaClass, origSelector, newImp, method_getTypeEncoding(origMethod)) {
class_replaceMethod(metaClass, newSelector, origImp, method_getTypeEncoding(origMethod))
}
else {
let newMethod = class_getClassMethod(managedObjectClass, newSelector)!
method_exchangeImplementations(origMethod, newMethod)
}
}
swizzle_awakeFromInsert: do {
let newSelector = NSSelectorFromString("cs_awakeFromInsert")
let awakeFromInsertValue: @convention(block) (Any) -> Void
if let customInitializers = customInitializers,
!customInitializers.isEmpty {
let initializers = Array(customInitializers.values)
awakeFromInsertValue = { (instance) in
initializers.forEach {
$0(instance)
}
}
}
else {
awakeFromInsertValue = { _ in }
}
let origSelector = #selector(CoreStoreManagedObject.awakeFromInsert)
let origMethod = class_getInstanceMethod(managedObjectClass, origSelector)!
let origImp = method_getImplementation(origMethod)
let newImp = imp_implementationWithBlock(awakeFromInsertValue)
if class_addMethod(managedObjectClass, origSelector, newImp, method_getTypeEncoding(origMethod)) {
class_replaceMethod(managedObjectClass, newSelector, origImp, method_getTypeEncoding(origMethod))
}
else {
let newMethod = class_getInstanceMethod(managedObjectClass, newSelector)!
method_exchangeImplementations(origMethod, newMethod)
}
}
}
for (dynamicEntity, entityDescription) in entityDescriptionsByEntity {
createManagedObjectSubclass(
for: entityDescription,
customGetterSetterByKeyPaths: allCustomGettersSetters[dynamicEntity]
customGetterSetterByKeyPaths: allCustomGettersSetters[dynamicEntity],
customInitializers: allCustomInitializers[dynamicEntity]
)
}

View File

@@ -370,6 +370,44 @@ public final class DataStack: Equatable {
}
}
/**
Prepares deinitializing the `DataStack` by removing all persistent stores. This is not necessary, but can help silence SQLite warnings when actively releasing and recreating `DataStack`s.
- parameter completion: the closure to execute after all persistent stores are removed
*/
public func unsafeRemoveAllPersistentStores(completion: @escaping () -> Void = {}) {
let coordinator = self.coordinator
coordinator.performAsynchronously {
withExtendedLifetime(coordinator) { coordinator in
coordinator.persistentStores.forEach {
_ = try? coordinator.remove($0)
}
}
DispatchQueue.main.async(execute: completion)
}
}
/**
Prepares deinitializing the `DataStack` by removing all persistent stores. This is not necessary, but can help silence SQLite warnings when actively releasing and recreating `DataStack`s.
*/
public func unsafeRemoveAllPersistentStoresAndWait() {
let coordinator = self.coordinator
coordinator.performSynchronously {
withExtendedLifetime(coordinator) { coordinator in
coordinator.persistentStores.forEach {
_ = try? coordinator.remove($0)
}
}
}
}
// MARK: 3rd Party Utilities
@@ -509,16 +547,6 @@ public final class DataStack: Equatable {
deinit {
let coordinator = self.coordinator
coordinator.performAsynchronously {
withExtendedLifetime(coordinator) { coordinator in
coordinator.persistentStores.forEach {
_ = try? coordinator.remove($0)
}
}
}
self.unsafeRemoveAllPersistentStores()
}
}

View File

@@ -40,6 +40,15 @@ extension FieldContainer {
@Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self)
var eyeColor: UIColor = .black
@Field.Coded(
"bloodType",
coder: {
encode: { $0.toData() },
decode: { BloodType(fromData: $0) }
}
)
var bloodType: BloodType = .unknown
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored.
@@ -85,6 +94,32 @@ extension FieldContainer {
valueTransformer: { Internals.AnyFieldCoder(fieldCoderType) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init<Coder: FieldCoderType>(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder fieldCoderType: Coder.Type,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) where Coder.FieldStoredValue == V {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(fieldCoderType) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -94,8 +129,14 @@ extension FieldContainer {
```
class Person: CoreStoreObject {
@Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self)
var eyeColor: UIColor = .black
@Field.Coded(
"bloodType",
coder: {
encode: { $0.toData() },
decode: { BloodType(fromData: $0) }
}
)
var bloodType: BloodType = .unknown
}
```
- parameter initial: the initial value for the property when the object is first created.
@@ -127,6 +168,32 @@ extension FieldContainer {
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: (encode: (V) -> Data?, decode: (Data?) -> V),
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -318,12 +385,29 @@ extension FieldContainer {
)
}
}
internal var initializer: CoreStoreManagedObject.CustomInitializer? {
guard let dynamicInitialValue = self.dynamicInitialValue else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.setPrimitiveValue(
dynamicInitialValue(),
forKey: keyPath
)
}
}
// MARK: FilePrivate
fileprivate init(
defaultValue: @escaping () -> Any?,
defaultValue: (() -> Any?)?,
keyPath: KeyPathString,
isOptional: Bool,
versionHashModifier: @escaping () -> String?,
@@ -331,6 +415,7 @@ extension FieldContainer {
valueTransformer: @escaping () -> Internals.AnyFieldCoder?,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)?,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? ,
dynamicInitialValue: (() -> V)?,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
self.keyPath = keyPath
@@ -346,14 +431,17 @@ extension FieldContainer {
renamingIdentifier: renamingIdentifier(),
valueTransformer: fieldCoder,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: Internals.AnyFieldCoder.TransformableDefaultValueCodingBox(
defaultValue: defaultValue(),
fieldCoder: fieldCoder
)
defaultValue: defaultValue.map {
Internals.AnyFieldCoder.TransformableDefaultValueCodingBox(
defaultValue: $0(),
fieldCoder: fieldCoder
) as Any
}
)
}
self.customGetter = customGetter
self.customSetter = customSetter
self.dynamicInitialValue = dynamicInitialValue
}
@@ -361,6 +449,7 @@ extension FieldContainer {
private let customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)?
private let customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)?
private let dynamicInitialValue: (() -> V)?
}
}
@@ -407,6 +496,32 @@ extension FieldContainer.Coded where V: FieldOptionalType {
valueTransformer: { Internals.AnyFieldCoder(coder) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init<Coder: FieldCoderType>(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: Coder.Type,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) where Coder.FieldStoredValue == V.Wrapped {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(coder) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -416,8 +531,14 @@ extension FieldContainer.Coded where V: FieldOptionalType {
```
class Person: CoreStoreObject {
@Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self)
var eyeColor: UIColor? = nil
@Field.Coded(
"bloodType",
coder: {
encode: { $0.toData() },
decode: { BloodType(fromData: $0) }
}
)
var bloodType: BloodType?
}
```
- parameter initial: the initial value for the property when the object is first created.
@@ -449,6 +570,32 @@ extension FieldContainer.Coded where V: FieldOptionalType {
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
coder: (encode: (V) -> Data?, decode: (Data?) -> V),
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(tag: UUID(), encode: coder.encode, decode: coder.decode) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -495,6 +642,31 @@ extension FieldContainer.Coded where V: DefaultNSSecureCodable {
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V>.self) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V>.self) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -541,6 +713,31 @@ extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSS
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V.Wrapped>.self) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
defaultValue: nil,
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
valueTransformer: { Internals.AnyFieldCoder(FieldCoders.DefaultNSSecureCoding<V.Wrapped>.self) },
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}

View File

@@ -36,7 +36,7 @@ extension FieldContainer {
/**
The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported.
```
class Animal: CoreStoreObject {
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@@ -85,6 +85,30 @@ extension FieldContainer {
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
wrappedValue: nil,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}
@@ -262,18 +286,36 @@ extension FieldContainer {
)
}
}
internal var initializer: CoreStoreManagedObject.CustomInitializer? {
guard let dynamicInitialValue = self.dynamicInitialValue else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.setPrimitiveValue(
dynamicInitialValue().cs_toFieldStoredNativeType(),
forKey: keyPath
)
}
}
// MARK: FilePrivate
fileprivate init(
wrappedValue initial: @escaping () -> V,
wrappedValue initial: (() -> V)?,
keyPath: KeyPathString,
isOptional: Bool,
versionHashModifier: @escaping () -> String?,
renamingIdentifier: @escaping () -> String?,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)?,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? ,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)?,
dynamicInitialValue: (() -> V)?,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
self.keyPath = keyPath
@@ -287,11 +329,12 @@ extension FieldContainer {
renamingIdentifier: renamingIdentifier(),
valueTransformer: nil,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: initial().cs_toFieldStoredNativeType()
defaultValue: initial?().cs_toFieldStoredNativeType()
)
}
self.customGetter = customGetter
self.customSetter = customSetter
self.dynamicInitialValue = dynamicInitialValue
}
@@ -299,6 +342,7 @@ extension FieldContainer {
private let customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)?
private let customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)?
private let dynamicInitialValue: (() -> V)?
}
}
@@ -331,7 +375,8 @@ extension FieldContainer.Stored where V: FieldOptionalType {
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
wrappedValue: initial,
@@ -341,6 +386,30 @@ extension FieldContainer.Stored where V: FieldOptionalType {
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: nil,
affectedByKeyPaths: affectedByKeyPaths
)
}
public init(
_ keyPath: KeyPathString,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)? = nil,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = [],
dynamicInitialValue: @escaping () -> V
) {
self.init(
wrappedValue: nil,
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
customGetter: customGetter,
customSetter: customSetter,
dynamicInitialValue: dynamicInitialValue,
affectedByKeyPaths: affectedByKeyPaths
)
}

View File

@@ -275,6 +275,8 @@ extension FieldContainer {
)
}
}
let initializer: CoreStoreManagedObject.CustomInitializer? = nil
// MARK: FilePrivate

View File

@@ -48,4 +48,5 @@ internal protocol FieldAttributeProtocol: FieldProtocol {
var entityDescriptionValues: () -> EntityDescriptionValues { get }
var getter: CoreStoreManagedObject.CustomGetter? { get }
var setter: CoreStoreManagedObject.CustomSetter? { get }
var initializer: CoreStoreManagedObject.CustomInitializer? { get }
}

View File

@@ -345,6 +345,30 @@ extension From where O: CoreStoreObject {
return self.select(R.self, [SelectTerm<O>.attribute(keyPath)])
}
/**
Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections
- parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections
- returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path
*/
@available(macOS 10.12, *)
public func sectionBy<T>(_ sectionKeyPath: KeyPath<O, FieldContainer<O>.Stored<T>>) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, { $0 })
}
/**
Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections
- parameter sectionKeyPath: the `KeyPath` to use to group the objects into sections
- returns: a `SectionMonitorChainBuilder` that is sectioned by the specified key path
*/
@available(macOS 10.12, *)
public func sectionBy<T>(_ sectionKeyPath: KeyPath<O, FieldContainer<O>.Virtual<T>>) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(O.meta[keyPath: sectionKeyPath].keyPath, { $0 })
}
/**
Creates a `SectionMonitorChainBuilder` with the key path to use to group `ListMonitor` objects into sections

View File

@@ -115,27 +115,6 @@ extension NSEntityDescription {
}
}
@nonobjc
internal var customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter] {
get {
if let userInfo = self.userInfo,
let value = userInfo[UserInfoKey.CoreStoreManagedObjectCustomGetterSetterByKeyPaths] {
return value as! [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]
}
return [:]
}
set {
cs_setUserInfo { (userInfo) in
userInfo[UserInfoKey.CoreStoreManagedObjectCustomGetterSetterByKeyPaths] = newValue
}
}
}
// MARK: Private
@@ -151,7 +130,6 @@ extension NSEntityDescription {
fileprivate static let CoreStoreManagedObjectUniqueConstraints = "CoreStoreManagedObjectUniqueConstraints"
fileprivate static let CoreStoreManagedObjectKeyPathsByAffectedKeyPaths = "CoreStoreManagedObjectKeyPathsByAffectedKeyPaths"
fileprivate static let CoreStoreManagedObjectCustomGetterSetterByKeyPaths = "CoreStoreManagedObjectCustomGetterSetterByKeyPaths"
}

View File

@@ -95,6 +95,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
Internals.Closure(callback),
forKey: observer
)
_ = self.lazySnapshot
}
/**