Compare commits

...

62 Commits
7.0.1 ... 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
John Estropia
73b7fcd907 Merge branch 'prototype/propertyWrapperFields' into develop 2020-03-26 02:04:33 +09:00
John Estropia
74c1a97af4 Version bump 2020-03-26 02:04:10 +09:00
John Estropia
97f2a53124 AppleDocs for Field source files 2020-03-26 01:57:32 +09:00
John Estropia
b6db872be0 Merge branch 'prototype/propertyWrapperFields' of github.com:JohnEstropia/CoreStore into prototype/propertyWrapperFields 2020-03-25 19:00:56 +09:00
John Estropia
0d9299f900 WIP: docs 2020-03-25 14:21:49 +09:00
John Estropia
7f928dc684 docs 2020-03-19 14:12:09 +09:00
John Estropia
231e138ab0 performant access of relationship objectIDs for snapshots 2020-02-21 13:51:17 +09:00
John Estropia
361dba58c6 bypass thread checks depending on location of Field call 2020-02-21 11:52:11 +09:00
John Estropia
e1b03b4a89 fix wrong optional configuration in Field.Virtual 2020-02-21 11:20:32 +09:00
John Estropia
0df6c737c1 fix wrong optional configuration in Field.Virtual 2020-02-21 11:17:39 +09:00
John Estropia
12c5aeaaa4 safer casting 2020-02-21 10:36:28 +09:00
John Estropia
dd3fb17dd0 fix runtime issue with Fields on objects with base classes 2020-02-21 10:19:42 +09:00
John Estropia
627a5d4355 add OrderBy utilities for Field.Stored 2020-02-19 22:01:46 +09:00
John Estropia
58629bc1df add missing predicate operator overloads 2020-02-19 13:58:14 +09:00
John Estropia
f925803b93 Create FUNDING.yml 2020-02-19 13:32:57 +09:00
John Estropia
843adf21f7 improved API for custom getters and setters in Field properties 2020-02-18 18:17:52 +09:00
John Estropia
2d1b1e0592 add Field.Coded dynamic lookups for ObjectPublisher and ObjectSnapshot 2020-02-17 18:30:18 +09:00
John Estropia
38e9878c04 Merge branch 'master' into prototype/propertyWrapperFields 2020-02-08 09:47:35 +09:00
John Estropia
8cb8b95c2e fix build for watchOS 2020-02-08 08:49:11 +09:00
John Estropia
8e4e308ccc Merge branch 'prototype/propertyWrapperFields' of github.com:JohnEstropia/CoreStore into prototype/propertyWrapperFields 2020-02-06 09:33:15 +09:00
John Estropia
f0f4049798 renamed Field.Computed to Field.Virtual to distinguish from Field.Derived 2020-02-06 09:33:08 +09:00
John Estropia
c20fe4ac17 add Field.Relationship dynamicMemberLookups 2020-02-05 11:03:57 +09:00
John Estropia
e9c3312612 Fix default encoders for top-level values 2020-01-21 17:03:12 +09:00
John Estropia
92ad895044 Field.Relationship propertyWrapper 2020-01-20 17:13:01 +09:00
John Estropia
bcc2d9def3 Field.Coded implementations for transformable attributes 2020-01-18 16:22:06 +09:00
John Estropia
43f61359da prototype new Fields as propertyWrappers (Swift 5.2 above only) 2020-01-15 18:29:58 +09:00
John Estropia
5e37ee4566 Reorganize properties source files 2020-01-10 17:04:51 +09:00
John Estropia
c544e0cce8 lazily evaluate NSEntityDescription-required fields from CoreStoreObject attributes 2020-01-09 17:00:43 +09:00
John Estropia
f119a3adec add SPM installation instructions to README 2020-01-08 11:21:41 +09:00
John Estropia
c951cb87a3 version bump 2020-01-08 11:03:43 +09:00
John Estropia
08147806a0 explicit exclusion of objc files in package.swift 2020-01-08 11:01:22 +09:00
John Estropia
4beb11519e Deprecation of ObjectiveC shivs 2020-01-08 10:26:27 +09:00
John Estropia
b7ebda4487 Use generic collection types in ListSnapshot mutators 2020-01-05 02:37:42 +09:00
John Estropia
b4489301ac fix SPM spec 2019-12-23 11:53:46 +09:00
John Estropia
c025e5acc6 version bump 2019-12-23 10:11:26 +09:00
John Estropia
57745f36a8 Allow purging of datasource 2019-12-17 21:10:01 +09:00
John Estropia
eef1c99f11 Allow custom views to consume ListSnapshot diffable data 2019-12-17 19:45:53 +09:00
John Estropia
9a19919392 add utility to create ObjectPublisher directly from a DynamicObject using its own context 2019-12-02 12:21:06 +09:00
John Estropia
3e2d62fe67 missed public modifier 2019-12-02 11:53:23 +09:00
John Estropia
6f275eb63a add "updatedItemIdentifiers" utility to ListSnapshot 2019-12-02 10:52:36 +09:00
John Estropia
b12dba4d15 optimizations 2019-11-29 20:09:43 +09:00
John Estropia
4ee1b04523 Support for fetchOffset in ListPublisher, optimize slicing logic 2019-11-18 19:52:57 +09:00
John Estropia
b1decc9853 Force fetchLimit for ListPublisher and ListSnapshot 2019-11-14 20:34:48 +09:00
John Estropia
c2e4c033ef Merge pull request #348 from ntnmrndn/fixWarning
Fix warning
2019-11-05 19:11:51 +09:00
John Estropia
e12223df85 fix casting error 2019-10-29 20:37:08 +09:00
John Estropia
468922d5ed fix casting issues 2019-10-29 20:30:03 +09:00
John Estropia
6b9a4b480b minor 2019-10-29 20:18:14 +09:00
Antoine Marandon
81b482e28b Fix warning 2019-10-29 17:27:25 +09:00
John Estropia
c112a84c0a Add debugDescription implementation for new Publisher and Snapshot types 2019-10-28 19:31:02 +09:00
John Estropia
88ab0b5e15 provide direct conversion from DynamicObject to ObjectSnapshot 2019-10-28 12:03:17 +09:00
John Estropia
717cb75720 Add utility to fetch ObjectPublishers by ObjectID 2019-10-28 11:23:12 +09:00
John Estropia
998938490c Make ObjectPublishers even lighter by lazy-loading observers 2019-10-25 19:16:38 +09:00
120 changed files with 10455 additions and 4560 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [JohnEstropia]

View File

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

File diff suppressed because it is too large Load Diff

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

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="12141" systemVersion="16F73" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="18G1012" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="Place" representedClassName="CoreStoreDemo.Place" syncable="YES">
<attribute name="latitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="NO" syncable="YES"/>
@@ -20,7 +20,7 @@
<memberEntity name="Place"/>
</configuration>
<elements>
<element name="Place" positionX="261" positionY="225" width="128" height="105"/>
<element name="Place" positionX="261" positionY="225" width="128" height="103"/>
<element name="TimeZone" positionX="297" positionY="270" width="128" height="120"/>
</elements>
</model>

View File

@@ -51,7 +51,7 @@ final class CollectionViewDemoViewController: UICollectionViewController {
]
self.filterBarButton = filterBarButton
self.dataSource = DiffableDataSource.CollectionView<Palette>(
self.dataSource = DiffableDataSource.CollectionViewAdapter<Palette>(
collectionView: self.collectionView,
dataStack: ColorsDemo.stack,
cellProvider: { (collectionView, indexPath, palette) in
@@ -118,7 +118,7 @@ final class CollectionViewDemoViewController: UICollectionViewController {
// MARK: Private
private var filterBarButton: UIBarButtonItem?
private var dataSource: DiffableDataSource.CollectionView<Palette>?
private var dataSource: DiffableDataSource.CollectionViewAdapter<Palette>?
deinit {
@@ -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

@@ -16,7 +16,7 @@ final class ListObserverDemoViewController: UITableViewController {
// MARK: - EditableDataSource
final class EditableDataSource: DiffableDataSource.TableView<Palette> {
final class EditableDataSource: DiffableDataSource.TableViewAdapter<Palette> {
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
@@ -130,7 +130,7 @@ final class ListObserverDemoViewController: UITableViewController {
// MARK: Private
private var filterBarButton: UIBarButtonItem?
private var dataSource: DiffableDataSource.TableView<Palette>?
private var dataSource: DiffableDataSource.TableViewAdapter<Palette>?
deinit {
@@ -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

@@ -30,6 +30,10 @@
@import CoreData;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// MARK: - BridgingTests
@implementation BridgingTests
@@ -261,3 +265,5 @@
}
@end
#pragma clang diagnostic pop

View File

@@ -37,78 +37,149 @@ import CoreStore
#endif
class Animal: CoreStoreObject {
let species = Value.Required<String>("species", initial: "Swift")
let master = Relationship.ToOne<Person>("master")
let color = Transformable.Optional<Color>("color")
@Field.Stored("species")
var species: String = "Swift"
@Field.Coded("color", coder: FieldCoders.NSCoding.self)
var color: Color? = .blue
@Field.Relationship("master")
var master: Person?
}
class Dog: Animal {
let nickname = Value.Optional<String>("nickname")
let age = Value.Required<Int>("age", initial: 1)
let friends = Relationship.ToManyOrdered<Dog>("friends")
let friendedBy = Relationship.ToManyUnordered<Dog>("friendedBy", inverse: { $0.friends })
static let commonNicknames = ["Spot", "Benjie", "Max", "Milo"]
@Field.Stored(
"nickname",
dynamicInitialValue: {
commonNicknames[.random(in: commonNicknames.indices)]
}
)
var nickname: String
@Field.Stored("age")
var age: Int = 1
@Field.Relationship("friends")
var friends: [Dog]
@Field.Relationship("friendedBy", inverse: \.$friends)
var friendedBy: Set<Dog>
}
struct CustomType {
var string = "customString"
}
enum Job: String, CaseIterable {
case unemployed
case engineer
case doctor
case lawyer
init?(data: Data) {
guard
let rawValue = String(data: data, encoding: .utf8),
let value = Self.init(rawValue: rawValue)
else {
return nil
}
self = value
}
func toData() -> Data {
return Data(self.rawValue.utf8)
}
}
class Person: CoreStoreObject {
let title = Value.Required<String>(
@Field.Stored(
"title",
initial: "Mr.",
customSetter: Person.setTitle
customSetter: { (object, field, newValue) in
field.primitiveValue = newValue
object.$displayName.primitiveValue = nil
}
)
let name = Value.Required<String>(
var title: String = "Mr."
@Field.Stored(
"name",
initial: "",
customSetter: Person.setName
customSetter: { (object, field, newValue) in
field.primitiveValue = newValue
object.$displayName.primitiveValue = nil
}
)
let displayName = Value.Optional<String>(
var name: String = ""
@Field.Virtual(
"displayName",
isTransient: true,
customGetter: Person.getDisplayName(_:),
customGetter: Person.getDisplayName(_:_:),
affectedByKeyPaths: Person.keyPathsAffectingDisplayName()
)
var displayName: String?
let spouse = Relationship.ToOne<Person>("spouse")
let pets = Relationship.ToManyUnordered<Animal>("pets", inverse: { $0.master })
@Field.Virtual(
"customType",
customGetter: { (object, field) in
private let _spouse = Relationship.ToOne<Person>("_spouseInverse", inverse: { $0.spouse })
if let value = field.primitiveValue {
private static func setTitle(_ partialObject: PartialObject<Person>, _ newValue: String) {
partialObject.setPrimitiveValue(newValue, for: { $0.title })
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
}
private static func setName(_ partialObject: PartialObject<Person>, _ newValue: String) {
partialObject.setPrimitiveValue(newValue, for: { $0.name })
partialObject.setPrimitiveValue(nil, for: { $0.displayName })
}
static func getDisplayName(_ partialObject: PartialObject<Person>) -> String? {
if let displayName = partialObject.primitiveValue(for: { $0.displayName }) {
return displayName
return value
}
let value = CustomType()
field.primitiveValue = value
return value
}
let title = partialObject.value(for: { $0.title })
let name = partialObject.value(for: { $0.name })
let displayName = "\(title) \(name)"
partialObject.setPrimitiveValue(displayName, for: { $0.displayName })
return displayName
)
var customField: CustomType
@Field.Coded(
"job",
coder: (
encode: { $0.toData() },
decode: { $0.flatMap(Job.init(data:)) ?? .unemployed }
),
dynamicInitialValue: {
Job.allCases[.random(in: Job.allCases.indices)]
}
)
var job: Job
@Field.Relationship("spouse")
var spouse: Person?
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Animal>
@Field.Relationship("_spouseInverse", inverse: \.$spouse)
private var spouseInverse: Person?
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
if let value = field.primitiveValue {
return value
}
let title = object.$title.value
let name = object.$name.value
let value = "\(title) \(name)"
field.primitiveValue = value
return value
}
static func keyPathsAffectingDisplayName() -> Set<String> {
private static func keyPathsAffectingDisplayName() -> Set<String> {
return [
String(keyPath: \Person.title),
String(keyPath: \Person.name)
String(keyPath: \Person.$title),
String(keyPath: \Person.$name)
]
}
}
@@ -131,20 +202,20 @@ class DynamicModelTests: BaseTestDataTestCase {
],
versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
"Person": [0x2831cf046084d96d, 0xbe19b13ace54641, 0x635a082728b0f6f0, 0x3d4ef2dd4b74a87c]
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
"Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992]
]
)
)
self.prepareStack(dataStack, configurations: [nil]) { (stack) in
let k1 = String(keyPath: \Animal.species)
let k1 = String(keyPath: \Animal.$species)
XCTAssertEqual(k1, "species")
let k2 = String(keyPath: \Dog.species)
let k2 = String(keyPath: \Dog.$species)
XCTAssertEqual(k2, "species")
let k3 = String(keyPath: \Dog.nickname)
let k3 = String(keyPath: \Dog.$nickname)
XCTAssertEqual(k3, "nickname")
let updateDone = self.expectation(description: "update-done")
@@ -156,27 +227,28 @@ class DynamicModelTests: BaseTestDataTestCase {
asynchronous: { (transaction) in
let animal = transaction.create(Into<Animal>())
XCTAssertEqual(animal.species.value, "Swift")
XCTAssertTrue(type(of: animal.species.value) == String.self)
XCTAssertEqual(animal.species, "Swift")
XCTAssertTrue(type(of: animal.species) == String.self)
XCTAssertEqual(animal.color, Color.blue)
animal.species .= "Sparrow"
XCTAssertEqual(animal.species.value, "Sparrow")
animal.species = "Sparrow"
XCTAssertEqual(animal.species, "Sparrow")
animal.color .= .yellow
XCTAssertEqual(animal.color.value, Color.yellow)
animal.color = .yellow
XCTAssertEqual(animal.color, Color.yellow)
for property in Animal.metaProperties(includeSuperclasses: true) {
switch property.keyPath {
case String(keyPath: \Animal.species):
XCTAssertTrue(property is ValueContainer<Animal>.Required<String>)
case String(keyPath: \Animal.$species):
XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)
case String(keyPath: \Animal.master):
XCTAssertTrue(property is RelationshipContainer<Animal>.ToOne<Person>)
case String(keyPath: \Animal.$master):
XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)
case String(keyPath: \Animal.color):
XCTAssertTrue(property is TransformableContainer<Animal>.Optional<Color>)
case String(keyPath: \Animal.$color):
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
default:
XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
@@ -184,34 +256,34 @@ class DynamicModelTests: BaseTestDataTestCase {
}
let dog = transaction.create(Into<Dog>())
XCTAssertEqual(dog.species.value, "Swift")
XCTAssertEqual(dog.nickname.value, nil)
XCTAssertEqual(dog.age.value, 1)
XCTAssertEqual(dog.species, "Swift")
XCTAssertEqual(dog.age, 1)
XCTAssertTrue(Dog.commonNicknames.contains(dog.nickname))
for property in Dog.metaProperties(includeSuperclasses: true) {
switch property.keyPath {
case String(keyPath: \Dog.species):
XCTAssertTrue(property is ValueContainer<Animal>.Required<String>)
case String(keyPath: \Dog.$species):
XCTAssertTrue(property is FieldContainer<Animal>.Stored<String>)
case String(keyPath: \Dog.master):
XCTAssertTrue(property is RelationshipContainer<Animal>.ToOne<Person>)
case String(keyPath: \Dog.$master):
XCTAssertTrue(property is FieldContainer<Animal>.Relationship<Person?>)
case String(keyPath: \Dog.color):
XCTAssertTrue(property is TransformableContainer<Animal>.Optional<Color>)
case String(keyPath: \Dog.$color):
XCTAssertTrue(property is FieldContainer<Animal>.Coded<Color?>)
case String(keyPath: \Dog.nickname):
XCTAssertTrue(property is ValueContainer<Dog>.Optional<String>)
case String(keyPath: \Dog.$nickname):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<String>)
case String(keyPath: \Dog.age):
XCTAssertTrue(property is ValueContainer<Dog>.Required<Int>)
case String(keyPath: \Dog.$age):
XCTAssertTrue(property is FieldContainer<Dog>.Stored<Int>)
case String(keyPath: \Dog.friends):
XCTAssertTrue(property is RelationshipContainer<Dog>.ToManyOrdered<Dog>)
case String(keyPath: \Dog.$friends):
XCTAssertTrue(property is FieldContainer<Dog>.Relationship<[Dog]>)
case String(keyPath: \Dog.friendedBy):
XCTAssertTrue(property is RelationshipContainer<Dog>.ToManyUnordered<Dog>)
case String(keyPath: \Dog.$friendedBy):
XCTAssertTrue(property is FieldContainer<Dog>.Relationship<Set<Dog>>)
default:
XCTFail("Unknown KeyPath: \"\(property.keyPath)\"")
@@ -231,17 +303,17 @@ class DynamicModelTests: BaseTestDataTestCase {
//
// #endif
let didSetObserver = dog.species.observe(options: [.new, .old]) { (object, change) in
let didSetObserver = dog.observe(\.$species, options: [.new, .old]) { (object, change) in
XCTAssertEqual(object, dog)
XCTAssertEqual(change.kind, .setting)
XCTAssertEqual(change.newValue, "Dog")
XCTAssertEqual(change.oldValue, "Swift")
XCTAssertFalse(change.isPrior)
XCTAssertEqual(object.species.value, "Dog")
XCTAssertEqual(object.species, "Dog")
didSetObserverDone.fulfill()
}
let willSetObserver = dog.species.observe(options: [.new, .old, .prior]) { (object, change) in
let willSetObserver = dog.observe(\.$species, options: [.new, .old, .prior]) { (object, change) in
XCTAssertEqual(object, dog)
XCTAssertEqual(change.kind, .setting)
@@ -250,28 +322,31 @@ class DynamicModelTests: BaseTestDataTestCase {
if change.isPrior {
XCTAssertNil(change.newValue)
XCTAssertEqual(object.species.value, "Swift")
XCTAssertEqual(object.species, "Swift")
willSetPriorObserverDone.fulfill()
}
else {
XCTAssertEqual(change.newValue, "Dog")
XCTAssertEqual(object.species.value, "Dog")
XCTAssertEqual(object.species, "Dog")
willSetNotPriorObserverDone.fulfill()
}
}
dog.species .= "Dog"
XCTAssertEqual(dog.species.value, "Dog")
dog.species = "Dog"
XCTAssertEqual(dog.species, "Dog")
didSetObserver.invalidate()
willSetObserver.invalidate()
dog.nickname .= "Spot"
XCTAssertEqual(dog.nickname.value, "Spot")
dog.nickname = "Spot"
XCTAssertEqual(dog.nickname, "Spot")
let person = transaction.create(Into<Person>())
XCTAssertTrue(person.pets.value.isEmpty)
XCTAssertTrue(person.pets.isEmpty)
XCTAssertEqual(person.customField.string, "customString")
let initialJob = person.job
XCTAssertTrue(Job.allCases.contains(initialJob))
XCTAssertEqual(
person.rawObject!
@@ -280,7 +355,7 @@ class DynamicModelTests: BaseTestDataTestCase {
["title", "name"]
)
person.name .= "Joe"
person.name = "Joe"
XCTAssertEqual(person.rawObject!.value(forKey: "name") as! String?, "Joe")
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "Mr. Joe")
@@ -288,46 +363,66 @@ class DynamicModelTests: BaseTestDataTestCase {
person.rawObject!.setValue("AAAA", forKey: "displayName")
XCTAssertEqual(person.rawObject!.value(forKey: "displayName") as! String?, "AAAA")
person.name .= "John"
XCTAssertEqual(person.name.value, "John")
XCTAssertEqual(person.displayName.value, "Mr. John") // Custom getter
person.name = "John"
XCTAssertEqual(person.name, "John")
XCTAssertEqual(person.displayName, "Mr. John") // Custom getter
let personSnapshot1 = person.asSnapshot(in: transaction)!
XCTAssertEqual(person.name.value, personSnapshot1.name)
XCTAssertEqual(person.title.value, personSnapshot1.title)
XCTAssertEqual(person.displayName.value, personSnapshot1.displayName)
XCTAssertEqual(person.name, personSnapshot1.$name)
XCTAssertEqual(person.title, personSnapshot1.$title)
XCTAssertEqual(person.displayName, personSnapshot1.$displayName)
XCTAssertEqual(person.job, personSnapshot1.$job)
person.title .= "Sir"
XCTAssertEqual(person.displayName.value, "Sir John")
person.title = "Sir"
XCTAssertEqual(person.displayName, "Sir John")
XCTAssertEqual(personSnapshot1.name, "John")
XCTAssertEqual(personSnapshot1.title, "Mr.")
XCTAssertEqual(personSnapshot1.displayName, "Mr. John")
XCTAssertEqual(personSnapshot1.$name, "John")
XCTAssertEqual(personSnapshot1.$title, "Mr.")
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
person.customField.string = "newCustomString"
XCTAssertEqual(person.customField.string, "newCustomString")
person.job = .engineer
XCTAssertEqual(person.job, .engineer)
let personSnapshot2 = person.asSnapshot(in: transaction)!
XCTAssertEqual(person.name.value, personSnapshot2.name)
XCTAssertEqual(person.title.value, personSnapshot2.title)
XCTAssertEqual(person.displayName.value, personSnapshot2.displayName)
XCTAssertEqual(person.name, personSnapshot2.$name)
XCTAssertEqual(person.title, personSnapshot2.$title)
XCTAssertEqual(person.displayName, personSnapshot2.$displayName)
XCTAssertEqual(person.job, personSnapshot2.$job)
var personSnapshot3 = personSnapshot2
personSnapshot3.name = "James"
XCTAssertEqual(personSnapshot1.name, "John")
XCTAssertEqual(personSnapshot1.displayName, "Mr. John")
XCTAssertEqual(personSnapshot2.name, "John")
XCTAssertEqual(personSnapshot2.displayName, "Sir John")
XCTAssertEqual(personSnapshot3.name, "James")
XCTAssertEqual(personSnapshot3.displayName, "Sir John")
personSnapshot3.$name = "James"
XCTAssertEqual(personSnapshot1.$name, "John")
XCTAssertEqual(personSnapshot1.$displayName, "Mr. John")
XCTAssertEqual(personSnapshot1.$job, initialJob)
XCTAssertEqual(personSnapshot2.$name, "John")
XCTAssertEqual(personSnapshot2.$displayName, "Sir John")
XCTAssertEqual(personSnapshot2.$job, .engineer)
XCTAssertEqual(personSnapshot3.$name, "James")
XCTAssertEqual(personSnapshot3.$displayName, "Sir John")
XCTAssertEqual(personSnapshot3.$job, .engineer)
person.pets.value.insert(dog)
person.pets.insert(dog)
XCTAssertEqual(person.pets.count, 1)
XCTAssertEqual(person.pets.value.first, dog)
XCTAssertEqual(person.pets.value.first?.master.value, person)
XCTAssertEqual(dog.master.value, person)
XCTAssertEqual(dog.master.value?.pets.value.first, dog)
XCTAssertEqual(person.pets.first, dog)
XCTAssertEqual(person.pets.first?.master, person)
XCTAssertEqual(dog.master, person)
XCTAssertEqual(dog.master?.pets.first, dog)
},
success: { _ in
let person = try! stack.fetchOne(From<Person>())
XCTAssertNotNil(person)
let personPublisher = person!.asPublisher(in: stack)
XCTAssertEqual(personPublisher.$name, "John")
XCTAssertEqual(personPublisher.$displayName, "Sir John")
XCTAssertEqual(personPublisher.$job, .engineer)
updateDone.fulfill()
},
failure: { _ in
@@ -338,59 +433,67 @@ class DynamicModelTests: BaseTestDataTestCase {
stack.perform(
asynchronous: { (transaction) in
let p1 = Where<Animal>({ $0.species == "Sparrow" })
let p1 = Where<Animal>({ $0.$species == "Sparrow" })
XCTAssertEqual(p1.predicate, NSPredicate(format: "%K == %@", "species", "Sparrow"))
let bird = try transaction.fetchOne(From<Animal>(), p1)
XCTAssertNotNil(bird)
XCTAssertEqual(bird!.species.value, "Sparrow")
XCTAssertEqual(bird!.species, "Sparrow")
XCTAssertEqual(bird!.color, Color.yellow)
let p2 = Where<Dog>({ $0.nickname == "Spot" })
let p2 = Where<Dog>({ $0.$nickname == "Spot" })
XCTAssertEqual(p2.predicate, NSPredicate(format: "%K == %@", "nickname", "Spot"))
let dog = try transaction.fetchOne(From<Dog>().where(\.nickname == "Spot"))
let dog = try transaction.fetchOne(From<Dog>().where(\.$nickname == "Spot"))
XCTAssertNotNil(dog)
XCTAssertEqual(dog!.nickname.value, "Spot")
XCTAssertEqual(dog!.species.value, "Dog")
XCTAssertEqual(dog!.nickname, "Spot")
XCTAssertEqual(dog!.species, "Dog")
let person = try transaction.fetchOne(From<Person>())
XCTAssertNotNil(person)
XCTAssertEqual(person!.pets.value.first, dog)
XCTAssertEqual(person!.name, "John")
XCTAssertEqual(person!.title, "Sir")
XCTAssertEqual(person!.displayName, "Sir John")
XCTAssertEqual(person!.customField.string, "customString")
XCTAssertEqual(person!.job, .engineer)
XCTAssertEqual(person!.pets.first, dog)
let p3 = Where<Dog>({ $0.age == 10 })
let p3 = Where<Dog>({ $0.$age == 10 })
XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))
let totalAge = try transaction.queryValue(From<Dog>().select(Int.self, .sum(\Dog.age)))
let totalAge = try transaction.queryValue(
From<Dog>().select(Int.self, .sum(\.$age))
)
XCTAssertEqual(totalAge, 1)
_ = try transaction.fetchAll(
From<Dog>()
.where(\Animal.species == "Dog" && \Dog.age == 10)
.where(\Animal.$species == "Dog" && \Dog.$age == 10)
)
_ = try transaction.fetchAll(
From<Dog>()
.where(\Dog.age == 10 && \Animal.species == "Dog")
.orderBy(.ascending({ $0.species }))
.where(\Dog.$age == 10 && \Animal.$species == "Dog")
.orderBy(.ascending({ $0.$species }))
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.species == "Dog" && $0.age == 10 })
Where<Dog>({ $0.$species == "Dog" && $0.$age == 10 })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age == 10 && $0.species == "Dog" })
Where<Dog>({ $0.$age == 10 && $0.$species == "Dog" })
)
_ = try transaction.fetchAll(
From<Dog>(),
Where<Dog>({ $0.age > 10 && $0.age <= 15 })
Where<Dog>({ $0.$age > 10 && $0.$age <= 15 })
)
_ = try transaction.fetchAll(
From<Dog>(),
(\Dog.age > 10 && \Dog.age <= 15)
(\Dog.$age > 10 && \Dog.$age <= 15)
)
},
success: { _ in
@@ -402,15 +505,20 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTFail()
}
)
self.waitAndCheckExpectations()
}
self.waitForExpectations(timeout: 10, handler: { _ in })
self.addTeardownBlock {
dataStack.unsafeRemoveAllPersistentStoresAndWait()
}
}
@objc
dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
XCTAssertEqual(String(keyPath: \Animal.species), "species")
XCTAssertEqual(String(keyPath: \Dog.species), "species")
XCTAssertEqual(String(keyPath: \Animal.$species), "species")
XCTAssertEqual(String(keyPath: \Dog.$species), "species")
}
@nonobjc

View File

@@ -31,6 +31,7 @@ import CoreStore
// MARK: - ErrorTests
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
final class ErrorTests: XCTestCase {
@objc

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

@@ -53,7 +53,7 @@ class ListObserverTests: BaseTestDataTestCase {
var events = 0
let willChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -67,7 +67,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 0
}
)
let didInsertSectionExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertSection:toSectionIndex:"),
object: observer,
handler: { (note) -> Bool in
@@ -87,7 +87,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 1
}
)
let didInsertObjectExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"),
object: observer,
handler: { (note) -> Bool in
@@ -119,7 +119,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 2
}
)
let didChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -184,7 +184,7 @@ class ListObserverTests: BaseTestDataTestCase {
var events = 0
let willChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -199,7 +199,7 @@ class ListObserverTests: BaseTestDataTestCase {
}
)
let didUpdateObjectExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didUpdateObject:atIndexPath:"),
object: observer,
handler: { (note) -> Bool in
@@ -250,7 +250,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 1 || events == 2
}
)
let didChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -329,7 +329,7 @@ class ListObserverTests: BaseTestDataTestCase {
var events = 0
let willChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -343,7 +343,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 0
}
)
let didMoveObjectExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didMoveObject:fromIndexPath:toIndexPath:"),
object: observer,
handler: { (note) -> Bool in
@@ -376,7 +376,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 1
}
)
let didChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -437,7 +437,7 @@ class ListObserverTests: BaseTestDataTestCase {
var events = 0
let willChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -451,7 +451,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 0
}
)
let didUpdateObjectExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteObject:fromIndexPath:"),
object: observer,
handler: { (note) -> Bool in
@@ -480,7 +480,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 1 || events == 2
}
)
let didDeleteSectionExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didDeleteSection:fromSectionIndex:"),
object: observer,
handler: { (note) -> Bool in
@@ -508,7 +508,7 @@ class ListObserverTests: BaseTestDataTestCase {
return events == 3
}
)
let didChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
object: observer,
handler: { (note) -> Bool in

View File

@@ -57,7 +57,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
var events = 0
let willUpdateExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"),
object: observer,
handler: { (note) -> Bool in
@@ -74,7 +74,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
return events == 0
}
)
let didUpdateExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didUpdateObject:changedPersistentKeys:"),
object: observer,
handler: { (note) -> Bool in
@@ -154,7 +154,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
var events = 0
let didDeleteExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"),
object: observer,
handler: { (note) -> Bool in

View File

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

View File

@@ -400,7 +400,7 @@ final class TransactionTests: BaseTestCase {
XCTAssertFalse(monitor.hasObjects())
var events = 0
let willChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -414,7 +414,7 @@ final class TransactionTests: BaseTestCase {
return events == 0
}
)
let didInsertObjectExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"),
object: observer,
handler: { (note) -> Bool in
@@ -444,7 +444,7 @@ final class TransactionTests: BaseTestCase {
return events == 1
}
)
let didChangeExpectation = self.expectation(
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorDidChange:"),
object: observer,
handler: { (note) -> Bool in
@@ -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

View File

@@ -67,7 +67,7 @@ final class WhereTests: XCTestCase {
dynamic func test_ThatDynamicModelKeyPaths_CanBeCreated() {
XCTAssertAllEqual(String(keyPath: \TestEntity1.testEntityID), "testEntityID")
XCTAssertAllEqual(String(keyPath: \Animal.color), "color")
XCTAssertAllEqual(String(keyPath: \Animal.$color), "color")
}
@objc
@@ -104,18 +104,18 @@ final class WhereTests: XCTestCase {
XCTAssertAllEqual(
"master.pets",
(\Animal.master ~ \.pets).description,
String(keyPath: \Animal.master ~ \.pets)
(\Animal.$master ~ \.$pets).description,
String(keyPath: \Animal.$master ~ \.$pets)
)
XCTAssertAllEqual(
"master.pets.species",
(\Animal.master ~ \.pets ~ \.species).description,
String(keyPath: \Animal.master ~ \.pets ~ \.species)
(\Animal.$master ~ \.$pets ~ \.$species).description,
String(keyPath: \Animal.$master ~ \.$pets ~ \.$species)
)
XCTAssertAllEqual(
"master.pets.master",
(\Animal.master ~ \.pets ~ \.master).description,
String(keyPath: \Animal.master ~ \.pets ~ \.master)
(\Animal.$master ~ \.$pets ~ \.$master).description,
String(keyPath: \Animal.$master ~ \.$pets ~ \.$master)
)
}
}
@@ -138,8 +138,8 @@ final class WhereTests: XCTestCase {
XCTAssertAllEqual(
"master.pets.@count",
(\Animal.master ~ \.pets).count().description,
String(keyPath: (\Animal.master ~ \.pets).count())
(\Animal.$master ~ \.$pets).count().description,
String(keyPath: (\Animal.$master ~ \.$pets).count())
)
}
}
@@ -162,13 +162,13 @@ final class WhereTests: XCTestCase {
XCTAssertAllEqual(
"ANY master.pets",
(\Animal.master ~ \.pets).any().description,
String(keyPath: (\Animal.master ~ \.pets).any())
(\Animal.$master ~ \.$pets).any().description,
String(keyPath: (\Animal.$master ~ \.$pets).any())
)
XCTAssertAllEqual(
"ANY master.pets.species",
(\Animal.master ~ \.pets ~ \.species).any().description,
String(keyPath: (\Animal.master ~ \.pets ~ \.species).any())
(\Animal.$master ~ \.$pets ~ \.$species).any().description,
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).any())
)
}
}
@@ -191,13 +191,13 @@ final class WhereTests: XCTestCase {
XCTAssertAllEqual(
"ALL master.pets",
(\Animal.master ~ \.pets).all().description,
String(keyPath: (\Animal.master ~ \.pets).all())
(\Animal.$master ~ \.$pets).all().description,
String(keyPath: (\Animal.$master ~ \.$pets).all())
)
XCTAssertAllEqual(
"ALL master.pets.species",
(\Animal.master ~ \.pets ~ \.species).all().description,
String(keyPath: (\Animal.master ~ \.pets ~ \.species).all())
(\Animal.$master ~ \.$pets ~ \.$species).all().description,
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).all())
)
}
}
@@ -220,13 +220,13 @@ final class WhereTests: XCTestCase {
XCTAssertAllEqual(
"NONE master.pets",
(\Animal.master ~ \.pets).none().description,
String(keyPath: (\Animal.master ~ \.pets).none())
(\Animal.$master ~ \.$pets).none().description,
String(keyPath: (\Animal.$master ~ \.$pets).none())
)
XCTAssertAllEqual(
"NONE master.pets.species",
(\Animal.master ~ \.pets ~ \.species).none().description,
String(keyPath: (\Animal.master ~ \.pets ~ \.species).none())
(\Animal.$master ~ \.$pets ~ \.$species).none().description,
String(keyPath: (\Animal.$master ~ \.$pets ~ \.$species).none())
)
}
}
@@ -247,7 +247,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.name) == dummy
let whereClause: Where<Animal> = (\.$master ~ \.$name) == dummy
let predicate = NSPredicate(format: "master.name == %@", dummy)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)
@@ -265,7 +265,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.spouse ~ \.name) == dummy
let whereClause: Where<Animal> = (\.$master ~ \.$spouse ~ \.$name) == dummy
let predicate = NSPredicate(format: "master.spouse.name == %@", dummy)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)
@@ -283,7 +283,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.pets).count() == count
let whereClause: Where<Animal> = (\.$master ~ \.$pets).count() == count
let predicate = NSPredicate(format: "master.pets.@count == %d", count)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)
@@ -301,7 +301,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).any() == dummy
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).any() == dummy
let predicate = NSPredicate(format: "ANY master.pets.species == %@", dummy)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)
@@ -319,7 +319,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).all() == dummy
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).all() == dummy
let predicate = NSPredicate(format: "ALL master.pets.species == %@", dummy)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)
@@ -337,7 +337,7 @@ final class WhereTests: XCTestCase {
}
do {
let whereClause: Where<Animal> = (\.master ~ \.pets ~ \.species).none() == dummy
let whereClause: Where<Animal> = (\.$master ~ \.$pets ~ \.$species).none() == dummy
let predicate = NSPredicate(format: "NONE master.pets.species == %@", dummy)
XCTAssertAllEqual(whereClause, Where<Animal>(predicate))
XCTAssertAllEqual(whereClause.predicate, predicate)

View File

@@ -45,8 +45,9 @@ let package = Package(
.testTarget(
name: "CoreStoreTests",
dependencies: ["CoreStore"],
path: "CoreStoreTests"
path: "CoreStoreTests",
exclude: ["BridgingTests.h", "BridgingTests.m"]
)
],
swiftLanguageVersions: [.v5_1]
swiftLanguageVersions: [.v5]
)

316
README.md
View File

@@ -19,8 +19,8 @@ Unleashing the real power of Core Data with the elegance and safety of Swift
<br />
</p>
* **Swift 5.1:** iOS 10+ / macOS 10.12+ / watchOS 3.0+ / tvOS 10.0+
* Previously supported 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:** iOS 10+ / macOS 10.12+ / watchOS 3.0+ / tvOS 10.0+
* Previously supported 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)
Upgrading from CoreStore 6.x (swift 5.0) to 7.x (Swift 5.1)? Check out the [🆕 features](#features) and make sure to read the [Change logs](https://github.com/JohnEstropia/CoreStore/releases).
@@ -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)
@@ -1506,9 +1512,9 @@ Note that the owner instance will not be retained. You may call `ListPublisher.r
The `ListSnapshot` returned from the `ListPublisher.snapshot` property returns a full-copy `struct` of all sections and `NSManagedObject` items in the list. This is ideal for managing states as they are thread-safe and are not affected by further changes to the result set. `ListPublisher` automatically updates its `snapshot` value to the latest state of the fetch.
Unlike `ListMonitor`s (See [`ListMonitor` examples](#observe-detailed-list-changes) below), a `ListPublisher` does not track detailed inserts, deletes, and moves. In return, a `ListPublisher` is a lot more lightweight and are designed to work well with `DiffableDataSource.TableView`s and `DiffableDataSource.CollectionView`s:
Unlike `ListMonitor`s (See [`ListMonitor` examples](#observe-detailed-list-changes) below), a `ListPublisher` does not track detailed inserts, deletes, and moves. In return, a `ListPublisher` is a lot more lightweight and are designed to work well with `DiffableDataSource.TableViewAdapter`s and `DiffableDataSource.CollectionViewAdapter`s:
```swift
self.dataSource = DiffableDataSource.CollectionView<Person>(
self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
@@ -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.
@@ -1883,6 +2106,13 @@ git submodule add https://github.com/JohnEstropia/CoreStore.git <destination dir
```
Drag and drop **CoreStore.xcodeproj** to your project.
### Install through Xcode's Swift Package Manager
From the **File** - **Swift Packages** - **Add Package Dependency…** menu, search for
```
CoreStore
```
where `JohnEstropia` is the *Owner* (forks may appear as well). Then add to your project.
### Objective-C support

View File

@@ -29,19 +29,22 @@ import CoreData
// MARK: - AttributeProtocol
internal protocol AttributeProtocol: PropertyProtocol {
static var attributeType: NSAttributeType { get }
var isOptional: Bool { get }
var isTransient: Bool { get }
var allowsExternalBinaryDataStorage: Bool { get }
var versionHashModifier: () -> String? { get }
var renamingIdentifier: () -> String? { get }
var defaultValue: () -> Any? { get }
var affectedByKeyPaths: () -> Set<String> { get }
internal protocol AttributeProtocol: AnyObject, PropertyProtocol {
typealias EntityDescriptionValues = (
attributeType: NSAttributeType,
isOptional: Bool,
isTransient: Bool,
allowsExternalBinaryDataStorage: Bool,
versionHashModifier: String?,
renamingIdentifier: String?,
affectedByKeyPaths: Set<String>,
defaultValue: Any?
)
var entityDescriptionValues: () -> EntityDescriptionValues { get }
var rawObject: CoreStoreManagedObject? { get set }
var getter: CoreStoreManagedObject.CustomGetter? { get }
var setter: CoreStoreManagedObject.CustomSetter? { get }
var valueForSnapshot: Any { get }
var valueForSnapshot: Any? { get }
}

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `AsynchronousDataTransaction`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSAsynchronousDataTransaction: CSBaseDataTransaction, CoreStoreObjectiveCType {
@@ -161,6 +162,7 @@ public final class CSAsynchronousDataTransaction: CSBaseDataTransaction, CoreSto
// MARK: - AsynchronousDataTransaction
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension AsynchronousDataTransaction: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -29,6 +29,7 @@ import CoreData
// MARK: - CSDataStack
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension CSDataStack {
/**

View File

@@ -29,6 +29,7 @@ import CoreData
// MARK: - CSDataStack
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@available(macOS 10.12, *)
extension CSDataStack {

View File

@@ -29,6 +29,7 @@ import CoreData
// MARK: - CSDataStack
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension CSDataStack {
/**

View File

@@ -28,6 +28,7 @@ import Foundation
// MARK: - CSDataStack
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension CSDataStack {
/**

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `DataStack`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
@@ -209,6 +210,7 @@ public final class CSDataStack: NSObject, CoreStoreObjectiveCType {
// MARK: - DataStack
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension DataStack: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -35,7 +35,7 @@ import CoreData
- SeeAlso: `CoreStoreError`
*/
@objc
public final class CSError: NSError, CoreStoreObjectiveCType {
public final class CSError: NSError {
/**
The `NSError` error domain for `CSError`.
@@ -44,6 +44,17 @@ public final class CSError: NSError, CoreStoreObjectiveCType {
*/
@objc
public static let errorDomain = CoreStoreErrorDomain
public var bridgeToSwift: CoreStoreError {
if let swift = self.swiftError {
return swift
}
let swift = CoreStoreError(_bridgedNSError: self) ?? .unknown
self.swiftError = swift
return swift
}
// MARK: NSObject
@@ -67,20 +78,6 @@ public final class CSError: NSError, CoreStoreObjectiveCType {
return "(\(String(reflecting: Self.self))) \(self.bridgeToSwift.coreStoreDumpString)"
}
// MARK: CoreStoreObjectiveCType
public var bridgeToSwift: CoreStoreError {
if let swift = self.swiftError {
return swift
}
let swift = CoreStoreError(_bridgedNSError: self) ?? .unknown
self.swiftError = swift
return swift
}
/**
Do not call directly!
*/
@@ -101,6 +98,9 @@ public final class CSError: NSError, CoreStoreObjectiveCType {
private var swiftError: CoreStoreError?
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension CSError: CoreStoreObjectiveCType {}
// MARK: - CSErrorCode
@@ -110,6 +110,7 @@ public final class CSError: NSError, CoreStoreObjectiveCType {
- SeeAlso: `CSError`
- SeeAlso: `CoreStoreError`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public enum CSErrorCode: Int {
@@ -152,15 +153,7 @@ public enum CSErrorCode: Int {
// MARK: - CoreStoreError
extension CoreStoreError: CoreStoreSwiftType, _ObjectiveCBridgeableError {
// MARK: CoreStoreSwiftType
public var bridgeToObjectiveC: CSError {
return CSError(self)
}
extension CoreStoreError: _ObjectiveCBridgeableError {
// MARK: _ObjectiveCBridgeableError
@@ -260,9 +253,11 @@ extension CoreStoreError: CoreStoreSwiftType, _ObjectiveCBridgeableError {
}
// MARK: Internal
// MARK: - Error
extension Error {
// MARK: Internal
internal var bridgeToSwift: CoreStoreError {
@@ -281,7 +276,8 @@ extension Error {
return .unknown
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal var bridgeToObjectiveC: NSError {
switch self {
@@ -297,3 +293,17 @@ extension Error {
}
}
}
// MARK: - CoreStoreError
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension CoreStoreError: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType
public var bridgeToObjectiveC: CSError {
return CSError(self)
}
}

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `InMemoryStore`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSInMemoryStore: NSObject, CSStorageInterface, CoreStoreObjectiveCType {
@@ -121,6 +122,7 @@ public final class CSInMemoryStore: NSObject, CSStorageInterface, CoreStoreObjec
// MARK: - InMemoryStore
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension InMemoryStore: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `MigrationResult`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSMigrationResult: NSObject, CoreStoreObjectiveCType {
@@ -173,6 +174,7 @@ public final class CSMigrationResult: NSObject, CoreStoreObjectiveCType {
// MARK: - MigrationResult
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension MigrationResult {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `MigrationType`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSMigrationType: NSObject, CoreStoreObjectiveCType {
@@ -119,6 +120,7 @@ public final class CSMigrationType: NSObject, CoreStoreObjectiveCType {
// MARK: - MigrationType
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension MigrationType: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `SQLiteStore`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSSQLiteStore: NSObject, CSLocalStorage, CoreStoreObjectiveCType {
@@ -196,6 +197,7 @@ public final class CSSQLiteStore: NSObject, CSLocalStorage, CoreStoreObjectiveCT
// MARK: - SQLiteStore
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension SQLiteStore: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `SetupResult`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSSetupResult: NSObject {
@@ -177,6 +178,7 @@ public final class CSSetupResult: NSObject {
// MARK: - SetupResult
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension SetupResult where Success: StorageInterface, Success: CoreStoreSwiftType, Success.ObjectiveCType: CSStorageInterface, Failure == CoreStoreError {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `SynchronousDataTransaction`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSSynchronousDataTransaction: CSBaseDataTransaction, CoreStoreObjectiveCType {
@@ -148,6 +149,7 @@ public final class CSSynchronousDataTransaction: CSBaseDataTransaction, CoreStor
// MARK: - SynchronousDataTransaction
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension SynchronousDataTransaction: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `Tweak`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSTweak: NSObject, CSFetchClause, CSQueryClause, CSDeleteClause, CoreStoreObjectiveCType {
@@ -90,6 +91,7 @@ public final class CSTweak: NSObject, CSFetchClause, CSQueryClause, CSDeleteClau
// MARK: - Tweak
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension Tweak: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import Foundation
- SeeAlso: `UnsafeDataModelSchema`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSUnsafeDataModelSchema: NSObject, CSDynamicSchema, CoreStoreObjectiveCType {
@@ -104,6 +105,7 @@ public final class CSUnsafeDataModelSchema: NSObject, CSDynamicSchema, CoreStore
// MARK: - UnsafeDataModelSchema
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension UnsafeDataModelSchema: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -34,6 +34,7 @@ import CoreData
- SeeAlso: `UnsafeDataTransaction`
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
@objc
public final class CSUnsafeDataTransaction: CSBaseDataTransaction, CoreStoreObjectiveCType {
/**
@@ -208,6 +209,7 @@ public final class CSUnsafeDataTransaction: CSBaseDataTransaction, CoreStoreObje
// MARK: - UnsafeDataTransaction
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension UnsafeDataTransaction: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -35,6 +35,7 @@ import Foundation
- SeeAlso: `XcodeDataModelSchema`
*/
@objc
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
public final class CSXcodeDataModelSchema: NSObject, CSDynamicSchema, CoreStoreObjectiveCType {
/**
@@ -104,6 +105,7 @@ public final class CSXcodeDataModelSchema: NSObject, CSDynamicSchema, CoreStoreO
// MARK: - XcodeDataModelSchema
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
extension XcodeDataModelSchema: CoreStoreSwiftType {
// MARK: CoreStoreSwiftType

View File

@@ -94,9 +94,10 @@ extension CoreStoreError: CustomDebugStringConvertible, CoreStoreDebugStringConv
firstLine = ".progressiveMigrationRequired"
info.append(("localStoreURL", localStoreURL))
case .asynchronousMigrationRequired(let localStoreURL):
case .asynchronousMigrationRequired(let localStoreURL, let NSError):
firstLine = ".asynchronousMigrationRequired"
info.append(("localStoreURL", localStoreURL))
info.append(("NSError", NSError))
case .internalError(let NSError):
firstLine = ".internalError"
@@ -373,15 +374,34 @@ fileprivate struct CoreStoreFetchedSectionInfoWrapper: CoreStoreDebugStringConve
var coreStoreDumpString: String {
return createFormattedString(
"\"\(self.sectionInfo.name)\" (", ")",
("numberOfObjects", self.sectionInfo.numberOfObjects),
("indexTitle", self.sectionInfo.indexTitle as Any)
"\"\(self.sectionName)\" (", ")",
("numberOfObjects", self.numberOfObjects),
("indexTitle", self.sectionIndexTitle as Any)
)
}
// MARK: FilePrivate
let sectionInfo: NSFetchedResultsSectionInfo
fileprivate init(_ sectionInfo: NSFetchedResultsSectionInfo) {
self.sectionName = sectionInfo.name
self.numberOfObjects = sectionInfo.numberOfObjects
self.sectionIndexTitle = sectionInfo.indexTitle
}
fileprivate init(_ section: Internals.DiffableDataSourceSnapshot.Section) {
self.sectionName = section.differenceIdentifier
self.numberOfObjects = section.elements.count
self.sectionIndexTitle = nil
}
// MARK: Private
private let sectionName: String
private let sectionIndexTitle: String?
private let numberOfObjects: Int
}
@available(macOS 10.12, *)
@@ -409,6 +429,56 @@ extension ListMonitor: CustomDebugStringConvertible, CoreStoreDebugStringConvert
}
// MARK: - ListPublisher
@available(macOS 10.12, *)
extension ListPublisher: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("snapshot", self.snapshot)
)
}
}
// MARK: - ListSnapshot
extension ListSnapshot: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("numberOfObjects", self.numberOfItems),
("sections", self.diffableSnapshot.sections.map(CoreStoreFetchedSectionInfoWrapper.init))
)
}
}
// MARK: - LocalStorageOptions
extension LocalStorageOptions: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
@@ -568,6 +638,56 @@ extension ObjectMonitor: CustomDebugStringConvertible, CoreStoreDebugStringConve
}
// MARK: - ObjectPublisher
extension ObjectPublisher: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("objectID", self.objectID()),
("object", self.object as Any)
)
}
}
// MARK: - ObjectSnapshot
extension ObjectSnapshot: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return formattedDebugDescription(self)
}
// MARK: CoreStoreDebugStringConvertible
public var coreStoreDumpString: String {
return createFormattedString(
"(", ")",
("objectID", self.objectID()),
("dictionaryForValues", self.dictionaryForValues())
)
}
}
// MARK: - OrderBy
extension OrderBy: CustomDebugStringConvertible, CoreStoreDebugStringConvertible {

View File

@@ -37,7 +37,7 @@
#error CoreStore Objective-C utilities can only be used on platforms that support C function overloading
#endif
#define CORESTORE_EXTERN extern
#define CORESTORE_EXTERN extern __deprecated_msg("CoreStore Objective-C API will be removed soon.")
#define CORESTORE_OVERLOADABLE __attribute__((__overloadable__))
#define CORESTORE_REQUIRES_NIL_TERMINATION __attribute__((sentinel(0, 1)))
#define CORESTORE_RETURNS_RETAINED __attribute__((ns_returns_retained))

View File

@@ -31,6 +31,7 @@ import Foundation
/**
`CoreStoreObjectiveCType`s are Objective-C accessible classes that represent CoreStore's Swift types.
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
public protocol CoreStoreObjectiveCType: AnyObject {
/**
@@ -55,6 +56,7 @@ public protocol CoreStoreObjectiveCType: AnyObject {
/**
`CoreStoreSwiftType`s are CoreStore's Swift types that are bridgeable to Objective-C.
*/
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
public protocol CoreStoreSwiftType {
/**
@@ -71,21 +73,25 @@ public protocol CoreStoreSwiftType {
// MARK: - Internal
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ closure: () -> T) -> T.ObjectiveCType {
return closure().bridgeToObjectiveC
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ closure: () -> [T]) -> [T.ObjectiveCType] {
return closure().map { $0.bridgeToObjectiveC }
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ closure: () -> T?) -> T.ObjectiveCType? {
return closure()?.bridgeToObjectiveC
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ closure: () throws -> T) throws -> T.ObjectiveCType {
do {
@@ -98,6 +104,7 @@ internal func bridge<T: CoreStoreSwiftType>(_ closure: () throws -> T) throws ->
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge(_ closure: () throws -> Void) throws {
do {
@@ -110,6 +117,7 @@ internal func bridge(_ closure: () throws -> Void) throws {
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ error: NSErrorPointer, _ closure: () throws -> T) -> T.ObjectiveCType? {
do {
@@ -125,6 +133,7 @@ internal func bridge<T: CoreStoreSwiftType>(_ error: NSErrorPointer, _ closure:
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge(_ error: NSErrorPointer, _ closure: () throws -> Void) -> Bool {
do {
@@ -140,6 +149,7 @@ internal func bridge(_ error: NSErrorPointer, _ closure: () throws -> Void) -> B
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T>(_ error: NSErrorPointer, _ closure: () throws -> T?) -> T? {
do {
@@ -155,6 +165,7 @@ internal func bridge<T>(_ error: NSErrorPointer, _ closure: () throws -> T?) ->
}
}
@available(*, deprecated, message: "CoreStore Objective-C API will be removed soon.")
internal func bridge<T: CoreStoreSwiftType>(_ error: NSErrorPointer, _ closure: () throws -> [T]) -> [T.ObjectiveCType]? {
do {

View File

@@ -198,9 +198,6 @@ public enum CoreStoreError: Error, CustomNSError, Hashable {
case (.userError(let error1), .userError(let error2)):
switch (error1, error2) {
case (let error1 as AnyHashable, let error2 as AnyHashable):
return error1 == error2
case (let error1 as NSError, let error2 as NSError):
return error1.isEqual(error2)

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

@@ -1,9 +1,26 @@
//
// CoreStoreObject+DataSources.swift
// CoreStore iOS
// CoreStore
//
// Created by John Estropia on 2019/10/04.
// Copyright © 2019 John Rommel Estropia. All rights reserved.
// 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.
//
#if canImport(UIKit) || canImport(AppKit)

View File

@@ -27,6 +27,33 @@ import Foundation
import CoreData
extension DynamicObject where Self: CoreStoreObject {
public func observe<O, V>(_ keyPath: KeyPath<Self, FieldContainer<O>.Stored<V>>, options: NSKeyValueObservingOptions = [], changeHandler: @escaping (Self, CoreStoreObjectValueDiff<V>) -> Void) -> CoreStoreObjectKeyValueObservation {
let result = _CoreStoreObjectKeyValueObservation(
object: self.rawObject!,
keyPath: self[keyPath: keyPath].keyPath,
callback: { (object, kind, newValue, oldValue, _, isPrior) in
let notification = CoreStoreObjectValueDiff<V>(
kind: kind,
newNativeValue: newValue as? V.QueryableNativeType,
oldNativeValue: oldValue as? V.QueryableNativeType,
isPrior: isPrior
)
changeHandler(
Self.cs_fromRaw(object: object),
notification
)
}
)
result.start(options)
return result
}
}
// MARK: CoreStoreObjectKeyValueObservation
/**

View File

@@ -26,6 +26,88 @@
import CoreData
import Foundation
// MARK: - FieldContainer.Value
extension FieldContainer.Stored {
/**
Creates a `Where` clause by comparing if a property is equal to a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.nickname == "John" }))
```
*/
public static func == (_ attribute: Self, _ value: V) -> Where<O> {
return Where(attribute.keyPath, isEqualTo: value)
}
/**
Creates a `Where` clause by comparing if a property is not equal to a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.nickname != "John" }))
```
*/
public static func != (_ attribute: Self, _ value: V) -> Where<O> {
return !Where(attribute.keyPath, isEqualTo: value)
}
/**
Creates a `Where` clause by comparing if a property is less than a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.age < 20 }))
```
*/
public static func < (_ attribute: Self, _ value: V) -> Where<O> {
return Where("%K < %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any)
}
/**
Creates a `Where` clause by comparing if a property is greater than a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.age > 20 }))
```
*/
public static func > (_ attribute: Self, _ value: V) -> Where<O> {
return Where("%K > %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any)
}
/**
Creates a `Where` clause by comparing if a property is less than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.age <= 20 }))
```
*/
public static func <= (_ attribute: Self, _ value: V) -> Where<O> {
return Where("%K <= %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any)
}
/**
Creates a `Where` clause by comparing if a property is greater than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where({ $0.age >= 20 }))
```
*/
public static func >= (_ attribute: Self, _ value: V) -> Where<O> {
return Where("%K >= %@", attribute.keyPath, value.cs_toFieldStoredNativeType() as Any)
}
/**
Creates a `Where` clause by checking if a sequence contains the value of a property
```
let dog = dataStack.fetchOne(From<Dog>().where({ ["Pluto", "Snoopy", "Scooby"] ~= $0.nickname }))
```
*/
public static func ~= <S: Sequence>(_ sequence: S, _ attribute: Self) -> Where<O> where S.Iterator.Element == V {
return Where(attribute.keyPath, isMemberOf: sequence)
}
}
// MARK: - ValueContainer.Required
extension ValueContainer.Required {

View File

@@ -69,6 +69,11 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable {
self.isMeta = false
self.rawObject = (rawObject as! CoreStoreManagedObject)
guard Self.meta.needsReflection else {
return
}
self.registerReceiver(
mirror: Mirror(reflecting: self),
object: self
@@ -117,6 +122,11 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable {
internal let rawObject: CoreStoreManagedObject?
internal let isMeta: Bool
internal lazy var needsReflection: Bool = self.containsLegacyAttributes(
mirror: Mirror(reflecting: self),
object: self
)
internal class func metaProperties(includeSuperclasses: Bool) -> [PropertyProtocol] {
func keyPaths(_ allKeyPaths: inout [PropertyProtocol], for dynamicType: CoreStoreObject.Type) {
@@ -137,9 +147,33 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable {
keyPaths(&allKeyPaths, for: self)
return allKeyPaths
}
// MARK: Private
private func containsLegacyAttributes(mirror: Mirror, object: CoreStoreObject) -> Bool {
if let superclassMirror = mirror.superclassMirror,
self.containsLegacyAttributes(mirror: superclassMirror, object: object) {
return true
}
for child in mirror.children {
switch child.value {
case is AttributeProtocol:
return true
case is RelationshipProtocol:
return true
default:
continue
}
}
return false
}
private func registerReceiver(mirror: Mirror, object: CoreStoreObject) {

View File

@@ -209,20 +209,26 @@ 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) = 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)
CoreStoreSchema.thirdPassConnectInheritanceTreeAndIndexes(for: entityDescriptionsByEntity)
CoreStoreSchema.fourthPassSynthesizeManagedObjectClasses(
for: entityDescriptionsByEntity,
allCustomGettersSetters: allCustomGettersSetters
allCustomGettersSetters: allCustomGettersSetters,
allCustomInitializers: allCustomInitializers,
allFieldCoders: allFieldCoders
)
rawModel.entities = entityDescriptionsByEntity.values.sorted(by: { $0.name! < $1.name! })
@@ -254,22 +260,57 @@ 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?
private func entityDescription(for entity: DynamicEntity, initializer: (DynamicEntity, ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter])) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) {
private func entityDescription(
for entity: DynamicEntity,
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]
) {
if let cachedEntityDescription = self.entityDescriptionsByEntity[entity] {
return (cachedEntityDescription, self.customGettersSettersByEntity[entity] ?? [:])
return (
cachedEntityDescription,
self.customGettersSettersByEntity[entity] ?? [:],
self.customInitializersByEntity[entity] ?? [:],
self.fieldCodersByEntity[entity] ?? [:]
)
}
let modelVersion = self.modelVersion
let (entityDescription, customGetterSetterByKeyPaths) = withoutActuallyEscaping(initializer, do: { $0(entity, modelVersion) })
let (entityDescription, customGetterSetterByKeyPaths, customInitializerByKeyPaths, fieldCoders) = withoutActuallyEscaping(
initializer,
do: { $0(entity, modelVersion) }
)
self.entityDescriptionsByEntity[entity] = entityDescription
self.customGettersSettersByEntity[entity] = customGetterSetterByKeyPaths
return (entityDescription, customGetterSetterByKeyPaths)
self.customInitializersByEntity[entity] = customInitializerByKeyPaths
self.fieldCodersByEntity[entity] = fieldCoders
return (
entityDescription,
customGetterSetterByKeyPaths,
customInitializerByKeyPaths,
fieldCoders
)
}
private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (entity: NSEntityDescription, customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]) {
private static func firstPassCreateEntityDescription(from entity: DynamicEntity, in modelVersion: ModelVersion) -> (
entity: NSEntityDescription,
customGetterSetterByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter],
customInitializerByKeyPaths: [KeyPathString: CoreStoreManagedObject.CustomInitializer],
fieldCoders: [KeyPathString: Internals.AnyFieldCoder]
) {
let entityDescription = NSEntityDescription()
entityDescription.coreStoreEntity = entity
@@ -279,30 +320,76 @@ 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] {
var propertyDescriptions: [NSPropertyDescription] = []
for property in type.metaProperties(includeSuperclasses: false) {
switch property {
case let attribute as FieldAttributeProtocol:
Internals.assert(
!NSManagedObject.instancesRespond(to: Selector(attribute.keyPath)),
"Attribute Property name \"\(String(reflecting: entity.type)).\(attribute.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(attribute.keyPath)\""
)
let entityDescriptionValues = attribute.entityDescriptionValues()
let description = NSAttributeDescription()
description.name = attribute.keyPath
description.attributeType = entityDescriptionValues.attributeType
description.isOptional = entityDescriptionValues.isOptional
description.defaultValue = entityDescriptionValues.defaultValue
description.isTransient = entityDescriptionValues.isTransient
description.allowsExternalBinaryDataStorage = entityDescriptionValues.allowsExternalBinaryDataStorage
description.versionHashModifier = entityDescriptionValues.versionHashModifier
description.renamingIdentifier = entityDescriptionValues.renamingIdentifier
let valueTransformer = entityDescriptionValues.valueTransformer
description.valueTransformerName = valueTransformer?.transformerName.rawValue
propertyDescriptions.append(description)
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:
Internals.assert(
!NSManagedObject.instancesRespond(to: Selector(relationship.keyPath)),
"Relationship Property name \"\(String(reflecting: entity.type)).\(relationship.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(relationship.keyPath)\""
)
let entityDescriptionValues = relationship.entityDescriptionValues()
let description = NSRelationshipDescription()
description.name = relationship.keyPath
description.minCount = entityDescriptionValues.minCount
description.maxCount = entityDescriptionValues.maxCount
description.isOrdered = entityDescriptionValues.isOrdered
description.deleteRule = entityDescriptionValues.deleteRule
description.versionHashModifier = entityDescriptionValues.versionHashModifier
description.renamingIdentifier = entityDescriptionValues.renamingIdentifier
propertyDescriptions.append(description)
keyPathsByAffectedKeyPaths[relationship.keyPath] = entityDescriptionValues.affectedByKeyPaths
case let attribute as AttributeProtocol:
Internals.assert(
!NSManagedObject.instancesRespond(to: Selector(attribute.keyPath)),
"Attribute Property name \"\(String(reflecting: entity.type)).\(attribute.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(attribute.keyPath)\""
)
let entityDescriptionValues = attribute.entityDescriptionValues()
let description = NSAttributeDescription()
description.name = attribute.keyPath
description.attributeType = Swift.type(of: attribute).attributeType
description.isOptional = attribute.isOptional
description.defaultValue = attribute.defaultValue()
description.isTransient = attribute.isTransient
description.allowsExternalBinaryDataStorage = attribute.allowsExternalBinaryDataStorage
description.versionHashModifier = attribute.versionHashModifier()
description.renamingIdentifier = attribute.renamingIdentifier()
description.attributeType = entityDescriptionValues.attributeType
description.isOptional = entityDescriptionValues.isOptional
description.defaultValue = entityDescriptionValues.defaultValue
description.isTransient = entityDescriptionValues.isTransient
description.allowsExternalBinaryDataStorage = entityDescriptionValues.allowsExternalBinaryDataStorage
description.versionHashModifier = entityDescriptionValues.versionHashModifier
description.renamingIdentifier = entityDescriptionValues.renamingIdentifier
propertyDescriptions.append(description)
keyPathsByAffectedKeyPaths[attribute.keyPath] = attribute.affectedByKeyPaths()
keyPathsByAffectedKeyPaths[attribute.keyPath] = entityDescriptionValues.affectedByKeyPaths
customGetterSetterByKeyPaths[attribute.keyPath] = (attribute.getter, attribute.setter)
case let relationship as RelationshipProtocol:
@@ -310,16 +397,17 @@ public final class CoreStoreSchema: DynamicSchema {
!NSManagedObject.instancesRespond(to: Selector(relationship.keyPath)),
"Relationship Property name \"\(String(reflecting: entity.type)).\(relationship.keyPath)\" is not allowed because it collides with \"\(String(reflecting: NSManagedObject.self)).\(relationship.keyPath)\""
)
let entityDescriptionValues = relationship.entityDescriptionValues()
let description = NSRelationshipDescription()
description.name = relationship.keyPath
description.minCount = relationship.minCount
description.maxCount = relationship.maxCount
description.isOrdered = relationship.isOrdered
description.deleteRule = relationship.deleteRule
description.versionHashModifier = relationship.versionHashModifier()
description.renamingIdentifier = relationship.renamingIdentifier()
description.minCount = entityDescriptionValues.minCount
description.maxCount = entityDescriptionValues.maxCount
description.isOrdered = entityDescriptionValues.isOrdered
description.deleteRule = entityDescriptionValues.deleteRule
description.versionHashModifier = entityDescriptionValues.versionHashModifier
description.renamingIdentifier = entityDescriptionValues.renamingIdentifier
propertyDescriptions.append(description)
keyPathsByAffectedKeyPaths[relationship.keyPath] = relationship.affectedByKeyPaths()
keyPathsByAffectedKeyPaths[relationship.keyPath] = entityDescriptionValues.affectedByKeyPaths
default:
continue
@@ -329,7 +417,12 @@ public final class CoreStoreSchema: DynamicSchema {
}
entityDescription.properties = createProperties(for: entity.type as! CoreStoreObject.Type)
entityDescription.keyPathsByAffectedKeyPaths = keyPathsByAffectedKeyPaths
return (entityDescription, customGetterSetterByKeyPaths)
return (
entityDescription,
customGetterSetterByKeyPaths,
customInitialValuesByKeyPaths,
fieldCoders
)
}
private static func secondPassConnectRelationshipAttributes(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription]) {
@@ -382,14 +475,34 @@ public final class CoreStoreSchema: DynamicSchema {
for property in entityType.metaProperties(includeSuperclasses: false) {
switch property {
case let relationship as FieldRelationshipProtocol:
let (destinationType, destinationKeyPath) = relationship.entityDescriptionValues().inverse
let destinationEntity = findEntity(for: destinationType)
let description = relationshipsByName[relationship.keyPath]!
description.destinationEntity = entityDescriptionsByEntity[destinationEntity]!
if let destinationKeyPath = destinationKeyPath {
let inverseRelationshipDescription = findInverseRelationshipMatching(
destinationEntity: destinationEntity,
destinationKeyPath: destinationKeyPath
)
description.inverseRelationship = inverseRelationshipDescription
inverseRelationshipDescription.inverseRelationship = description
inverseRelationshipDescription.destinationEntity = entityDescription
description.destinationEntity!.properties = description.destinationEntity!.properties
}
case let relationship as RelationshipProtocol:
let (destinationType, destinationKeyPath) = relationship.inverse
let (destinationType, destinationKeyPath) = relationship.entityDescriptionValues().inverse
let destinationEntity = findEntity(for: destinationType)
let description = relationshipsByName[relationship.keyPath]!
description.destinationEntity = entityDescriptionsByEntity[destinationEntity]!
if let destinationKeyPath = destinationKeyPath() {
if let destinationKeyPath = destinationKeyPath {
let inverseRelationshipDescription = findInverseRelationshipMatching(
destinationEntity: destinationEntity,
@@ -502,9 +615,18 @@ public final class CoreStoreSchema: DynamicSchema {
}
}
private static func fourthPassSynthesizeManagedObjectClasses(for entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription], allCustomGettersSetters: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]]) {
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!
@@ -516,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
@@ -579,41 +702,89 @@ 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]
)
}
_ = allFieldCoders
.flatMap({ (_, values) in values })
.reduce(
into: [:] as [NSValueTransformerName: Internals.AnyFieldCoder],
{ (result, element) in result[element.value.transformerName] = element.value }
)
.forEach({ (_, fieldCoder) in fieldCoder.register() })
}
}

View File

@@ -41,7 +41,19 @@ extension DataStack {
*/
public func publishObject<O: DynamicObject>(_ object: O) -> ObjectPublisher<O> {
return ObjectPublisher<O>(objectID: object.cs_id(), context: self.unsafeContext())
return self.publishObject(object.cs_id())
}
/**
Creates an `ObjectPublisher` for a `DynamicObject` with the specified `ObjectID`. Multiple objects may then register themselves to be notified when changes are made to the `DynamicObject`.
- parameter objectID: the `ObjectID` of the object to observe changes from
- returns: an `ObjectPublisher` that broadcasts changes to `object`
*/
public func publishObject<O: DynamicObject>(_ objectID: O.ObjectID) -> ObjectPublisher<O> {
let context = self.unsafeContext()
return context.objectPublisher(objectID: objectID)
}
/**

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

@@ -0,0 +1,220 @@
//
// DiffableDataSource.BaseAdapter.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) || canImport(AppKit)
import Foundation
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - BaseAdapter
/**
The `DiffableDataSource.BaseAdapter` serves as a superclass for consumers of `ListPublisher` and `ListSnapshot` diffable data.
```
self.dataSource = DiffableDataSource.TableViewAdapter<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class BaseAdapter<O: DynamicObject, T: Target>: NSObject {
// MARK: Public
/**
The object type represented by this dataSource
*/
public typealias ObjectType = O
/**
The target to be updated by this dataSource
*/
public let target: T
/**
The `DataStack` where object fetches are performed
*/
public let dataStack: DataStack
/**
Initializes the `DiffableDataSource.BaseAdapter` object. This instance needs to be held on (retained) for as long as the target's lifecycle.
```
self.dataSource = DiffableDataSource.TableViewAdapterAdapter<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
- parameter tableView: the `UITableView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.TableViewAdapter`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object
*/
public init(target: T, dataStack: DataStack) {
self.target = target
self.dataStack = dataStack
self.dispatcher = Internals.DiffableDataUIDispatcher<O>(dataStack: dataStack)
}
/**
Clears the target.
- parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`.
*/
open func purge(animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) {
self.dispatcher.purge(
target: self.target,
animatingDifferences: animatingDifferences,
performUpdates: { target, changeset, setSections in
target.reload(
using: changeset,
animated: animatingDifferences,
setData: setSections
)
},
completion: completion
)
}
/**
Reloads the target using a `ListSnapshot`. This is typically from the `snapshot` property of a `ListPublisher`:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
``
- parameter snapshot: the `ListSnapshot` used to reload the target with. This is typically from the `snapshot` property of a `ListPublisher`.
- parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`.
*/
open func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) {
let diffableSnapshot = snapshot.diffableSnapshot
self.dispatcher.apply(
diffableSnapshot,
target: self.target,
animatingDifferences: animatingDifferences,
performUpdates: { target, changeset, setSections in
target.reload(
using: changeset,
animated: animatingDifferences,
setData: setSections
)
},
completion: completion
)
}
/**
Returns the number of sections
- parameter indexPath: the `IndexPath` to search for
- returns: the number of sections
*/
public func numberOfSections() -> Int {
return self.dispatcher.numberOfSections()
}
/**
Returns the number of items at the specified section, or `nil` if the section is not found
- parameter section: the section index to search for
- returns: the number of items at the specified section, or `nil` if the section is not found
*/
public func numberOfItems(inSection section: Int) -> Int? {
return self.dispatcher.numberOfItems(inSection: section)
}
/**
Returns the section identifier at the specified index, or `nil` if not found
- parameter section: the section index to search for
- returns: the section identifier at the specified indec, or `nil` if not found
*/
public func sectionID(for section: Int) -> String? {
return self.dispatcher.sectionIdentifier(inSection: section)
}
/**
Returns the object identifier for the item at the specified `IndexPath`, or `nil` if not found
- parameter indexPath: the `IndexPath` to search for
- returns: the object identifier for the item at the specified `IndexPath`, or `nil` if not found
*/
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
return self.dispatcher.itemIdentifier(for: indexPath)
}
/**
Returns the `IndexPath` for the item with the specified object identifier, or `nil` if not found
- parameter itemID: the object identifier to search for
- returns: the `IndexPath` for the item with the specified object identifier, or `nil` if not found
*/
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
return self.dispatcher.indexPath(for: itemID)
}
// MARK: Internal
internal let dispatcher: Internals.DiffableDataUIDispatcher<O>
}
}
#endif

View File

@@ -1,297 +0,0 @@
//
// DiffableDataSource.CollectionView-UIKit.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) && (os(iOS) || os(tvOS))
import UIKit
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - CollectionView
/**
The `DiffableDataSource.CollectionView` serves as a `UICollectionViewDataSource` that handles `ListPublisher` snapshots for a `UICollectionView`. Subclasses of `DiffableDataSource.CollectionView` may override some `UICollectionViewDataSource` methods as needed.
The `DiffableDataSource.CollectionView` instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionView<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
`DiffableDataSource.CollectionView` fully handles the reload animations.
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class CollectionView<O: DynamicObject>: NSObject, UICollectionViewDataSource {
// MARK: Public
/**
The object type represented by this dataSource
*/
public typealias ObjectType = O
/**
Initializes the `DiffableDataSource.CollectionView`. This instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionView<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
- parameter collectionView: the `UICollectionView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.CollectionView`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UICollectionViewCell` for the object
- parameter supplementaryViewProvider: an optional closure for providing `UICollectionReusableView` supplementary views. If not set, defaults to returning `nil`
*/
@nonobjc
public init(collectionView: UICollectionView, dataStack: DataStack, cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil }) {
self.collectionView = collectionView
self.cellProvider = cellProvider
self.supplementaryViewProvider = supplementaryViewProvider
self.dataStack = dataStack
self.dispatcher = Internals.DiffableDataUIDispatcher<O>(dataStack: dataStack)
super.init()
collectionView.dataSource = self
}
/**
Reloads the `UICollectionView` using a `ListSnapshot`. This is typically from the `snapshot` property of a `ListPublisher`:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
- parameter snapshot: the `ListSnapshot` used to reload the `UITableView` with. This is typically from the `snapshot` property of a `ListPublisher`.
- parameter animatingDifferences: if `true`, animations will be applied as configured by the `defaultRowAnimation` value. Defaults to `true`.
*/
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
let diffableSnapshot = snapshot.diffableSnapshot
self.dispatcher.apply(
diffableSnapshot as! Internals.DiffableDataSourceSnapshot,
view: self.collectionView,
animatingDifferences: animatingDifferences,
performUpdates: { collectionView, changeset, setSections in
collectionView.reload(
using: changeset,
setData: setSections
)
}
)
}
/**
Returns the object identifier for the item at the specified `IndexPath`, or `nil` if not found
- parameter indexPath: the `IndexPath` to search for
- returns: the object identifier for the item at the specified `IndexPath`, or `nil` if not found
*/
@nonobjc
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
return self.dispatcher.itemIdentifier(for: indexPath)
}
/**
Returns the `IndexPath` for the item with the specified object identifier, or `nil` if not found
- parameter itemID: the object identifier to search for
- returns: the `IndexPath` for the item with the specified object identifier, or `nil` if not found
*/
@nonobjc
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
return self.dispatcher.indexPath(for: itemID)
}
// MARK: - UICollectionViewDataSource
@objc
public dynamic func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.dispatcher.numberOfSections()
}
@objc
public dynamic func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.dispatcher.numberOfItems(inSection: section)
}
@objc
open dynamic func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let objectID = self.dispatcher.itemIdentifier(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
}
guard let cell = self.cellProvider(collectionView, indexPath, object) else {
Internals.abort("\(Internals.typeName(UICollectionViewDataSource.self)) returned a `nil` cell for \(Internals.typeName(IndexPath.self)) \(indexPath)")
}
return cell
}
@objc
open dynamic func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let view = self.supplementaryViewProvider(collectionView, kind, indexPath) else {
return UICollectionReusableView()
}
return view
}
// MARK: Private
private weak var collectionView: UICollectionView?
private let dataStack: DataStack
private let cellProvider: (UICollectionView, IndexPath, O) -> UICollectionViewCell?
private let supplementaryViewProvider: (UICollectionView, String, IndexPath) -> UICollectionReusableView?
private let dispatcher: Internals.DiffableDataUIDispatcher<O>
}
}
// MARK: - UICollectionView
extension UICollectionView {
// MARK: FilePrivate
// Implementation based on https://github.com/ra1028/DiffableDataSources
@nonobjc
fileprivate func reload<C, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void
) {
if case .none = window, let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
for changeset in stagedChangeset {
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
self.performBatchUpdates(
{
setData(changeset.data)
if !changeset.sectionDeleted.isEmpty {
self.deleteSections(IndexSet(changeset.sectionDeleted))
}
if !changeset.sectionInserted.isEmpty {
self.insertSections(IndexSet(changeset.sectionInserted))
}
if !changeset.sectionUpdated.isEmpty {
self.reloadSections(IndexSet(changeset.sectionUpdated))
}
for (source, target) in changeset.sectionMoved {
self.moveSection(source, toSection: target)
}
if !changeset.elementDeleted.isEmpty {
self.deleteItems(
at: changeset.elementDeleted.map { IndexPath(item: $0.element, section: $0.section) }
)
}
if !changeset.elementInserted.isEmpty {
self.insertItems(
at: changeset.elementInserted.map { IndexPath(item: $0.element, section: $0.section) }
)
}
if !changeset.elementUpdated.isEmpty {
self.reloadItems(
at: changeset.elementUpdated.map { IndexPath(item: $0.element, section: $0.section) }
)
}
for (source, target) in changeset.elementMoved {
self.moveItem(
at: IndexPath(item: source.element, section: source.section),
to: IndexPath(item: target.element, section: target.section)
)
}
},
completion: nil
)
}
}
}
#endif

View File

@@ -0,0 +1,232 @@
//
// DiffableDataSource.CollectionViewAdapter-AppKit.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(AppKit) && os(macOS)
import AppKit
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - CollectionView
/**
The `DiffableDataSource.CollectionViewAdapter` serves as a `NSCollectionViewDataSource` that handles `ListPublisher` snapshots for a `NSCollectionView`. Subclasses of `DiffableDataSource.CollectionViewAdapter` may override some `NSCollectionViewDataSource` methods as needed.
The `DiffableDataSource.CollectionViewAdapter` instance needs to be held on (retained) for as long as the `NSCollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
itemProvider: { (collectionView, indexPath, person) in
let item = collectionView.makeItem(withIdentifier: .collectionViewItem, for: indexPath) as! PersonItem
item.setPerson(person)
return item
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
`DiffableDataSource.CollectionViewAdapter` fully handles the reload animations.
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class CollectionViewAdapter<O: DynamicObject>: BaseAdapter<O, DefaultCollectionViewTarget<NSCollectionView>>, NSCollectionViewDataSource {
// MARK: Public
/**
Initializes the `DiffableDataSource.CollectionViewAdapter`. This instance needs to be held on (retained) for as long as the `NSCollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
itemProvider: { (collectionView, indexPath, person) in
let item = collectionView.makeItem(withIdentifier: .collectionViewItem, for: indexPath) as! PersonItem
item.setPerson(person)
return item
}
)
```
- parameter collectionView: the `NSCollectionView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.CollectionViewAdapter`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object
*/
@nonobjc
public init(collectionView: NSCollectionView, dataStack: DataStack, itemProvider: @escaping (NSCollectionView, IndexPath, O) -> NSCollectionViewItem?, supplementaryViewProvider: @escaping (NSCollectionView, String, IndexPath) -> NSView? = { _, _, _ in nil }) {
self.itemProvider = itemProvider
self.supplementaryViewProvider = supplementaryViewProvider
super.init(target: .init(collectionView), dataStack: dataStack)
collectionView.dataSource = self
}
// MARK: - NSCollectionViewDataSource
@objc
public dynamic func numberOfSections(in collectionView: NSCollectionView) -> Int {
return self.numberOfSections()
}
@objc
public dynamic func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return self.numberOfItems(inSection: section) ?? 0
}
@objc
open dynamic func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
guard let objectID = self.itemID(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
}
guard let item = self.itemProvider(collectionView, indexPath, object) else {
Internals.abort("\(Internals.typeName(NSCollectionViewDataSource.self)) returned a `nil` item for \(Internals.typeName(IndexPath.self)) \(indexPath)")
}
return item
}
@objc
open dynamic func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView {
guard let view = self.supplementaryViewProvider(collectionView, kind, indexPath) else {
return NSView()
}
return view
}
// MARK: Private
private let itemProvider: (NSCollectionView, IndexPath, O) -> NSCollectionViewItem?
private let supplementaryViewProvider: (NSCollectionView, String, IndexPath) -> NSView?
}
// MARK: - DefaultCollectionViewTarget
public struct DefaultCollectionViewTarget<T: NSCollectionView>: Target {
// MARK: Public
public typealias Base = T
public private(set) weak var base: Base?
public init(_ base: Base) {
self.base = base
}
// MARK: DiffableDataSource.Target:
public var shouldSuspendBatchUpdates: Bool {
return self.base?.window == nil
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
self.base?.deleteSections(indices)
}
public func insertSections(at indices: IndexSet, animated: Bool) {
self.base?.insertSections(indices)
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
self.base?.reloadSections(indices)
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
self.base?.moveSection(index, toSection: newIndex)
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.deleteItems(at: Set(indexPaths))
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.insertItems(at: Set(indexPaths))
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.reloadItems(at: Set(indexPaths))
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
self.base?.moveItem(at: indexPath, to: newIndexPath)
}
public func performBatchUpdates(updates: () -> Void, animated: Bool) {
self.base?.animator().performBatchUpdates(updates, completionHandler: nil)
}
public func reloadData() {
self.base?.reloadData()
}
}
}
// MARK: - Deprecated
extension DiffableDataSource {
@available(*, deprecated, renamed: "CollectionViewAdapter")
public typealias CollectionView = CollectionViewAdapter
}
#endif

View File

@@ -0,0 +1,231 @@
//
// DiffableDataSource.CollectionViewAdapter-UIKit.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) && (os(iOS) || os(tvOS))
import UIKit
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - CollectionView
/**
The `DiffableDataSource.CollectionViewAdapter` serves as a `UICollectionViewDataSource` that handles `ListPublisher` snapshots for a `UICollectionView`. Subclasses of `DiffableDataSource.CollectionViewAdapter` may override some `UICollectionViewDataSource` methods as needed.
The `DiffableDataSource.CollectionViewAdapter` instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
`DiffableDataSource.CollectionViewAdapter` fully handles the reload animations.
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class CollectionViewAdapter<O: DynamicObject>: BaseAdapter<O, DefaultCollectionViewTarget<UICollectionView>>, UICollectionViewDataSource {
// MARK: Public
/**
Initializes the `DiffableDataSource.CollectionViewAdapter`. This instance needs to be held on (retained) for as long as the `UICollectionView`'s lifecycle.
```
self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
- parameter collectionView: the `UICollectionView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.CollectionViewAdapter`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UICollectionViewCell` for the object
- parameter supplementaryViewProvider: an optional closure for providing `UICollectionReusableView` supplementary views. If not set, defaults to returning `nil`
*/
public init(collectionView: UICollectionView, dataStack: DataStack, cellProvider: @escaping (UICollectionView, IndexPath, O) -> UICollectionViewCell?, supplementaryViewProvider: @escaping (UICollectionView, String, IndexPath) -> UICollectionReusableView? = { _, _, _ in nil }) {
self.cellProvider = cellProvider
self.supplementaryViewProvider = supplementaryViewProvider
super.init(target: .init(collectionView), dataStack: dataStack)
collectionView.dataSource = self
}
// MARK: - UICollectionViewDataSource
@objc
public dynamic func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.numberOfSections()
}
@objc
public dynamic func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.numberOfItems(inSection: section) ?? 0
}
@objc
open dynamic func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let objectID = self.itemID(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
}
guard let cell = self.cellProvider(collectionView, indexPath, object) else {
Internals.abort("\(Internals.typeName(UICollectionViewDataSource.self)) returned a `nil` cell for \(Internals.typeName(IndexPath.self)) \(indexPath)")
}
return cell
}
@objc
open dynamic func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let view = self.supplementaryViewProvider(collectionView, kind, indexPath) else {
return UICollectionReusableView()
}
return view
}
// MARK: Private
private let cellProvider: (UICollectionView, IndexPath, O) -> UICollectionViewCell?
private let supplementaryViewProvider: (UICollectionView, String, IndexPath) -> UICollectionReusableView?
}
// MARK: - DefaultCollectionViewTarget
public struct DefaultCollectionViewTarget<T: UICollectionView>: Target {
// MARK: Public
public typealias Base = T
public private(set) weak var base: Base?
public init(_ base: Base) {
self.base = base
}
// MARK: DiffableDataSource.Target
public var shouldSuspendBatchUpdates: Bool {
return self.base?.window == nil
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
self.base?.deleteSections(indices)
}
public func insertSections(at indices: IndexSet, animated: Bool) {
self.base?.insertSections(indices)
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
self.base?.reloadSections(indices)
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
self.base?.moveSection(index, toSection: newIndex)
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.deleteItems(at: indexPaths)
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.insertItems(at: indexPaths)
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.reloadItems(at: indexPaths)
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
self.base?.moveItem(at: indexPath, to: newIndexPath)
}
public func performBatchUpdates(updates: () -> Void, animated: Bool) {
self.base?.performBatchUpdates(updates, completion: nil)
}
public func reloadData() {
self.base?.reloadData()
}
}
}
// MARK: - Deprecated
extension DiffableDataSource {
@available(*, deprecated, renamed: "CollectionViewAdapter")
public typealias CollectionView = CollectionViewAdapter
}
#endif

View File

@@ -1,372 +0,0 @@
//
// DiffableDataSource.TableView-UIKit.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) && (os(iOS) || os(tvOS))
import UIKit
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - TableView
/**
The `DiffableDataSource.TableView` serves as a `UITableViewDataSource` that handles `ListPublisher` snapshots for a `UITableView`. Subclasses of `DiffableDataSource.TableView` may override some `UITableViewDataSource` methods as needed.
The `DiffableDataSource.TableView` instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
```
self.dataSource = DiffableDataSource.TableView<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
`DiffableDataSource.TableView` fully handles the reload animations. To turn change the default animation, set the `defaultRowAnimation`.
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class TableView<O: DynamicObject>: NSObject, UITableViewDataSource {
// MARK: Open
/**
The animation style for row changes
*/
@nonobjc
open var defaultRowAnimation: UITableView.RowAnimation = .automatic
// MARK: Public
/**
The object type represented by this dataSource
*/
public typealias ObjectType = O
/**
Initializes the `DiffableDataSource.TableView`. This instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
```
self.dataSource = DiffableDataSource.TableView<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
- parameter tableView: the `UITableView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.TableView`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object
*/
@nonobjc
public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?) {
self.tableView = tableView
self.cellProvider = cellProvider
self.dataStack = dataStack
self.dispatcher = Internals.DiffableDataUIDispatcher<O>(dataStack: dataStack)
super.init()
tableView.dataSource = self
}
/**
Reloads the `UITableView` using a `ListSnapshot`. This is typically from the `snapshot` property of a `ListPublisher`:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
If the `defaultRowAnimation` is configured to, animations are also applied accordingly.
- parameter snapshot: the `ListSnapshot` used to reload the `UITableView` with. This is typically from the `snapshot` property of a `ListPublisher`.
- parameter animatingDifferences: if `true`, animations will be applied as configured by the `defaultRowAnimation` value. Defaults to `true`.
*/
@nonobjc
public func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true) {
let diffableSnapshot = snapshot.diffableSnapshot
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
//
// self.modernDataSource.apply(
// diffableSnapshot as! NSDiffableDataSourceSnapshot<String, NSManagedObjectID>,
// animatingDifferences: animatingDifferences,
// completion: nil
// )
// }
// else {
self.dispatcher.apply(
diffableSnapshot as! Internals.DiffableDataSourceSnapshot,
view: self.tableView,
animatingDifferences: animatingDifferences,
performUpdates: { tableView, changeset, setSections in
tableView.reload(
using: changeset,
with: self.defaultRowAnimation,
setData: setSections
)
}
)
// }
}
/**
Returns the object identifier for the item at the specified `IndexPath`, or `nil` if not found
- parameter indexPath: the `IndexPath` to search for
- returns: the object identifier for the item at the specified `IndexPath`, or `nil` if not found
*/
@nonobjc
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
return self.dispatcher.itemIdentifier(for: indexPath)
}
/**
Returns the `IndexPath` for the item with the specified object identifier, or `nil` if not found
- parameter itemID: the object identifier to search for
- returns: the `IndexPath` for the item with the specified object identifier, or `nil` if not found
*/
@nonobjc
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
return self.dispatcher.indexPath(for: itemID)
}
// MARK: - UITableViewDataSource
@objc
public dynamic func numberOfSections(in tableView: UITableView) -> Int {
return self.dispatcher.numberOfSections()
}
@objc
public dynamic func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dispatcher.numberOfItems(inSection: section)
}
@objc
open dynamic func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.dispatcher.sectionIdentifier(inSection: section)
}
@objc
open dynamic func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return nil
}
@objc
open dynamic func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let objectID = self.dispatcher.itemIdentifier(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
}
guard let cell = self.cellProvider(tableView, indexPath, object) else {
Internals.abort("\(Internals.typeName(UITableViewDataSource.self)) returned a `nil` cell for \(Internals.typeName(IndexPath.self)) \(indexPath)")
}
return cell
}
@objc
open dynamic func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
@objc
open dynamic func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
@objc
open dynamic func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {}
// MARK: Private
@nonobjc
private weak var tableView: UITableView?
@nonobjc
private let dataStack: DataStack
@nonobjc
private let cellProvider: (UITableView, IndexPath, O) -> UITableViewCell?
@nonobjc
private let dispatcher: Internals.DiffableDataUIDispatcher<O>
}
}
// MARK: - UITableView
extension UITableView {
// MARK: FilePrivate
// Implementation based on https://github.com/ra1028/DiffableDataSources
@nonobjc
fileprivate func reload<C, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
with animation: @autoclosure () -> RowAnimation,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void
) {
self.reload(
using: stagedChangeset,
deleteSectionsAnimation: animation(),
insertSectionsAnimation: animation(),
reloadSectionsAnimation: animation(),
deleteRowsAnimation: animation(),
insertRowsAnimation: animation(),
reloadRowsAnimation: animation(),
interrupt: interrupt,
setData: setData
)
}
// Implementation based on https://github.com/ra1028/DiffableDataSources
@nonobjc
fileprivate func reload<C, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
deleteSectionsAnimation: @autoclosure () -> RowAnimation,
insertSectionsAnimation: @autoclosure () -> RowAnimation,
reloadSectionsAnimation: @autoclosure () -> RowAnimation,
deleteRowsAnimation: @autoclosure () -> RowAnimation,
insertRowsAnimation: @autoclosure () -> RowAnimation,
reloadRowsAnimation: @autoclosure () -> RowAnimation,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void
) {
if case .none = window, let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
for changeset in stagedChangeset {
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
self.cs_performBatchUpdates {
setData(changeset.data)
if !changeset.sectionDeleted.isEmpty {
self.deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation())
}
if !changeset.sectionInserted.isEmpty {
self.insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation())
}
if !changeset.sectionUpdated.isEmpty {
self.reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation())
}
for (source, target) in changeset.sectionMoved {
self.moveSection(source, toSection: target)
}
if !changeset.elementDeleted.isEmpty {
self.deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation())
}
if !changeset.elementInserted.isEmpty {
self.insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation())
}
if !changeset.elementUpdated.isEmpty {
self.reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation())
}
for (source, target) in changeset.elementMoved {
self.moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section))
}
}
}
}
@nonobjc
private func cs_performBatchUpdates(_ updates: () -> Void) {
if #available(iOS 11.0, tvOS 11.0, *) {
self.performBatchUpdates(updates)
}
else {
self.beginUpdates()
updates()
self.endUpdates()
}
}
}
#endif

View File

@@ -0,0 +1,266 @@
//
// DiffableDataSource.TableViewAdapter-UIKit.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) && (os(iOS) || os(tvOS))
import UIKit
import CoreData
// MARK: - DiffableDataSource
extension DiffableDataSource {
// MARK: - TableViewAdapter
/**
The `DiffableDataSource.TableViewAdapterAdapter` serves as a `UITableViewDataSource` that handles `ListPublisher` snapshots for a `UITableView`. Subclasses of `DiffableDataSource.TableViewAdapter` may override some `UITableViewDataSource` methods as needed.
The `DiffableDataSource.TableViewAdapterAdapter` instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
```
self.dataSource = DiffableDataSource.TableViewAdapter<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
The dataSource can then apply changes from a `ListPublisher` as shown:
```
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot,
animatingDifferences: true
)
}
```
`DiffableDataSource.TableViewAdapter` fully handles the reload animations.
- SeeAlso: CoreStore's DiffableDataSource implementation is based on https://github.com/ra1028/DiffableDataSources
*/
open class TableViewAdapter<O: DynamicObject>: BaseAdapter<O, DefaultTableViewTarget<UITableView>>, UITableViewDataSource {
// MARK: Publi
/**
Initializes the `DiffableDataSource.TableViewAdapter`. This instance needs to be held on (retained) for as long as the `UITableView`'s lifecycle.
```
self.dataSource = DiffableDataSource.TableViewAdapter<Person>(
tableView: self.tableView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (tableView, indexPath, person) in
let cell = tableView.dequeueReusableCell(withIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
```
- parameter tableView: the `UITableView` to set the `dataSource` of. This instance is not retained by the `DiffableDataSource.TableViewAdapter`.
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object
*/
public init(tableView: UITableView, dataStack: DataStack, cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?) {
self.cellProvider = cellProvider
super.init(target: .init(tableView), dataStack: dataStack)
tableView.dataSource = self
}
/**
The target `UITableView`
*/
public var tableView: UITableView? {
return self.target.base
}
// MARK: - UITableViewDataSource
@objc
public dynamic func numberOfSections(in tableView: UITableView) -> Int {
return self.numberOfSections()
}
@objc
public dynamic func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfItems(inSection: section) ?? 0
}
@objc
open dynamic func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sectionID(for: section)
}
@objc
open dynamic func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return nil
}
@objc
open dynamic func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let objectID = self.itemID(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
guard let object = self.dataStack.fetchExisting(objectID) as O? else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) has been deleted")
}
guard let cell = self.cellProvider(tableView, indexPath, object) else {
Internals.abort("\(Internals.typeName(UITableViewDataSource.self)) returned a `nil` cell for \(Internals.typeName(IndexPath.self)) \(indexPath)")
}
return cell
}
@objc
open dynamic func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
@objc
open dynamic func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
@objc
open dynamic func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {}
// MARK: Private
@nonobjc
private let cellProvider: (UITableView, IndexPath, O) -> UITableViewCell?
}
// MARK: - DefaultTableViewTarget
public struct DefaultTableViewTarget<T: UITableView>: Target {
// MARK: Public
public typealias Base = T
public private(set) weak var base: Base?
public init(_ base: Base) {
self.base = base
}
// MARK: DiffableDataSource.Target
public var shouldSuspendBatchUpdates: Bool {
return self.base?.window == nil
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
self.base?.deleteSections(indices, with: .automatic)
}
public func insertSections(at indices: IndexSet, animated: Bool) {
self.base?.insertSections(indices, with: .automatic)
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
self.base?.reloadSections(indices, with: .automatic)
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
self.base?.moveSection(index, toSection: newIndex)
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.deleteRows(at: indexPaths, with: .automatic)
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.insertRows(at: indexPaths, with: .automatic)
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
self.base?.reloadRows(at: indexPaths, with: .automatic)
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
self.base?.moveRow(at: indexPath, to: newIndexPath)
}
public func performBatchUpdates(updates: () -> Void, animated: Bool) {
guard let base = self.base else {
return
}
if #available(iOS 11.0, tvOS 11.0, *) {
base.performBatchUpdates(updates)
}
else {
base.beginUpdates()
updates()
base.endUpdates()
}
}
public func reloadData() {
self.base?.reloadData()
}
}
}
// MARK: - Deprecated
extension DiffableDataSource {
@available(*, deprecated, renamed: "TableViewAdapter")
public typealias TableView = TableViewAdapter
}
#endif

View File

@@ -0,0 +1,215 @@
//
// DiffableDataSource.Target.swift
// CoreStore
//
// Copyright © 2018 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.
//
#if canImport(UIKit) || canImport(AppKit)
import Foundation
// MARK: - DiffableDataSourc
extension DiffableDataSource {
// MARK: - Target
/**
The `DiffableDataSource.Target` protocol allows custom views to consume `ListSnapshot` diffable data similar to how `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` reloads data for their corresponding views.
*/
public typealias Target = DiffableDataSourceTarget
}
// MARK: - DiffableDataSource.Target
/**
The `DiffableDataSource.Target` protocol allows custom views to consume `ListSnapshot` diffable data similar to how `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` reloads data for their corresponding views.
*/
public protocol DiffableDataSourceTarget {
// MARK: Public
/**
Whether `reloadData()` should be executed instead of `performBatchUpdates(updates:animated:)`.
*/
var shouldSuspendBatchUpdates: Bool { get }
/**
Deletes one or more sections.
*/
func deleteSections(at indices: IndexSet, animated: Bool)
/**
Inserts one or more sections
*/
func insertSections(at indices: IndexSet, animated: Bool)
/**
Reloads the specified sections.
*/
func reloadSections(at indices: IndexSet, animated: Bool)
/**
Moves a section to a new location.
*/
func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool)
/**
Deletes the items specified by an array of index paths.
*/
func deleteItems(at indexPaths: [IndexPath], animated: Bool)
/**
Inserts items at the locations identified by an array of index paths.
*/
func insertItems(at indexPaths: [IndexPath], animated: Bool)
/**
Reloads the specified items.
*/
func reloadItems(at indexPaths: [IndexPath], animated: Bool)
/**
Moves the item at a specified location to a destination location.
*/
func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool)
/**
Animates multiple insert, delete, reload, and move operations as a group.
*/
func performBatchUpdates(updates: () -> Void, animated: Bool)
/**
Reloads all sections and items.
*/
func reloadData()
}
extension DiffableDataSource.Target {
// MARK: Internal
internal func reload<C, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
animated: Bool,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void
) {
if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
for changeset in stagedChangeset {
if let interrupt = interrupt,
interrupt(changeset),
let data = stagedChangeset.last?.data {
setData(data)
self.reloadData()
return
}
self.performBatchUpdates(
updates: {
setData(changeset.data)
if !changeset.sectionDeleted.isEmpty {
self.deleteSections(
at: IndexSet(changeset.sectionDeleted),
animated: animated
)
}
if !changeset.sectionInserted.isEmpty {
self.insertSections(
at: IndexSet(changeset.sectionInserted),
animated: animated
)
}
if !changeset.sectionUpdated.isEmpty {
self.reloadSections(
at: IndexSet(changeset.sectionUpdated),
animated: animated
)
}
for (source, target) in changeset.sectionMoved {
self.moveSection(
at: source,
to: target,
animated: animated
)
}
if !changeset.elementDeleted.isEmpty {
self.deleteItems(
at: changeset.elementDeleted.map {
IndexPath(item: $0.element, section: $0.section)
},
animated: animated
)
}
if !changeset.elementInserted.isEmpty {
self.insertItems(
at: changeset.elementInserted.map {
IndexPath(item: $0.element, section: $0.section)
},
animated: animated
)
}
if !changeset.elementUpdated.isEmpty {
self.reloadItems(
at: changeset.elementUpdated.map {
IndexPath(item: $0.element, section: $0.section)
},
animated: animated
)
}
for (source, target) in changeset.elementMoved {
self.moveItem(
at: IndexPath(item: source.element, section: source.section),
to: IndexPath(item: target.element, section: target.section),
animated: animated
)
}
},
animated: animated
)
}
}
}
#endif

View File

@@ -23,12 +23,12 @@
// SOFTWARE.
//
#if os(iOS) || os(tvOS) || os(macOS)
#if canImport(UIKit) || canImport(AppKit)
// MARK: - DiffableDataSource
/**
Namespace for diffable data source types. See `DiffableDataSource.TableView` and `DiffableDataSource.CollectionView` for actual implementations
Namespace for diffable data source types. See `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` for actual implementations
*/
public enum DiffableDataSource {}

View File

@@ -114,7 +114,8 @@ extension NSManagedObject: DynamicObject {
public class func cs_fromRaw(object: NSManagedObject) -> Self {
return unsafeDowncast(object, to: self)
// unsafeDowncast fails debug assertion starting Swift 5.2
return _unsafeUncheckedDowncast(object, to: self)
}
public static func cs_matches(object: NSManagedObject) -> Bool {
@@ -153,41 +154,102 @@ extension CoreStoreObject {
public class func cs_snapshotDictionary(id: ObjectID, context: NSManagedObjectContext) -> [String: Any]? {
func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) {
var values: [KeyPathString: Any] = [:]
if self.meta.needsReflection {
if let superClassMirror = mirror.superclassMirror {
func initializeAttributes(mirror: Mirror, object: Self, into attributes: inout [KeyPathString: Any]) {
initializeAttributes(
mirror: superClassMirror,
object: object,
into: &attributes
)
if let superClassMirror = mirror.superclassMirror {
initializeAttributes(
mirror: superClassMirror,
object: object,
into: &attributes
)
}
for child in mirror.children {
switch child.value {
case let property as FieldAttributeProtocol:
Internals.assert(
object.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue."
)
attributes[property.keyPath] = type(of: property).read(
field: property,
for: object.rawObject!
)
case let property as FieldRelationshipProtocol:
Internals.assert(
object.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue."
)
attributes[property.keyPath] = type(of: property).valueForSnapshot(
field: property,
for: object.rawObject!
)
case let property as AttributeProtocol:
attributes[property.keyPath] = property.valueForSnapshot
case let property as RelationshipProtocol:
attributes[property.keyPath] = property.valueForSnapshot
default:
continue
}
}
}
for child in mirror.children {
guard let object = context.fetchExisting(id) as CoreStoreObject? else {
switch child.value {
return nil
}
initializeAttributes(
mirror: Mirror(reflecting: object),
object: object as! Self,
into: &values
)
}
else {
case let property as AttributeProtocol:
attributes[property.keyPath] = property.valueForSnapshot
guard
let object = context.fetchExisting(id) as CoreStoreObject?,
let rawObject = object.rawObject
else {
case let property as RelationshipProtocol:
attributes[property.keyPath] = property.valueForSnapshot
return nil
}
for property in self.metaProperties(includeSuperclasses: true) {
switch property {
case let property as FieldAttributeProtocol:
Internals.assert(
object.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue."
)
values[property.keyPath] = type(of: property).read(
field: property,
for: rawObject
)
case let property as FieldRelationshipProtocol:
Internals.assert(
object.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(type(of: property).dynamicObjectType))'s value outside it's designated queue."
)
values[property.keyPath] = type(of: property).valueForSnapshot(
field: property,
for: object.rawObject!
)
default:
continue
}
}
}
guard let object = context.fetchExisting(id) as CoreStoreObject? else {
return nil
}
var values: [KeyPathString: Any] = [:]
initializeAttributes(
mirror: Mirror(reflecting: object),
object: object as! Self,
into: &values
)
return values
}

View File

@@ -1,143 +0,0 @@
//
// DynamicObjectMeta.swift
// CoreStore iOS
//
// Created by John Estropia on 2019/08/20.
// Copyright © 2019 John Rommel Estropia. All rights reserved.
//
#if swift(>=5.1)
import CoreData
import Foundation
// MARK: - DynamicObjectMeta
@dynamicMemberLookup
public struct DynamicObjectMeta<R, D>: CustomDebugStringConvertible {
// MARK: Public
public typealias Root = R
public typealias Destination = D
// MARK: CustomDebugStringConvertible
public var debugDescription: String {
return self.keyPathString
}
// MARK: Internal
internal let keyPathString: KeyPathString
internal init(keyPathString: KeyPathString) {
self.keyPathString = keyPathString
}
internal func appending<D2>(keyPathString: KeyPathString) -> DynamicObjectMeta<(R, D), D2> {
return .init(keyPathString: [self.keyPathString, keyPathString].joined(separator: "."))
}
}
// MARK: - DynamicObjectMeta where Destination: NSManagedObject
extension DynamicObjectMeta where Destination: NSManagedObject {
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: AllowedObjectiveCAttributeKeyPathValue>(dynamicMember member: KeyPath<Destination, V>) -> DynamicObjectMeta<(Root, Destination), V.ReturnValueType> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSManagedObject>(dynamicMember member: KeyPath<Destination, V>) -> DynamicObjectMeta<(Root, Destination), V> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSManagedObject>(dynamicMember member: KeyPath<Destination, V?>) -> DynamicObjectMeta<(Root, Destination), V> {
// TODO: not working
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSOrderedSet>(dynamicMember member: KeyPath<Destination, V>) -> DynamicObjectMeta<(Root, Destination), V> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSOrderedSet>(dynamicMember member: KeyPath<Destination, V?>) -> DynamicObjectMeta<(Root, Destination), V> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSSet>(dynamicMember member: KeyPath<Destination, V>) -> DynamicObjectMeta<(Root, Destination), V> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<V: NSSet>(dynamicMember member: KeyPath<Destination, V?>) -> DynamicObjectMeta<(Root, Destination), V> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
}
// MARK: - DynamicObjectMeta where Destination: CoreStoreObject
extension DynamicObjectMeta where Destination: CoreStoreObject {
/**
Returns the value for the property identified by a given key.
*/
public subscript<K: AttributeKeyPathStringConvertible>(dynamicMember member: KeyPath<Destination, K>) -> DynamicObjectMeta<(Root, Destination), K.ReturnValueType> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<K: RelationshipKeyPathStringConvertible>(dynamicMember member: KeyPath<Destination, K>) -> DynamicObjectMeta<(Root, Destination), K.DestinationValueType> {
let keyPathString = String(keyPath: member)
return self.appending(keyPathString: keyPathString)
}
}
#endif

View File

@@ -68,89 +68,124 @@ extension DynamicSchema {
for (attributeName, attribute) in attributesByName {
let containerType: String
if attribute.attributeType == .transformableAttributeType {
if attribute.isOptional {
containerType = "Transformable.Optional"
}
else {
containerType = "Transformable.Required"
}
if attribute.isTransient || attribute.attributeType == .undefinedAttributeType {
containerType = "Field.Virtual"
}
else if attribute.attributeType == .transformableAttributeType {
containerType = "Field.Coded"
}
else {
if attribute.isOptional {
containerType = "Value.Optional"
}
else {
containerType = "Value.Required"
}
containerType = "Field.Stored"
}
let valueType: Any.Type
var valueTypeString: String
var defaultString = ""
var coderString = ""
switch attribute.attributeType {
case .integer16AttributeType:
valueType = Int16.self
valueTypeString = String(describing: Int16.self)
if let defaultValue = (attribute.defaultValue as! Int16.QueryableNativeType?).flatMap(Int16.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue)"
defaultString = " = \(defaultValue)"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .integer32AttributeType:
valueType = Int32.self
valueTypeString = String(describing: Int32.self)
if let defaultValue = (attribute.defaultValue as! Int32.QueryableNativeType?).flatMap(Int32.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue)"
defaultString = " = \(defaultValue)"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .integer64AttributeType:
valueType = Int64.self
valueTypeString = String(describing: Int64.self)
if let defaultValue = (attribute.defaultValue as! Int64.QueryableNativeType?).flatMap(Int64.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue)"
defaultString = " = \(defaultValue)"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .decimalAttributeType:
valueType = NSDecimalNumber.self
valueTypeString = String(describing: NSDecimalNumber.self)
if let defaultValue = (attribute.defaultValue as! NSDecimalNumber?) {
defaultString = ", initial: NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")"
defaultString = " = NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .doubleAttributeType:
valueType = Double.self
valueTypeString = String(describing: Double.self)
if let defaultValue = (attribute.defaultValue as! Double.QueryableNativeType?).flatMap(Double.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue)"
defaultString = " = \(defaultValue)"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .floatAttributeType:
valueType = Float.self
valueTypeString = String(describing: Float.self)
if let defaultValue = (attribute.defaultValue as! Float.QueryableNativeType?).flatMap(Float.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue)"
defaultString = " = \(defaultValue)"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .stringAttributeType:
valueType = String.self
valueTypeString = String(describing: String.self)
if let defaultValue = (attribute.defaultValue as! String.QueryableNativeType?).flatMap(String.cs_fromQueryableNativeType) {
// TODO: escape strings
defaultString = ", initial: \"\(defaultValue)\""
defaultString = " = \"\(defaultValue.replacingOccurrences(of: "\\", with: "\\\\"))\""
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .booleanAttributeType:
valueType = Bool.self
valueTypeString = String(describing: Bool.self)
if let defaultValue = (attribute.defaultValue as! Bool.QueryableNativeType?).flatMap(Bool.cs_fromQueryableNativeType) {
defaultString = ", initial: \(defaultValue ? "true" : "false")"
defaultString = " = \(defaultValue ? "true" : "false")"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .dateAttributeType:
valueType = Date.self
valueTypeString = String(describing: Date.self)
if let defaultValue = (attribute.defaultValue as! Date.QueryableNativeType?).flatMap(Date.cs_fromQueryableNativeType) {
defaultString = ", initial: Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))"
defaultString = " = Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .binaryDataAttributeType:
valueType = Data.self
valueTypeString = String(describing: Data.self)
if let defaultValue = (attribute.defaultValue as! Data.QueryableNativeType?).flatMap(Data.cs_fromQueryableNativeType) {
let bytes = defaultValue.withUnsafeBytes { (pointer) in
@@ -158,46 +193,106 @@ extension DynamicSchema {
.bindMemory(to: UInt64.self)
.map({ "\("0x\(String($0, radix: 16, uppercase: false))")" })
}
defaultString = ", initial: Data(bytes: [\(bytes.joined(separator: ", "))])"
defaultString = " = Data(bytes: [\(bytes.joined(separator: ", "))])"
}
else if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
case .transformableAttributeType:
if let valueTransformerName = attribute.valueTransformerName {
coderString = ", coder: /* Required compatible FieldCoderType implementation for ValueTransformer named \"\(valueTransformerName)\" */"
}
else {
coderString = ", coder: FieldCoders.NSCoding.self"
}
if let attributeValueClassName = attribute.attributeValueClassName {
valueType = NSClassFromString(attributeValueClassName)!
valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!)
}
else {
valueTypeString = "/* <required> */"
}
if let defaultValue = attribute.defaultValue {
switch defaultValue {
case let defaultValueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
if let defaultValue = defaultValueBox.value {
defaultString = " = /* \"\(defaultValue)\" */"
}
else if attribute.isOptional {
defaultString = " = nil"
}
else {
defaultString = " = /* <required> */"
}
case let defaultValue:
defaultString = " = /* \"\(defaultValue)\" */"
}
}
else if attribute.isOptional {
defaultString = " = nil"
}
else {
valueType = (NSCoding & NSCopying).self
defaultString = " = /* <required> */"
}
if let defaultValue = attribute.defaultValue {
defaultString = ", initial: /* \"\(defaultValue)\" */"
if attribute.isOptional {
valueTypeString += "?"
}
else if !attribute.isOptional {
defaultString = ", initial: /* required */"
case .undefinedAttributeType where attribute.isTransient:
coderString = ", customGetter: \\* <required> *\\"
if let attributeValueClassName = attribute.attributeValueClassName {
valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!)
}
else {
valueTypeString = " = /* <required> */"
}
if attribute.isOptional {
valueTypeString += "?"
defaultString = " = nil"
}
default:
fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)")
}
let transientString = attribute.isTransient ? ", isTransient: true" : ""
// TODO: escape strings
let versionHashModifierString = attribute.versionHashModifier
.flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? ""
// TODO: escape strings
.map({ ", versionHashModifier: \"\($0)\"" }) ?? ""
let renamingIdentifierString = attribute.renamingIdentifier
.flatMap({ ($0 == attributeName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? ""
output.append(" let \(attributeName) = \(containerType)<\(String(describing: valueType))>(\"\(attributeName)\"\(defaultString)\(transientString)\(versionHashModifierString)\(renamingIdentifierString))\n")
.map({ ($0 == attributeName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? ""
if attributeName.hasPrefix("_") {
output.append(" #warning(\"Field variable names cannot start with underscores)")
}
output.append(" @\(containerType)(\"\(attributeName)\"\(versionHashModifierString)\(renamingIdentifierString)\(coderString))\n")
output.append(" var \(attributeName): \(valueTypeString)\(defaultString)\n\n")
}
}
let relationshipsByName = entity.relationshipsByName
if !relationshipsByName.isEmpty {
output.append(" \n")
for (relationshipName, relationship) in relationshipsByName {
let containerType: String
let destinationEntityName = relationship.destinationEntity!.name!
var minCountString = ""
var maxCountString = ""
if relationship.isToMany {
@@ -206,11 +301,11 @@ extension DynamicSchema {
let maxCount = relationship.maxCount
if relationship.isOrdered {
containerType = "Relationship.ToManyOrdered"
containerType = "[\(destinationEntityName)]"
}
else {
containerType = "Relationship.ToManyUnordered"
containerType = "Set<\(destinationEntityName)>"
}
if minCount > 0 {
@@ -222,16 +317,16 @@ extension DynamicSchema {
}
}
else {
containerType = "Relationship.ToOne"
containerType = "\(destinationEntityName)?"
}
var inverseString = ""
let relationshipQualifier = "\(entityName).\(relationshipName)"
if !addedInverse.contains(relationshipQualifier),
let inverseRelationship = relationship.inverseRelationship {
inverseString = ", inverse: { $0.\(inverseRelationship.name) }"
addedInverse.insert("\(relationship.destinationEntity!.name!).\(inverseRelationship.name)")
inverseString = ", inverse: \\.$\(inverseRelationship.name)"
addedInverse.insert("\(destinationEntityName).\(inverseRelationship.name)")
}
var deleteRuleString = ""
if relationship.deleteRule != .nullifyDeleteRule {
@@ -252,10 +347,15 @@ extension DynamicSchema {
}
}
let versionHashModifierString = relationship.versionHashModifier
.flatMap({ ", versionHashModifier: \"\($0)\"" }) ?? ""
.map({ ", versionHashModifier: \"\($0)\"" }) ?? ""
let renamingIdentifierString = relationship.renamingIdentifier
.flatMap({ ($0 == relationshipName ? "" : ", renamingIdentifier: \"\($0)\"") as String }) ?? ""
output.append(" let \(relationshipName) = \(containerType)<\(relationship.destinationEntity!.name!)>(\"\(relationshipName)\"\(inverseString)\(deleteRuleString)\(minCountString)\(maxCountString)\(versionHashModifierString)\(renamingIdentifierString))\n")
.map({ ($0 == relationshipName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? ""
if relationshipName.hasPrefix("_") {
output.append(" #error(\"Field variable names cannot start with underscores)\n")
}
output.append(" @Field.Relationship(\"\(relationshipName)\"\(minCountString)\(maxCountString)\(inverseString)\(deleteRuleString)\(versionHashModifierString)\(renamingIdentifierString))\n")
output.append(" var \(relationshipName): \(containerType)\n\n")
}
}
}

View File

@@ -0,0 +1,232 @@
//
// FIeldRelationshipType.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: - FieldRelationshipType
/**
Values to be used for `Field.Relationship` properties.
*/
public protocol FieldRelationshipType {
/**
The destination object's type
*/
associatedtype DestinationObjectType: CoreStoreObject
/**
The Objective-C native type synthesized by Core Data
*/
associatedtype NativeValueType: AnyObject
/**
The corresponding value for this field returned from `ObjectSnapshot` properties.
*/
associatedtype SnapshotValueType
/**
The corresponding value for this field returned from `ObjectPublisher` properties.
*/
associatedtype PublishedType
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_toReturnType(from value: NativeValueType?) -> Self
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_toNativeType(from value: Self) -> NativeValueType?
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType
/**
Used internally by CoreStore. Do not call directly.
*/
static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType
}
// MARK: - FieldRelationshipToOneType: FieldRelationshipType
public protocol FieldRelationshipToOneType: FieldRelationshipType {}
// MARK: - FieldRelationshipToManyType: FieldRelationshipType where Self: Sequence
public protocol FieldRelationshipToManyType: FieldRelationshipType where Self: Sequence {}
// MARK: - FieldRelationshipToManyOrderedType: FieldRelationshipToManyType
public protocol FieldRelationshipToManyOrderedType: FieldRelationshipToManyType {}
// MARK: - FieldRelationshipToManyUnorderedType: FieldRelationshipToManyType
public protocol FieldRelationshipToManyUnorderedType: FieldRelationshipToManyType {}
// MARK: - Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrapped: CoreStoreObject
extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrapped: CoreStoreObject {
// MARK: FieldRelationshipType
public typealias DestinationObjectType = Wrapped
public typealias NativeValueType = NSManagedObject
public typealias SnapshotValueType = NSManagedObjectID?
public typealias PublishedType = ObjectPublisher<DestinationObjectType>?
public static func cs_toReturnType(from value: NativeValueType?) -> Self {
return value.map(Wrapped.cs_fromRaw(object:))
}
public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType {
return value.map(context.objectPublisher(objectID:))
}
public static func cs_toNativeType(from value: Self) -> NativeValueType? {
return value?.cs_toRaw()
}
public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType {
return value?.objectID()
}
public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType {
return objectIDs.first
}
}
// MARK: - Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyOrderedType where Element: CoreStoreObject
extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyOrderedType where Element: CoreStoreObject {
// MARK: FieldRelationshipType
public typealias DestinationObjectType = Element
public typealias NativeValueType = NSOrderedSet
public typealias SnapshotValueType = [NSManagedObjectID]
public typealias PublishedType = [ObjectPublisher<DestinationObjectType>]
public static func cs_toReturnType(from value: NativeValueType?) -> Self {
guard let value = value else {
return []
}
return value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) })
}
public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType {
return value.map(context.objectPublisher(objectID:))
}
public static func cs_toNativeType(from value: Self) -> NativeValueType? {
return NSOrderedSet(array: value.map({ $0.rawObject! }))
}
public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType {
return value.map({ $0.objectID() })
}
public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType {
return objectIDs
}
}
// MARK: - Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyUnorderedType where Element: CoreStoreObject
extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelationshipToManyUnorderedType where Element: CoreStoreObject {
// MARK: FieldRelationshipType
public typealias DestinationObjectType = Element
public typealias NativeValueType = NSSet
public typealias SnapshotValueType = Set<NSManagedObjectID>
public typealias PublishedType = Set<ObjectPublisher<DestinationObjectType>>
public static func cs_toReturnType(from value: NativeValueType?) -> Self {
guard let value = value else {
return []
}
return Set(value.map({ Element.cs_fromRaw(object: $0 as! NSManagedObject) }))
}
public static func cs_toPublishedType(from value: SnapshotValueType, in context: NSManagedObjectContext) -> PublishedType {
return PublishedType(value.map(context.objectPublisher(objectID:)))
}
public static func cs_toNativeType(from value: Self) -> NativeValueType? {
return NSSet(array: value.map({ $0.rawObject! }))
}
public static func cs_toSnapshotType(from value: PublishedType) -> SnapshotValueType {
return SnapshotValueType(value.map({ $0.objectID() }))
}
public static func cs_valueForSnapshot(from objectIDs: [DestinationObjectType.ObjectID]) -> SnapshotValueType {
return .init(objectIDs)
}
}

744
Sources/Field.Coded.swift Normal file
View File

@@ -0,0 +1,744 @@
//
// Field.Coded.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: - FieldContainer
extension FieldContainer {
// MARK: - Coded
/**
The containing type for stored property values. Any type supported by the specified encoder/decoder are allowed.
```
class Animal: 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
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored.
*/
@propertyWrapper
public struct Coded<V>: AttributeKeyPathStringConvertible, FieldAttributeProtocol {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self)
var eyeColor: UIColor = .black
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 coder: The `FieldCoderType` to be used for encoding and decoding the value
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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<Coder: FieldCoderType>(
wrappedValue initial: @autoclosure @escaping () -> V,
_ 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> = []
) where Coder.FieldStoredValue == V {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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
)
}
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@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.
- 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 coder: The closures to be used for encoding and decoding the value
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V,
_ 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> = []
) {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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
)
}
// MARK: @propertyWrapper
@available(*, unavailable)
public var wrappedValue: V {
get { fatalError() }
set { fatalError() }
}
public var projectedValue: Self {
return self
}
public static subscript(
_enclosingInstance instance: O,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<O, V>,
storage storageKeyPath: ReferenceWritableKeyPath<O, Self>
) -> V {
get {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V
}
set {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue)
}
}
// MARK: AnyKeyPathStringConvertible
public var cs_keyPathString: String {
return self.keyPath
}
// MARK: KeyPathStringConvertible
public typealias ObjectType = O
public typealias DestinationValueType = V
// MARK: AttributeKeyPathStringConvertible
public typealias ReturnValueType = DestinationValueType
// MARK: PropertyProtocol
internal let keyPath: KeyPathString
// MARK: FieldProtocol
internal static var dynamicObjectType: CoreStoreObject.Type {
return ObjectType.self
}
internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
let field = field as! Self
if let customGetter = field.customGetter {
return customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field)
)
}
let keyPath = field.keyPath
switch rawObject.value(forKey: keyPath) {
case let rawValue as V:
return rawValue
default:
return nil
}
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
Internals.assert(
rawObject.isEditableInContext() == true,
"Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction."
)
let newValue = newValue as! V
let field = field as! Self
let keyPath = field.keyPath
if let customSetter = field.customSetter {
return customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field),
newValue
)
}
return rawObject.setValue(newValue, forKey: keyPath)
}
// MARK: FieldAttributeProtocol
internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues
internal var getter: CoreStoreManagedObject.CustomGetter? {
let keyPath = self.keyPath
guard let customGetter = self.customGetter else {
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
switch rawObject.primitiveValue(forKey: keyPath) {
case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
rawObject.setPrimitiveValue(valueBox.value, forKey: keyPath)
return valueBox.value
case let value:
return value
}
}
}
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
let value = customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self)
)
return value
}
}
internal var setter: CoreStoreManagedObject.CustomSetter? {
guard let customSetter = self.customSetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any, _ newValue: Any?) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.willChangeValue(forKey: keyPath)
defer {
rawObject.didChangeValue(forKey: keyPath)
}
customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self),
newValue as! V
)
}
}
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: (() -> Any?)?,
keyPath: KeyPathString,
isOptional: Bool,
versionHashModifier: @escaping () -> String?,
renamingIdentifier: @escaping () -> String?,
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
self.entityDescriptionValues = {
let fieldCoder = valueTransformer()
return (
attributeType: .transformableAttributeType,
isOptional: isOptional,
isTransient: false,
allowsExternalBinaryDataStorage: false,
versionHashModifier: versionHashModifier(),
renamingIdentifier: renamingIdentifier(),
valueTransformer: fieldCoder,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: defaultValue.map {
Internals.AnyFieldCoder.TransformableDefaultValueCodingBox(
defaultValue: $0(),
fieldCoder: fieldCoder
) as Any
}
)
}
self.customGetter = customGetter
self.customSetter = customSetter
self.dynamicInitialValue = dynamicInitialValue
}
// MARK: Private
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)?
}
}
// MARK: - FieldContainer.Coded where V: FieldOptionalType
extension FieldContainer.Coded where V: FieldOptionalType {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Coded("eyeColor", coder: FieldCoders.NSCoding.self)
var eyeColor: UIColor? = nil
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 coder: The `FieldCoderType` to be used for encoding and decoding the value
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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<Coder: FieldCoderType>(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ 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> = []
) where Coder.FieldStoredValue == V.Wrapped {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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
)
}
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@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.
- 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 coder: The closures to be used for encoding and decoding the value
- parameter customGetter: use this closure as an "override" for the default property getter. The closure receives a `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ 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> = []
) {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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
)
}
}
// MARK: - FieldContainer.Coded where V: DefaultNSSecureCodable
extension FieldContainer.Coded where V: DefaultNSSecureCodable {
/**
Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`.
```
class Person: CoreStoreObject {
@Field.Coded("customInfo")
var customInfo: NSDictionary = [:]
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V,
_ 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> = []
) {
self.init(
defaultValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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
)
}
}
// MARK: - FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable
extension FieldContainer.Coded where V: FieldOptionalType, V.Wrapped: DefaultNSSecureCodable {
/**
Initializes the metadata for the property. This overload is for types supported by Core Data's default NSSecureCodable implementation: `NSArray`, `NSDictionary`, `NSSet`, `NSString`, `NSNumber`, `NSDate`, `NSData`, `NSURL`, `NSUUID`, and `NSNull`.
```
class Person: CoreStoreObject {
@Field.Coded("customInfo")
var customInfo: NSDictionary? = nil
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ 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> = []
) {
self.init(
defaultValue: { initial().cs_wrappedValue },
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
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

@@ -0,0 +1,585 @@
//
// Field.ToOne.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: - FieldContainer
extension FieldContainer {
// MARK: - Relationship
/**
The containing type for relationships. Any `CoreStoreObject` subclass can be a destination type. Inverse relationships should be declared from the destination type as well, using the `inverse:` argument for the relationship.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Dog>
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Relationship(...) var` syntax will be ignored.
*/
@propertyWrapper
public struct Relationship<V: FieldRelationshipType>: RelationshipKeyPathStringConvertible, FieldRelationshipProtocol {
/**
Overload for compiler error message only
*/
@available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.")
public init(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
minCount: Int = 0,
maxCount: Int = 0,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil
) {
fatalError()
}
/**
Overload for compiler error message only
*/
@available(*, unavailable, message: "Field.Relationship properties are not allowed to have initial values, including `nil`.")
public init<D>(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
minCount: Int = 0,
maxCount: Int = 0,
inverse: KeyPath<V.DestinationObjectType, FieldContainer<V.DestinationObjectType>.Relationship<D>>,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil
) {
fatalError()
}
// MARK: @propertyWrapper
@available(*, unavailable)
public var wrappedValue: V {
get { fatalError() }
set { fatalError() }
}
public var projectedValue: Self {
return self
}
public static subscript(
_enclosingInstance instance: O,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<O, V>,
storage storageKeyPath: ReferenceWritableKeyPath<O, Self>
) -> V {
get {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V
}
set {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue)
}
}
// MARK: AnyKeyPathStringConvertible
public var cs_keyPathString: String {
return self.keyPath
}
// MARK: KeyPathStringConvertible
public typealias ObjectType = O
public typealias DestinationValueType = V.DestinationObjectType
// MARK: RelationshipKeyPathStringConvertible
public typealias ReturnValueType = V
// MARK: PropertyProtocol
internal let keyPath: KeyPathString
// MARK: FieldProtocol
internal static var dynamicObjectType: CoreStoreObject.Type {
return ObjectType.self
}
internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
let field = field as! Self
let keyPath = field.keyPath
return V.cs_toReturnType(
from: rawObject.value(forKey: keyPath) as! V.NativeValueType?
)
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
Internals.assert(
rawObject.isEditableInContext() == true,
"Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction."
)
let newValue = newValue as! V
let field = field as! Self
let keyPath = field.keyPath
return rawObject.setValue(
V.cs_toNativeType(from: newValue),
forKey: keyPath
)
}
// MARK: FieldRelationshipProtocol
internal let entityDescriptionValues: () -> FieldRelationshipProtocol.EntityDescriptionValues
internal static func valueForSnapshot(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = field as! Self
return V.cs_valueForSnapshot(from: rawObject.objectIDs(forRelationshipNamed: field.keyPath))
}
// MARK: FilePrivate
fileprivate init(
keyPath: KeyPathString,
isToMany: Bool,
isOrdered: Bool,
deleteRule: DeleteRule,
inverseKeyPath: @escaping () -> KeyPathString?,
versionHashModifier: @escaping () -> String?,
renamingIdentifier: @escaping () -> String?,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>,
minCount: Int,
maxCount: Int
) {
self.keyPath = keyPath
self.entityDescriptionValues = {
let range = (Swift.max(0, minCount) ... maxCount)
return (
isToMany: isToMany,
isOrdered: isOrdered,
deleteRule: deleteRule.nativeValue,
inverse: (type: V.DestinationObjectType.self, keyPath: inverseKeyPath()),
versionHashModifier: versionHashModifier(),
renamingIdentifier: renamingIdentifier(),
affectedByKeyPaths: affectedByKeyPaths(),
minCount: range.lowerBound,
maxCount: range.upperBound
)
}
}
// MARK: - DeleteRule
/**
These constants define what happens to relationships when an object is deleted.
*/
public enum DeleteRule {
// MARK: Public
/**
If the object is deleted, back pointers from the objects to which it is related are nullified.
*/
case nullify
/**
If the object is deleted, the destination object or objects of this relationship are also deleted.
*/
case cascade
/**
If the destination of this relationship is not nil, the delete creates a validation error.
*/
case deny
// MARK: Internal
internal var nativeValue: NSDeleteRule {
switch self {
case .nullify: return .nullifyDeleteRule
case .cascade: return .cascadeDeleteRule
case .deny: return .denyDeleteRule
}
}
}
}
}
// MARK: - FieldContainer.Relationship where V: FieldRelationshipToOneType
extension FieldContainer.Relationship where V: FieldRelationshipToOneType {
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
keyPath: keyPath,
isToMany: false,
isOrdered: false,
deleteRule: deleteRule,
inverseKeyPath: { nil },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: 0,
maxCount: 1
)
}
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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<D>(
_ keyPath: KeyPathString,
inverse: KeyPath<V.DestinationObjectType, FieldContainer<V.DestinationObjectType>.Relationship<D>>,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) where D: FieldRelationshipType {
self.init(
keyPath: keyPath,
isToMany: false,
isOrdered: false,
deleteRule: deleteRule,
inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: 0,
maxCount: 1
)
}
}
// MARK: - FieldContainer.Relationship: ToManyRelationshipKeyPathStringConvertible where V: FieldRelationshipToManyType
extension FieldContainer.Relationship: ToManyRelationshipKeyPathStringConvertible where V: FieldRelationshipToManyType {}
// MARK: - FieldContainer.Relationship where V: FieldRelationshipToManyOrderedType
extension FieldContainer.Relationship where V: FieldRelationshipToManyOrderedType {
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Array<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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,
minCount: Int = 0,
maxCount: Int = 0,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
keyPath: keyPath,
isToMany: true,
isOrdered: true,
deleteRule: deleteRule,
inverseKeyPath: { nil },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: minCount,
maxCount: maxCount
)
}
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Array<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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<D>(
_ keyPath: KeyPathString,
minCount: Int = 0,
maxCount: Int = 0,
inverse: KeyPath<V.DestinationObjectType, FieldContainer<V.DestinationObjectType>.Relationship<D>>,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) where D: FieldRelationshipType {
self.init(
keyPath: keyPath,
isToMany: true,
isOrdered: true,
deleteRule: deleteRule,
inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: minCount,
maxCount: maxCount
)
}
}
// MARK: - FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedType
extension FieldContainer.Relationship where V: FieldRelationshipToManyUnorderedType {
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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,
minCount: Int = 0,
maxCount: Int = 0,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) {
self.init(
keyPath: keyPath,
isToMany: true,
isOrdered: false,
deleteRule: deleteRule,
inverseKeyPath: { nil },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: minCount,
maxCount: maxCount
)
}
/**
Initializes the metadata for the relationship. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object. Make sure to declare this relationship's inverse relationship on its destination object. Due to Swift's compiler limitation, only one of the relationship and its inverse can declare an `inverse:` argument.
```
class Dog: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Dog>
}
```
- parameter keyPath: the permanent name for this relationship.
- parameter minCount: the minimum number of objects in this relationship UNLESS THE RELATIONSHIP IS EMPTY. This means there might be zero objects in the relationship, which might be less than `minCount`. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter maxCount: the maximum number of objects in this relationship. If the number of objects in the relationship do not satisfy `minCount` and `maxCount`, the transaction's commit (or auto-commit) would fail with a validation error.
- parameter inverse: the inverse relationship that is declared for the destination object. All relationships require an "inverse", so updates to to this object's relationship are also reflected on its destination object.
- parameter deleteRule: defines what happens to relationship when an object is deleted. Valid values are `.nullify`, `.cascade`, and `.delete`. Defaults to `.nullify`.
- 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 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<D>(
_ keyPath: KeyPathString,
minCount: Int = 0,
maxCount: Int = 0,
inverse: KeyPath<V.DestinationObjectType, FieldContainer<V.DestinationObjectType>.Relationship<D>>,
deleteRule: DeleteRule = .nullify,
versionHashModifier: @autoclosure @escaping () -> String? = nil,
previousVersionKeyPath: @autoclosure @escaping () -> String? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []
) where D: FieldRelationshipType {
self.init(
keyPath: keyPath,
isToMany: true,
isOrdered: false,
deleteRule: deleteRule,
inverseKeyPath: { V.DestinationObjectType.meta[keyPath: inverse].keyPath },
versionHashModifier: versionHashModifier,
renamingIdentifier: previousVersionKeyPath,
affectedByKeyPaths: affectedByKeyPaths,
minCount: minCount,
maxCount: maxCount
)
}
}

416
Sources/Field.Stored.swift Normal file
View File

@@ -0,0 +1,416 @@
//
// Field.Stored.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: - FieldContainer
extension FieldContainer {
// MARK: - Stored
/**
The containing type for stored property values. Any type that conforms to `FieldStorableType` are supported.
```
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("nickname")
var nickname: String?
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Stored(...) var` syntax will be ignored.
*/
@propertyWrapper
public struct Stored<V: FieldStorableType>: AttributeKeyPathStringConvertible, FieldAttributeProtocol {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V,
_ 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> = []
) {
self.init(
wrappedValue: initial,
keyPath: keyPath,
isOptional: false,
versionHashModifier: versionHashModifier,
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
)
}
// MARK: @propertyWrapper
@available(*, unavailable)
public var wrappedValue: V {
get { fatalError() }
set { fatalError() }
}
public var projectedValue: Self {
return self
}
public static subscript(
_enclosingInstance instance: O,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<O, V>,
storage storageKeyPath: ReferenceWritableKeyPath<O, Self>
) -> V {
get {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V
}
set {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue)
}
}
// MARK: AnyKeyPathStringConvertible
public var cs_keyPathString: String {
return self.keyPath
}
// MARK: KeyPathStringConvertible
public typealias ObjectType = O
public typealias DestinationValueType = V
// MARK: AttributeKeyPathStringConvertible
public typealias ReturnValueType = DestinationValueType
// MARK: PropertyProtocol
internal let keyPath: KeyPathString
// MARK: FieldProtocol
internal static var dynamicObjectType: CoreStoreObject.Type {
return ObjectType.self
}
internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
let field = field as! Self
if let customGetter = field.customGetter {
return customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field)
)
}
let keyPath = field.keyPath
switch rawObject.value(forKey: keyPath) {
case let rawValue as V.FieldStoredNativeType:
return V.cs_fromFieldStoredNativeType(rawValue)
default:
return nil
}
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
Internals.assert(
rawObject.isEditableInContext() == true,
"Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction."
)
let newValue = newValue as! V
let field = field as! Self
let keyPath = field.keyPath
if let customSetter = field.customSetter {
return customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field),
newValue
)
}
return rawObject.setValue(
newValue.cs_toFieldStoredNativeType(),
forKey: keyPath
)
}
// MARK: FieldAttributeProtocol
internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues
internal var getter: CoreStoreManagedObject.CustomGetter? {
guard let customGetter = self.customGetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
let value = customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self)
)
return value.cs_toFieldStoredNativeType()
}
}
internal var setter: CoreStoreManagedObject.CustomSetter? {
guard let customSetter = self.customSetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any, _ newValue: Any?) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.willChangeValue(forKey: keyPath)
defer {
rawObject.didChangeValue(forKey: keyPath)
}
customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self),
V.cs_fromFieldStoredNativeType(newValue as! V.FieldStoredNativeType)
)
}
}
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: (() -> 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)?,
dynamicInitialValue: (() -> V)?,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
self.keyPath = keyPath
self.entityDescriptionValues = {
(
attributeType: V.cs_rawAttributeType,
isOptional: isOptional,
isTransient: false,
allowsExternalBinaryDataStorage: false,
versionHashModifier: versionHashModifier(),
renamingIdentifier: renamingIdentifier(),
valueTransformer: nil,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: initial?().cs_toFieldStoredNativeType()
)
}
self.customGetter = customGetter
self.customSetter = customSetter
self.dynamicInitialValue = dynamicInitialValue
}
// MARK: Private
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)?
}
}
// MARK: - FieldContainer.Stored where V: FieldOptionalType
extension FieldContainer.Stored where V: FieldOptionalType {
/**
Initializes the metadata for the property.
```
class Person: CoreStoreObject {
@Field.Stored("nickname")
var nickname: String?
}
```
- parameter initial: the initial value for the property when the object is first created.
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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(
wrappedValue initial: @autoclosure @escaping () -> V = nil,
_ 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> = []
) {
self.init(
wrappedValue: initial,
keyPath: keyPath,
isOptional: true,
versionHashModifier: versionHashModifier,
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
)
}
}

356
Sources/Field.Virtual.swift Normal file
View File

@@ -0,0 +1,356 @@
//
// Field.Virtual.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: - FieldContainer
extension FieldContainer {
// MARK: - Virtual
/**
The containing type for computed property values. Because this value is not persisted to the backing store, any type is supported. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types.
```
class Animal: CoreStoreObject {
@Field.Virtual(
"pluralName",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var pluralName: String
@Field.Stored("species")
var species: String = ""
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.Virtual(...) var` syntax will be ignored.
*/
@propertyWrapper
public struct Virtual<V>: AttributeKeyPathStringConvertible, FieldAttributeProtocol {
/**
Initializes the metadata for the property. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types.
```
class Person: CoreStoreObject {
@Field.Virtual(
"pluralName",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var pluralName: String
@Field.Stored("species")
var species: String = ""
}
```
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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 (_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? = nil,
affectedByKeyPaths: @autoclosure @escaping () -> Set<KeyPathString> = []) {
self.init(
keyPath: keyPath,
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
/**
Overload for compiler error message only
*/
@available(*, unavailable, message: "Field.Virtual properties are not allowed to have initial values, including `nil`.")
public init(
wrappedValue initial: @autoclosure @escaping () -> V,
_ keyPath: KeyPathString,
versionHashModifier: @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> = []) {
fatalError()
}
// MARK: @propertyWrapper
@available(*, unavailable)
public var wrappedValue: V {
get { fatalError() }
set { fatalError() }
}
public var projectedValue: Self {
return self
}
public static subscript(
_enclosingInstance instance: O,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<O, V>,
storage storageKeyPath: ReferenceWritableKeyPath<O, Self>
) -> V {
get {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.read(field: instance[keyPath: storageKeyPath], for: instance.rawObject!) as! V
}
set {
Internals.assert(
instance.rawObject != nil,
"Attempted to access values from a \(Internals.typeName(O.self)) meta object. Meta objects are only used for querying keyPaths and infering types."
)
Internals.assert(
instance.rawObject?.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
return self.modify(field: instance[keyPath: storageKeyPath], for: instance.rawObject!, newValue: newValue)
}
}
// MARK: AnyKeyPathStringConvertible
public var cs_keyPathString: String {
return self.keyPath
}
// MARK: KeyPathStringConvertible
public typealias ObjectType = O
public typealias DestinationValueType = V
// MARK: AttributeKeyPathStringConvertible
public typealias ReturnValueType = DestinationValueType
// MARK: PropertyProtocol
internal let keyPath: KeyPathString
// MARK: FieldProtocol
internal static var dynamicObjectType: CoreStoreObject.Type {
return ObjectType.self
}
internal static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any? {
let field = field as! Self
if let customGetter = field.customGetter {
return customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field)
)
}
let keyPath = field.keyPath
switch rawObject.value(forKey: keyPath) {
case let rawValue as V:
return rawValue
default:
return nil
}
}
internal static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?) {
Internals.assert(
rawObject.isEditableInContext() == true,
"Attempted to update a \(Internals.typeName(O.self))'s value from outside a transaction."
)
let newValue = newValue as! V
let field = field as! Self
let keyPath = field.keyPath
if let customSetter = field.customSetter {
return customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: field),
newValue
)
}
return rawObject.setValue(newValue, forKey: keyPath)
}
// MARK: FieldAttributeProtocol
internal let entityDescriptionValues: () -> FieldAttributeProtocol.EntityDescriptionValues
internal var getter: CoreStoreManagedObject.CustomGetter? {
guard let customGetter = self.customGetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any) -> Any? in
let rawObject = id as! CoreStoreManagedObject
rawObject.willAccessValue(forKey: keyPath)
defer {
rawObject.didAccessValue(forKey: keyPath)
}
return customGetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self)
)
}
}
internal var setter: CoreStoreManagedObject.CustomSetter? {
guard let customSetter = self.customSetter else {
return nil
}
let keyPath = self.keyPath
return { (_ id: Any, _ newValue: Any?) -> Void in
let rawObject = id as! CoreStoreManagedObject
rawObject.willChangeValue(forKey: keyPath)
defer {
rawObject.didChangeValue(forKey: keyPath)
}
return customSetter(
ObjectProxy<O>(rawObject),
ObjectProxy<O>.FieldProxy<V>(rawObject: rawObject, field: self),
newValue as! V
)
}
}
let initializer: CoreStoreManagedObject.CustomInitializer? = nil
// MARK: FilePrivate
fileprivate init(
keyPath: KeyPathString,
customGetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>) -> V)?,
customSetter: ((_ object: ObjectProxy<O>, _ field: ObjectProxy<O>.FieldProxy<V>, _ newValue: V) -> Void)? ,
affectedByKeyPaths: @escaping () -> Set<KeyPathString>) {
self.keyPath = keyPath
self.entityDescriptionValues = {
(
attributeType: .undefinedAttributeType,
isOptional: true,
isTransient: true,
allowsExternalBinaryDataStorage: false,
versionHashModifier: nil,
renamingIdentifier: nil,
valueTransformer: nil,
affectedByKeyPaths: affectedByKeyPaths(),
defaultValue: nil
)
}
self.customGetter = customGetter
self.customSetter = customSetter
}
// MARK: Private
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)?
}
}
// MARK: - FieldContainer.Virtual where V: FieldOptionalType
extension FieldContainer.Virtual where V: FieldOptionalType {
/**
Initializes the metadata for the property. `Field.Virtual` properties are not allowed to have initial values, including `nil` for optional types.
```
class Person: CoreStoreObject {
@Field.Virtual(
"pluralName",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var pluralName: String?
@Field.Stored("species")
var species: String = ""
}
```
- 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 `ObjectProxy<O>`, which acts as a type-safe proxy for the receiver. When accessing the property value from `ObjectProxy<O>`, make sure to use `field.primitiveValue` instead of `field.value`, which would unintentionally execute the same closure again recursively. Do not make assumptions on the thread/queue that the closure is executed on; accessors may be called from `NSError` logs for example.
- parameter customSetter: use this closure as an "override" for the default property setter. The closure receives a `ObjectProxy<O>`, 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<O>`, make sure to use `field.primitiveValue` instead of `field.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: ((_ 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> = []) {
self.init(
keyPath: keyPath,
customGetter: customGetter,
customSetter: customSetter,
affectedByKeyPaths: affectedByKeyPaths
)
}
}

91
Sources/Field.swift Normal file
View File

@@ -0,0 +1,91 @@
//
// Field.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: - DynamicObject
extension DynamicObject where Self: CoreStoreObject {
/**
The containing type for value propertiess.
```
class Pet: CoreStoreObject {
@Field.Stored("species")
var species = ""
@Field.Stored("nickname")
var nickname: String?
@Field.Coded("color", coder: FieldCoders.Plist.self)
var eyeColor: UIColor?
@Field.Relationship("owner", inverse: \.$pets)
var owner: Person?
@Field.Relationship("children")
var children: Array<Pet>
@Field.Relationship("parents", inverse: \.$children)
var parents: Set<Pet>
}
```
- Important: `Field` properties are required to be used as `@propertyWrapper`s. Any other declaration not using the `@Field.*(...) var` syntax will result in undefined behavior.
*/
public typealias Field = FieldContainer<Self>
}
// MARK: - FieldContainer
/**
The containing type for value properties. Use the `Field` typealias instead for shorter syntax.
```
class Pet: CoreStoreObject {
@Field.Stored("species")
var species = ""
@Field.Stored("nickname")
var nickname: String?
@Field.Coded("color", coder: FieldCoders.Plist.self)
var eyeColor: UIColor?
@Field.Relationship("owner", inverse: \.$pets)
var owner: Person?
@Field.Relationship("children")
var children: Array<Pet>
@Field.Relationship("parents", inverse: \.$children)
var parents: Set<Pet>
}
```
*/
public enum FieldContainer<O: CoreStoreObject> {}

View File

@@ -0,0 +1,52 @@
//
// FieldAttributeProtocol.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 Foundation
import CoreData
// MARK: - FieldAttributeProtocol
internal protocol FieldAttributeProtocol: FieldProtocol {
typealias EntityDescriptionValues = (
attributeType: NSAttributeType,
isOptional: Bool,
isTransient: Bool,
allowsExternalBinaryDataStorage: Bool,
versionHashModifier: String?,
renamingIdentifier: String?,
valueTransformer: Internals.AnyFieldCoder?,
affectedByKeyPaths: Set<KeyPathString>,
defaultValue: Any?
)
static var dynamicObjectType: CoreStoreObject.Type { get }
var entityDescriptionValues: () -> EntityDescriptionValues { get }
var getter: CoreStoreManagedObject.CustomGetter? { get }
var setter: CoreStoreManagedObject.CustomSetter? { get }
var initializer: CoreStoreManagedObject.CustomInitializer? { get }
}

View File

@@ -0,0 +1,58 @@
//
// FieldCoderType.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 Foundation
import CoreData
// MARK: - FieldCoderType
/**
Types that implement encoding to and decoding from `Data` to be used in `Field.Coded` properties' `coder:` argument.
```
class Person: CoreStoreObject {
@Field.Coded("profile", coder: FieldCoders.Json.self)
var profile: Profile = .init()
}
```
*/
public protocol FieldCoderType {
/**
The type to encode to and decode from `Data`
*/
associatedtype FieldStoredValue
/**
Encodes the value to `Data`
*/
static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data?
/**
Decodes the value from `Data`
*/
static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue?
}

View File

@@ -0,0 +1,95 @@
//
// FieldCoders.DefaultNSSecureCoding.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 Foundation
// MARK: - FieldCoders
extension FieldCoders {
/**
A `FieldCoderType` that implements the default Core Data transformable attribute behavior, which uses a `ValueTransformer` named `.secureUnarchiveFromDataTransformerName`.
*/
public struct DefaultNSSecureCoding<T: DefaultNSSecureCodable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = T
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
return ValueTransformer(forName: self.transformerName)?.reverseTransformedValue(fieldValue) as? Data
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
return ValueTransformer(forName: self.transformerName)?.transformedValue(data) as! FieldStoredValue?
}
// MARK: Internal
internal static var transformerName: NSValueTransformerName {
if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) {
return .secureUnarchiveFromDataTransformerName
}
else {
return .keyedUnarchiveFromDataTransformerName
}
}
}
}
// MARK: - DefaultNSSecureCodable
/**
Types that are supported by `FieldCoders.DefaultNSSecureCoding`
*/
public protocol DefaultNSSecureCodable: NSObject, NSSecureCoding {}
extension NSArray: DefaultNSSecureCodable {}
extension NSDictionary: DefaultNSSecureCodable {}
extension NSSet: DefaultNSSecureCodable {}
extension NSString: DefaultNSSecureCodable {}
extension NSNumber: DefaultNSSecureCodable {}
extension NSDate: DefaultNSSecureCodable {}
extension NSData: DefaultNSSecureCodable {}
extension NSURL: DefaultNSSecureCodable {}
extension NSUUID: DefaultNSSecureCodable {}
extension NSNull: DefaultNSSecureCodable {}

View File

@@ -0,0 +1,63 @@
//
// FieldCoders.Json.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 Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - Json
/**
A `FieldCoderType` that implements JSON encoding and decoding of `Codable` values.
- Important: Due to restrictions of `JSONEncoder`, the value will be contained in single-item array before encoding.
*/
public struct Json<V: Codable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
return try! JSONEncoder().encode([fieldValue])
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
return try! JSONDecoder().decode([FieldStoredValue].self, from: data).first
}
}
}

View File

@@ -0,0 +1,94 @@
//
// FieldCoders.NSCoding.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 Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - NSCoding
/**
A `FieldCoderType` that implements encoding and decoding of `NSCoding` values
*/
public struct NSCoding<V: Foundation.NSObject & Foundation.NSCoding>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) {
return try! NSKeyedArchiver.archivedData(
withRootObject: fieldValue,
requiringSecureCoding: self.requiresSecureCoding
)
}
else {
return NSKeyedArchiver.archivedData(withRootObject: fieldValue)
}
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
if #available(iOS 11.0, macOS 10.13, watchOS 4.0, tvOS 11.0, *) {
return try! NSKeyedUnarchiver.unarchivedObject(ofClass: FieldStoredValue.self, from: data)
}
else {
return NSKeyedUnarchiver.unarchiveObject(with: data) as! FieldStoredValue?
}
}
// MARK: Private
private static var requiresSecureCoding: Bool {
switch FieldStoredValue.self {
case let valueType as NSSecureCoding.Type:
return valueType.supportsSecureCoding
default:
return false
}
}
}
}

View File

@@ -0,0 +1,63 @@
//
// FieldCoders.Plist.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 Foundation
// MARK: - FieldCoders
extension FieldCoders {
// MARK: - Plist
/**
A `FieldCoderType` that implements Binary-Plist encoding and decoding of `Codable` values.
- Important: Due to restrictions of `JSONEncoder`, the value will be contained in single-item array before encoding.
*/
public struct Plist<V: Codable>: FieldCoderType {
// MARK: FieldCoderType
public typealias FieldStoredValue = V
public static func encodeToStoredData(_ fieldValue: FieldStoredValue?) -> Data? {
guard let fieldValue = fieldValue else {
return nil
}
return try! PropertyListEncoder().encode([fieldValue])
}
public static func decodeFromStoredData(_ data: Data?) -> FieldStoredValue? {
guard let data = data else {
return nil
}
return try! PropertyListDecoder().decode([FieldStoredValue].self, from: data).first
}
}
}

30
Sources/FieldCoders.swift Normal file
View File

@@ -0,0 +1,30 @@
//
// FieldCoders.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.
//
/**
Namespace for Built-in Field Coders
*/
public enum FieldCoders {}

View File

@@ -0,0 +1,59 @@
//
// FieldOptionalType.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 Foundation
// MARK: - FieldOptionalType
/**
Optional values to be used for `Field` properties.
*/
public protocol FieldOptionalType: ExpressibleByNilLiteral {
/**
The type for the wrapped value
*/
associatedtype Wrapped
/**
Used internally by CoreStore. Do not call directly.
*/
var cs_wrappedValue: Wrapped? { get }
}
// MARK: - Optional: FieldOptionalType
extension Optional: FieldOptionalType {
// MARK: FieldOptionalType
@inlinable
public var cs_wrappedValue: Wrapped? {
return self
}
}

View File

@@ -0,0 +1,38 @@
//
// FieldProtocol.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 Foundation
import CoreData
// MARK: - FieldProtocol
internal protocol FieldProtocol: PropertyProtocol {
static var dynamicObjectType: CoreStoreObject.Type { get }
static func read(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any?
static func modify(field: FieldProtocol, for rawObject: CoreStoreManagedObject, newValue: Any?)
}

View File

@@ -0,0 +1,49 @@
//
// FieldRelationshipProtocol.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 Foundation
import CoreData
// MARK: - FieldRelationshipProtocol
internal protocol FieldRelationshipProtocol: FieldProtocol {
typealias EntityDescriptionValues = (
isToMany: Bool,
isOrdered: Bool,
deleteRule: NSDeleteRule,
inverse: (type: CoreStoreObject.Type, KeyPathString?),
versionHashModifier: String?,
renamingIdentifier: String?,
affectedByKeyPaths: Set<KeyPathString>,
minCount: Int,
maxCount: Int
)
var entityDescriptionValues: () -> EntityDescriptionValues { get }
static func valueForSnapshot(field: FieldProtocol, for rawObject: CoreStoreManagedObject) -> Any?
}

View File

@@ -0,0 +1,262 @@
//
// FieldStorableType.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
import CoreGraphics
// MARK: - FieldStorableType
/**
Values to be used for `Field.Stored` properties.
*/
public protocol FieldStorableType {
/**
The `NSAttributeType` for this type
*/
associatedtype FieldStoredNativeType
/**
The `NSAttributeType` for this type. Used internally by CoreStore. Do not call directly.
*/
static var cs_rawAttributeType: NSAttributeType { get }
/**
Creates an instance of this type from raw native value. Used internally by CoreStore. Do not call directly.
*/
@inline(__always)
static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self
/**
Creates `FieldStoredNativeType` value from this instance. Used internally by CoreStore. Do not call directly.
*/
@inline(__always)
func cs_toFieldStoredNativeType() -> Any?
}
// MARK: - FieldStorableType where Self: ImportableAttributeType, FieldStoredNativeType == QueryableNativeType
extension FieldStorableType where Self: ImportableAttributeType, FieldStoredNativeType == QueryableNativeType {
@inline(__always)
public static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
@inline(__always)
public func cs_toFieldStoredNativeType() -> Any? {
return self.cs_toQueryableNativeType()
}
}
// MARK: - Bool
extension Bool: FieldStorableType {}
// MARK: - CGFloat
extension CGFloat: FieldStorableType {}
// MARK: - Data
extension Data: FieldStorableType {}
// MARK: - Date
extension Date: FieldStorableType {}
// MARK: - Double
extension Double: FieldStorableType {}
// MARK: - Float
extension Float: FieldStorableType {}
// MARK: - Int
extension Int: FieldStorableType {}
// MARK: - Int8
extension Int8: FieldStorableType {}
// MARK: - Int16
extension Int16: FieldStorableType {}
// MARK: - Int32
extension Int32: FieldStorableType {}
// MARK: - Int64
extension Int64: FieldStorableType {}
// MARK: - NSData
extension NSData: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - NSDate
extension NSDate: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - NSNumber
extension NSNumber: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - NSString
extension NSString: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - NSURL
extension NSURL: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - NSUUID
extension NSUUID: FieldStorableType {
@nonobjc @inline(__always)
public class func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
return self.cs_fromQueryableNativeType(value)!
}
}
// MARK: - String
extension String: FieldStorableType {}
// MARK: - URL
extension URL: FieldStorableType {}
// MARK: - UUID
extension UUID: FieldStorableType {}
// MARK: - Optional<FieldStorableType>
extension Optional: FieldStorableType where Wrapped: FieldStorableType {
// MARK: FieldStorableType
public typealias FieldStoredNativeType = Wrapped.FieldStoredNativeType?
public static var cs_rawAttributeType: NSAttributeType {
return Wrapped.cs_rawAttributeType
}
@inline(__always)
public static func cs_fromFieldStoredNativeType(_ value: FieldStoredNativeType) -> Self {
switch value {
case nil,
is NSNull:
return nil
case let value?:
return Wrapped.cs_fromFieldStoredNativeType(value)
}
}
@inline(__always)
public func cs_toFieldStoredNativeType() -> Any? {
switch self {
case nil,
is NSNull:
return nil
case let value?:
return value.cs_toFieldStoredNativeType()
}
}
}

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

@@ -0,0 +1,222 @@
//
// Internals.AnyFieldCoder.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 Foundation
// MARK: - Internals
extension Internals {
// MARK: - AnyFieldCoder
internal struct AnyFieldCoder {
// MARK: Internal
internal let transformerName: NSValueTransformerName
internal let transformer: Foundation.ValueTransformer
internal let encodeToStoredData: (_ fieldValue: Any?) -> Data?
internal let decodeFromStoredData: (_ data: Data?) -> Any?
internal init<Coder: FieldCoderType>(_ fieldCoder: Coder.Type) {
let transformer = CustomValueTransformer(fieldCoder: fieldCoder)
self.transformerName = transformer.id
self.transformer = transformer
self.encodeToStoredData = {
switch $0 {
case let value as Coder.FieldStoredValue:
return fieldCoder.encodeToStoredData(value)
case let valueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
return fieldCoder.encodeToStoredData(valueBox.value as! Coder.FieldStoredValue?)
default:
return fieldCoder.encodeToStoredData(nil)
}
}
self.decodeFromStoredData = { fieldCoder.decodeFromStoredData($0) }
}
internal init<V>(tag: UUID, encode: @escaping (V) -> Data?, decode: @escaping (Data?) -> V) {
let transformer = CustomValueTransformer(tag: tag)
self.transformerName = transformer.id
self.transformer = transformer
self.encodeToStoredData = { encode($0 as! V) }
self.decodeFromStoredData = { decode($0) }
}
internal func register() {
let transformerName = self.transformerName
if #available(iOS 12.0, tvOS 12.0, watchOS 5.0, macOS 10.14, *) {
if transformerName == .secureUnarchiveFromDataTransformerName {
return
}
}
switch transformerName {
case .keyedUnarchiveFromDataTransformerName,
.unarchiveFromDataTransformerName,
.isNotNilTransformerName,
.isNilTransformerName,
.negateBooleanTransformerName:
return
case let transformerName:
Self.cachedCoders[transformerName] = self
Foundation.ValueTransformer.setValueTransformer(
self.transformer,
forName: transformerName
)
}
}
// MARK: FilePrivate
fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:]
// MARK: - TransformableDefaultValueCodingBox
@objc(_CoreStore_Internals_TransformableDefaultValueCodingBox)
internal final class TransformableDefaultValueCodingBox: NSObject, NSSecureCoding {
// MARK: Internal
@objc
internal dynamic let transformerName: String
@objc
internal dynamic let data: Data
@nonobjc
internal let value: Any?
internal init?(defaultValue: Any?, fieldCoder: Internals.AnyFieldCoder?) {
guard
let fieldCoder = fieldCoder,
let defaultValue = defaultValue,
!(defaultValue is NSNull),
let data = fieldCoder.encodeToStoredData(defaultValue)
else {
return nil
}
self.transformerName = fieldCoder.transformerName.rawValue
self.value = defaultValue
self.data = data
}
// MARK: NSSecureCoding
@objc
dynamic class var supportsSecureCoding: Bool {
return true
}
// MARK: NSCoding
@objc
dynamic required init?(coder aDecoder: NSCoder) {
guard
case let transformerName as String = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName)),
let transformer = ValueTransformer(forName: .init(transformerName)),
case let data as Data = aDecoder.decodeObject(forKey: #keyPath(TransformableDefaultValueCodingBox.data)),
let value = transformer.reverseTransformedValue(data)
else {
return nil
}
self.transformerName = transformerName
self.data = data
self.value = value
}
@objc
dynamic func encode(with coder: NSCoder) {
coder.encode(self.data, forKey: #keyPath(TransformableDefaultValueCodingBox.data))
coder.encode(self.transformerName, forKey: #keyPath(TransformableDefaultValueCodingBox.transformerName))
}
}
}
// MARK: - CustomValueTransformer
fileprivate final class CustomValueTransformer: ValueTransformer {
// MARK: FilePrivate
fileprivate let id: NSValueTransformerName
fileprivate init<Coder: FieldCoderType>(fieldCoder: Coder.Type) {
self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(String(reflecting: fieldCoder))>.transformerName")
}
fileprivate init(tag: UUID) {
self.id = .init(rawValue: "CoreStore.FieldCoders.CustomValueTransformer<\(tag.uuidString)>.transformerName")
}
// MARK: ValueTransformer
override class func transformedValueClass() -> AnyClass {
return NSData.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
return AnyFieldCoder.cachedCoders[self.id]?.encodeToStoredData(value) as Data?
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
return AnyFieldCoder.cachedCoders[self.id]?.decodeFromStoredData(value as! Data?)
}
}
}

View File

@@ -47,9 +47,13 @@ extension Internals {
// MARK: Internal
init(sections: [NSFetchedResultsSectionInfo]) {
init(sections: [NSFetchedResultsSectionInfo], fetchOffset: Int, fetchLimit: Int) {
self.structure = .init(sections: sections)
self.structure = .init(
sections: sections,
fetchOffset: Swift.max(0, fetchOffset),
fetchLimit: (fetchLimit > 0) ? fetchLimit : nil
)
}
var sections: [Section] {
@@ -92,6 +96,11 @@ extension Internals {
return self.structure.allItemIDs
}
var updatedItemIdentifiers: Set<NSManagedObjectID> {
return self.structure.reloadedItems
}
func numberOfItems(inSection identifier: String) -> Int {
return self.itemIdentifiers(inSection: identifier).count
@@ -117,22 +126,22 @@ extension Internals {
return self.structure.allSectionIDs.firstIndex(of: identifier)
}
mutating func appendItems(_ identifiers: [NSManagedObjectID], toSection sectionIdentifier: String?) {
mutating func appendItems<C: Collection>(_ identifiers: C, toSection sectionIdentifier: String?) where C.Element == NSManagedObjectID {
self.structure.append(itemIDs: identifiers, to: sectionIdentifier)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], beforeItem beforeIdentifier: NSManagedObjectID) {
mutating func insertItems<C: Collection>(_ identifiers: C, beforeItem beforeIdentifier: NSManagedObjectID) where C.Element == NSManagedObjectID {
self.structure.insert(itemIDs: identifiers, before: beforeIdentifier)
}
mutating func insertItems(_ identifiers: [NSManagedObjectID], afterItem afterIdentifier: NSManagedObjectID) {
mutating func insertItems<C: Collection>(_ identifiers: C, afterItem afterIdentifier: NSManagedObjectID) where C.Element == NSManagedObjectID {
self.structure.insert(itemIDs: identifiers, after: afterIdentifier)
}
mutating func deleteItems(_ identifiers: [NSManagedObjectID]) {
mutating func deleteItems<S: Sequence>(_ identifiers: S) where S.Element == NSManagedObjectID {
self.structure.remove(itemIDs: identifiers)
}
@@ -152,27 +161,27 @@ extension Internals {
self.structure.move(itemID: identifier, after: toIdentifier)
}
mutating func reloadItems(_ identifiers: [NSManagedObjectID]) {
mutating func reloadItems<S: Sequence>(_ identifiers: S) where S.Element == NSManagedObjectID {
self.structure.update(itemIDs: identifiers)
}
mutating func appendSections(_ identifiers: [String]) {
mutating func appendSections<C: Collection>(_ identifiers: C) where C.Element == String {
self.structure.append(sectionIDs: identifiers)
}
mutating func insertSections(_ identifiers: [String], beforeSection toIdentifier: String) {
mutating func insertSections<C: Collection>(_ identifiers: C, beforeSection toIdentifier: String) where C.Element == String {
self.structure.insert(sectionIDs: identifiers, before: toIdentifier)
}
mutating func insertSections(_ identifiers: [String], afterSection toIdentifier: String) {
mutating func insertSections<C: Collection>(_ identifiers: C, afterSection toIdentifier: String) where C.Element == String {
self.structure.insert(sectionIDs: identifiers, after: toIdentifier)
}
mutating func deleteSections(_ identifiers: [String]) {
mutating func deleteSections<S: Sequence>(_ identifiers: S) where S.Element == String {
self.structure.remove(sectionIDs: identifiers)
}
@@ -187,7 +196,7 @@ extension Internals {
self.structure.move(sectionID: identifier, after: toIdentifier)
}
mutating func reloadSections(_ identifiers: [String]) {
mutating func reloadSections<S: Sequence>(_ identifiers: S) where S.Element == String {
self.structure.update(sectionIDs: identifiers)
}
@@ -268,23 +277,54 @@ extension Internals {
// MARK: Internal
var sections: [Section]
private(set) var reloadedItems: Set<NSManagedObjectID>
init() {
self.sections = []
self.reloadedItems = []
}
init(sections: [NSFetchedResultsSectionInfo]) {
init(sections: [NSFetchedResultsSectionInfo], fetchOffset: Int, fetchLimit: Int?) {
self.sections = sections.map {
let sliceItems: (_ array: [Any], _ offset: Int) -> Array<Any>.SubSequence
if let fetchLimit = fetchLimit {
Section(
differenceIdentifier: $0.name,
items: $0.objects?
.compactMap({ ($0 as? NSManagedObject)?.objectID })
.map({ Item(differenceIdentifier: $0) }) ?? []
var remainingCount = fetchLimit
sliceItems = {
let slice = $0[$1...].prefix(remainingCount)
remainingCount -= slice.count
return slice
}
}
else {
sliceItems = { $0[$1...] }
}
var newSections: [Internals.DiffableDataSourceSnapshot.Section] = []
var ignoreCount = fetchOffset
for section in sections {
let objects = section.objects ?? []
guard objects.indices.contains(ignoreCount) else {
ignoreCount -= objects.count
continue
}
let items = sliceItems(objects, ignoreCount)
.map({ Item(differenceIdentifier: ($0 as! NSManagedObject).objectID) })
ignoreCount = 0
guard !items.isEmpty else {
continue
}
newSections.append(
Section(differenceIdentifier: section.name, items: items)
)
}
self.sections = newSections
self.reloadedItems = []
}
var allSectionIDs: [String] {
@@ -308,10 +348,10 @@ extension Internals {
func section(containing itemID: NSManagedObjectID) -> String? {
return self.itemPositionMap()[itemID]?.section.differenceIdentifier
return self.itemPositionMap(itemID)?.section.differenceIdentifier
}
mutating func append(itemIDs: [NSManagedObjectID], to sectionID: String?) {
mutating func append<C: Collection>(itemIDs: C, to sectionID: String?) where C.Element == NSManagedObjectID {
let index: Array<Section>.Index
if let sectionID = sectionID {
@@ -335,9 +375,9 @@ extension Internals {
self.sections[index].elements.append(contentsOf: items)
}
mutating func insert(itemIDs: [NSManagedObjectID], before beforeItemID: NSManagedObjectID) {
mutating func insert<C: Collection>(itemIDs: C, before beforeItemID: NSManagedObjectID) where C.Element == NSManagedObjectID {
guard let itemPosition = self.itemPositionMap()[beforeItemID] else {
guard let itemPosition = self.itemPositionMap(beforeItemID) else {
Internals.abort("Item \(beforeItemID) does not exist")
}
@@ -346,9 +386,9 @@ extension Internals {
.insert(contentsOf: items, at: itemPosition.itemRelativeIndex)
}
mutating func insert(itemIDs: [NSManagedObjectID], after afterItemID: NSManagedObjectID) {
mutating func insert<C: Collection>(itemIDs: C, after afterItemID: NSManagedObjectID) where C.Element == NSManagedObjectID {
guard let itemPosition = self.itemPositionMap()[afterItemID] else {
guard let itemPosition = self.itemPositionMap(afterItemID) else {
Internals.abort("Item \(afterItemID) does not exist")
}
@@ -359,7 +399,7 @@ extension Internals {
.insert(contentsOf: items, at: itemIndex)
}
mutating func remove(itemIDs: [NSManagedObjectID]) {
mutating func remove<S: Sequence>(itemIDs: S) where S.Element == NSManagedObjectID {
let itemPositionMap = self.itemPositionMap()
var removeIndexSetMap: [Int: IndexSet] = [:]
@@ -390,13 +430,18 @@ extension Internals {
}
}
mutating func removeAllEmptySections() {
self.sections.removeAll(where: { $0.elements.isEmpty })
}
mutating func move(itemID: NSManagedObjectID, before beforeItemID: NSManagedObjectID) {
guard let removed = self.remove(itemID: itemID) else {
Internals.abort("Item \(itemID) does not exist")
}
guard let itemPosition = self.itemPositionMap()[beforeItemID] else {
guard let itemPosition = self.itemPositionMap(beforeItemID) else {
Internals.abort("Item \(beforeItemID) does not exist")
}
@@ -410,7 +455,7 @@ extension Internals {
Internals.abort("Item \(itemID) does not exist")
}
guard let itemPosition = self.itemPositionMap()[afterItemID] else {
guard let itemPosition = self.itemPositionMap(afterItemID) else {
Internals.abort("Item \(afterItemID) does not exist")
}
@@ -423,6 +468,7 @@ extension Internals {
mutating func update<S: Sequence>(itemIDs: S) where S.Element == NSManagedObjectID {
let itemPositionMap = self.itemPositionMap()
var newItemIDs: Set<NSManagedObjectID> = []
for itemID in itemIDs {
guard let itemPosition = itemPositionMap[itemID] else {
@@ -431,16 +477,18 @@ extension Internals {
}
self.sections[itemPosition.sectionIndex]
.elements[itemPosition.itemRelativeIndex].isReloaded = true
newItemIDs.insert(itemID)
}
self.reloadedItems.formUnion(newItemIDs)
}
mutating func append(sectionIDs: [String]) {
mutating func append<C: Collection>(sectionIDs: C) where C.Element == String {
let newSections = sectionIDs.lazy.map({ Section(differenceIdentifier: $0) })
self.sections.append(contentsOf: newSections)
}
mutating func insert(sectionIDs: [String], before beforeSectionID: String) {
mutating func insert<C: Collection>(sectionIDs: C, before beforeSectionID: String) where C.Element == String {
guard let sectionIndex = self.sectionIndex(of: beforeSectionID) else {
@@ -450,7 +498,7 @@ extension Internals {
self.sections.insert(contentsOf: newSections, at: sectionIndex)
}
mutating func insert(sectionIDs: [String], after afterSectionID: String) {
mutating func insert<C: Collection>(sectionIDs: C, after afterSectionID: String) where C.Element == String {
guard let beforeIndex = self.sectionIndex(of: afterSectionID) else {
@@ -461,7 +509,7 @@ extension Internals {
self.sections.insert(contentsOf: newSections, at: sectionIndex)
}
mutating func remove(sectionIDs: [String]) {
mutating func remove<S: Sequence>(sectionIDs: S) where S.Element == String {
for sectionID in sectionIDs {
@@ -519,7 +567,7 @@ extension Internals {
@discardableResult
private mutating func remove(itemID: NSManagedObjectID) -> Item? {
guard let itemPosition = self.itemPositionMap()[itemID] else {
guard let itemPosition = self.itemPositionMap(itemID) else {
return nil
}
@@ -537,6 +585,28 @@ extension Internals {
return self.sections.remove(at: sectionIndex)
}
private func itemPositionMap(_ itemID: NSManagedObjectID) -> ItemPosition? {
let sections = self.sections
for (sectionIndex, section) in sections.enumerated() {
for (itemRelativeIndex, item) in section.elements.enumerated() {
guard item.differenceIdentifier == itemID else {
continue
}
return ItemPosition(
item: item,
itemRelativeIndex: itemRelativeIndex,
section: section,
sectionIndex: sectionIndex
)
}
}
return nil
}
private func itemPositionMap() -> [NSManagedObjectID: ItemPosition] {
return self.sections.enumerated().reduce(into: [:]) { result, section in

View File

@@ -51,8 +51,38 @@ extension Internals {
self.dataStack = dataStack
}
func purge<Target: DiffableDataSource.Target>(
target: Target?,
animatingDifferences: Bool,
performUpdates: @escaping (
Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void,
completion: @escaping () -> Void
) {
self.apply(
.init(),
target: target,
animatingDifferences: animatingDifferences,
performUpdates: performUpdates,
completion: completion
)
}
func apply<View: AnyObject>(_ snapshot: DiffableDataSourceSnapshot, view: View?, animatingDifferences: Bool, performUpdates: @escaping (View, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, @escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void) -> Void) {
func apply<Target: DiffableDataSource.Target>(
_ snapshot: DiffableDataSourceSnapshot,
target: Target?,
animatingDifferences: Bool,
performUpdates: @escaping (
Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void,
completion: @escaping () -> Void
) {
self.dispatcher.dispatch { [weak self] in
@@ -64,36 +94,42 @@ extension Internals {
self.currentSnapshot = snapshot
let newSections = snapshot.sections
guard let view = view else {
guard let target = target else {
return self.sections = newSections
self.sections = newSections
return
}
let performDiffingUpdates: () -> Void = {
let changeset = StagedChangeset(source: self.sections, target: newSections)
performUpdates(view, changeset) { sections in
performUpdates(target, changeset) { sections in
self.sections = sections
}
}
#if canImport(QuartzCore)
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
if !animatingDifferences {
CATransaction.begin()
CATransaction.setDisableActions(true)
performDiffingUpdates()
CATransaction.commit()
return
}
#endif
performDiffingUpdates()
CATransaction.commit()
#else
performDiffingUpdates()
completion()
#endif
}
}
@@ -104,14 +140,23 @@ extension Internals {
return snapshot
}
func sectionIdentifier(inSection section: Int) -> String? {
guard self.sections.indices.contains(section) else {
return nil
}
return self.sections[section].differenceIdentifier
}
func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
guard (0 ..< self.sections.endIndex) ~= indexPath.section else {
guard self.sections.indices.contains(indexPath.section) else {
return nil
}
let items = self.sections[indexPath.section].elements
guard (0 ..< items.endIndex) ~= indexPath.item else {
guard items.indices.contains(indexPath.item) else {
return nil
}
@@ -138,15 +183,14 @@ extension Internals {
return self.sections.count
}
func numberOfItems(inSection section: Int) -> Int {
func numberOfItems(inSection section: Int) -> Int? {
guard self.sections.indices.contains(section) else {
return nil
}
return self.sections[section].elements.count
}
func sectionIdentifier(inSection section: Int) -> String {
return self.sections[section].differenceIdentifier
}
// MARK: Private

View File

@@ -41,9 +41,6 @@ import AppKit
internal protocol FetchedDiffableDataSourceSnapshotHandler: AnyObject {
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
// func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, sectionIndexTitleForSectionName sectionName: String?) -> String?
@@ -80,15 +77,6 @@ extension Internals {
internal func initialFetch() {
// #if canImport(UIKit) || canImport(AppKit)
//
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
//
// return
// }
//
// #endif
guard let fetchedResultsController = self.fetchedResultsController else {
return
@@ -98,26 +86,14 @@ extension Internals {
// MARK: NSFetchedResultsControllerDelegate
// #if canImport(UIKit) || canImport(AppKit)
//
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
// @objc
// dynamic func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
//
// self.handler?.controller(
// controller,
// didChangeContentWith: snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
// )
// }
//
// #endif
@objc
dynamic func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
var snapshot = Internals.DiffableDataSourceSnapshot(
sections: controller.sections ?? []
sections: controller.sections ?? [],
fetchOffset: controller.fetchRequest.fetchOffset,
fetchLimit: controller.fetchRequest.fetchLimit
)
snapshot.reloadSections(self.reloadedSectionIDs)
snapshot.reloadItems(self.reloadedItemIDs)

View File

@@ -359,6 +359,42 @@ public func ~= <O: NSManagedObject, D: NSManagedObject, S: Sequence>(_ sequence:
}
// MARK: - KeyPath where Root: CoreStoreObject, Value: FieldContainer<Root>.Stored<QueryableAttributeType & Equatable>
/**
Creates a `Where` clause by comparing if a property is equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$nickname == "John"))
```
*/
public func == <O, V>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return Where<O>(keyPath, isEqualTo: value)
}
/**
Creates a `Where` clause by comparing if a property is not equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$nickname != "John"))
```
*/
public func != <O, V>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return !Where<O>(keyPath, isEqualTo: value)
}
/**
Creates a `Where` clause by checking if a sequence contains the value of a property
```
let dog = dataStack.fetchOne(From<Dog>().where(["Pluto", "Snoopy", "Scooby"] ~= \.nickname))
```
*/
public func ~= <O, V, S: Sequence>(_ sequence: S, _ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>) -> Where<O> where S.Iterator.Element == V {
return Where<O>(O.meta[keyPath: keyPath].keyPath, isMemberOf: sequence)
}
// MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer<Root>.Required<QueryableAttributeType & Equatable>
/**
@@ -433,6 +469,17 @@ public func ~= <O, V, S: Sequence>(_ sequence: S, _ keyPath: KeyPath<O, ValueCon
// MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer<Root>.Required<QueryableAttributeType & Comparable>
/**
Creates a `Where` clause by comparing if a property is less than a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age < 20))
```
*/
public func < <O, V: Comparable>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return Where<O>("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is less than a value
```
@@ -444,6 +491,17 @@ public func < <O, V: Comparable>(_ keyPath: KeyPath<O, ValueContainer<O>.Require
return Where<O>("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType())
}
/**
Creates a `Where` clause by comparing if a property is greater than a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age > 20))
```
*/
public func > <O, V: Comparable>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return Where<O>("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is greater than a value
```
@@ -455,6 +513,17 @@ public func > <O, V: Comparable>(_ keyPath: KeyPath<O, ValueContainer<O>.Require
return Where<O>("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType())
}
/**
Creates a `Where` clause by comparing if a property is less than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age <= 20))
```
*/
public func <= <O, V: Comparable>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return Where<O>("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is less than or equal to a value
```
@@ -466,6 +535,17 @@ public func <= <O, V: Comparable>(_ keyPath: KeyPath<O, ValueContainer<O>.Requir
return Where<O>("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toQueryableNativeType())
}
/**
Creates a `Where` clause by comparing if a property is greater than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age >= 20))
```
*/
public func >= <O, V: Comparable>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> {
return Where<O>("%K >= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is greater than or equal to a value
```
@@ -480,6 +560,17 @@ public func >= <O, V: Comparable>(_ keyPath: KeyPath<O, ValueContainer<O>.Requir
// MARK: - KeyPath where Root: CoreStoreObject, Value: ValueContainer<Root>.Optional<QueryableAttributeType & Comparable>
/**
Creates a `Where` clause by comparing if a property is less than a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age < 20))
```
*/
public func < <O, V: FieldOptionalType>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> where V.Wrapped: Comparable {
return Where<O>("%K < %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is less than a value
```
@@ -498,6 +589,17 @@ public func < <O, V>(_ keyPath: KeyPath<O, ValueContainer<O>.Optional<V>>, _ val
}
}
/**
Creates a `Where` clause by comparing if a property is greater than a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age > 20))
```
*/
public func > <O, V: FieldOptionalType>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> where V.Wrapped: Comparable {
return Where<O>("%K > %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is greater than a value
```
@@ -516,6 +618,17 @@ public func > <O, V>(_ keyPath: KeyPath<O, ValueContainer<O>.Optional<V>>, _ val
}
}
/**
Creates a `Where` clause by comparing if a property is less than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age <= 20))
```
*/
public func <= <O, V: FieldOptionalType>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> where V.Wrapped: Comparable {
return Where<O>("%K <= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is less than or equal to a value
```
@@ -534,6 +647,17 @@ public func <= <O, V>(_ keyPath: KeyPath<O, ValueContainer<O>.Optional<V>>, _ va
}
}
/**
Creates a `Where` clause by comparing if a property is greater than or equal to a value
```
let person = dataStack.fetchOne(From<Person>().where(\.$age >= 20))
```
*/
public func >= <O, V: FieldOptionalType>(_ keyPath: KeyPath<O, FieldContainer<O>.Stored<V>>, _ value: V) -> Where<O> where V.Wrapped: Comparable {
return Where<O>("%K >= %@", O.meta[keyPath: keyPath].keyPath, value.cs_toFieldStoredNativeType() as! V.FieldStoredNativeType)
}
/**
Creates a `Where` clause by comparing if a property is greater than or equal to a value
```
@@ -555,6 +679,17 @@ public func >= <O, V>(_ keyPath: KeyPath<O, ValueContainer<O>.Optional<V>>, _ va
// MARK: - KeyPath where Root: CoreStoreObject, Value: RelationshipContainer<Root>.ToOne<CoreStoreObject>
/**
Creates a `Where` clause by comparing if a property is equal to a value
```
let dog = dataStack.fetchOne(From<Dog>().where(\.$master == john))
```
*/
public func == <O, D: FieldRelationshipToOneType>(_ keyPath: KeyPath<O, FieldContainer<O>.Relationship<D>>, _ object: D.DestinationObjectType?) -> Where<O> {
return Where<O>(O.meta[keyPath: keyPath].keyPath, isEqualTo: object)
}
/**
Creates a `Where` clause by comparing if a property is equal to a value
```
@@ -577,6 +712,17 @@ public func == <O, D>(_ keyPath: KeyPath<O, RelationshipContainer<O>.ToOne<D>>,
return Where<O>(O.meta[keyPath: keyPath].keyPath, isEqualTo: object)
}
/**
Creates a `Where` clause by comparing if a property is not equal to a value
```
let dog = dataStack.fetchOne(From<Dog>().where(\.$master != john))
```
*/
public func != <O, D: FieldRelationshipToOneType>(_ keyPath: KeyPath<O, FieldContainer<O>.Relationship<D>>, _ object: D.DestinationObjectType?) -> Where<O> {
return !Where<O>(O.meta[keyPath: keyPath].keyPath, isEqualTo: object)
}
/**
Creates a `Where` clause by comparing if a property is not equal to a value
```
@@ -599,6 +745,17 @@ public func != <O, D>(_ keyPath: KeyPath<O, RelationshipContainer<O>.ToOne<D>>,
return !Where<O>(O.meta[keyPath: keyPath].keyPath, isEqualTo: object)
}
/**
Creates a `Where` clause by checking if a sequence contains a value of a property
```
let dog = dataStack.fetchOne(From<Dog>().where([john, bob, joe] ~= \.$master))
```
*/
public func ~= <O, D: FieldRelationshipToOneType, S: Sequence>(_ sequence: S, _ keyPath: KeyPath<O, FieldContainer<O>.Relationship<D>>) -> Where<O> where S.Iterator.Element == D.DestinationObjectType {
return Where<O>(O.meta[keyPath: keyPath].keyPath, isMemberOf: sequence)
}
/**
Creates a `Where` clause by checking if a sequence contains a value of a property
```

View File

@@ -106,47 +106,47 @@ extension Int64: AllowedObjectiveCKeyPathValue {
extension NSData: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSData
}
extension NSDate: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSDate
}
extension NSManagedObject: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSManagedObject
}
extension NSNumber: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSNumber
}
extension NSString: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSString
}
extension NSSet: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSSet
}
extension NSOrderedSet: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSOrderedSet
}
extension NSURL: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSURL
}
extension NSUUID: AllowedOptionalObjectiveCKeyPathValue {
public typealias DestinationValueType = Self
public typealias DestinationValueType = NSUUID
}
extension String: AllowedOptionalObjectiveCKeyPathValue {
@@ -240,32 +240,32 @@ extension Int64: AllowedObjectiveCAttributeKeyPathValue {
extension NSData: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSData
}
extension NSDate: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSDate
}
extension NSNumber: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSNumber
}
extension NSString: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSString
}
extension NSURL: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSURL
}
extension NSUUID: AllowedObjectiveCAttributeKeyPathValue {
public typealias ReturnValueType = Self
public typealias ReturnValueType = NSUUID
}
extension String: AllowedObjectiveCAttributeKeyPathValue {

View File

@@ -62,7 +62,7 @@ import SwiftUI
.orderBy(.ascending(\.lastName))
)
```
All access to the `ListPublisher` items should be done via its `snapshot` value, which is a `struct` of type `ListSnapshot<O>`. `ListSnapshot`s are also designed to work well with `DiffableDataSource.TableView`s and `DiffableDataSource.CollectionView`s. For detailed examples, refer to the documentation for `DiffableDataSource.TableView` and `DiffableDataSource.CollectionView`.
All access to the `ListPublisher` items should be done via its `snapshot` value, which is a `struct` of type `ListSnapshot<O>`. `ListSnapshot`s are also designed to work well with `DiffableDataSource.TableViewAdapter`s and `DiffableDataSource.CollectionViewAdapter`s. For detailed examples, refer to the documentation for `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter`.
*/
@available(macOS 10.12, *)
public final class ListPublisher<O: DynamicObject>: Hashable {
@@ -397,15 +397,6 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
extension ListPublisher: FetchedDiffableDataSourceSnapshotHandler {
// MARK: FetchedDiffableDataSourceSnapshotHandler
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
// internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>) {
//
// self.snapshot = .init(
// diffableSnapshot: snapshot,
// context: controller.managedObjectContext
// )
// }
internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: Internals.DiffableDataSourceSnapshot) {

View File

@@ -37,7 +37,7 @@ import AppKit
// MARK: - ListSnapshot
/**
A `ListSnapshot` holds a stable list of `DynamicObject` identifiers. This is typically created by a `ListPublisher` and are designed to work well with `DiffableDataSource.TableView`s and `DiffableDataSource.CollectionView`s. For detailed examples, see the documentation on `DiffableDataSource.TableView` and `DiffableDataSource.CollectionView`.
A `ListSnapshot` holds a stable list of `DynamicObject` identifiers. This is typically created by a `ListPublisher` and are designed to work well with `DiffableDataSource.TableViewAdapter`s and `DiffableDataSource.CollectionViewAdapter`s. For detailed examples, see the documentation on `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter`.
While the `ListSnapshot` stores only object identifiers, all accessors to its items return `ObjectPublisher`s, which are lazily created. For more details, see the documentation on `ListObject`.
@@ -83,7 +83,10 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
*/
public subscript(safeIndex index: Index) -> ObjectPublisher<O>? {
let context = self.context!
guard let context = self.context else {
return nil
}
let itemIDs = self.diffableSnapshot.itemIdentifiers
guard itemIDs.indices.contains(index) else {
@@ -118,7 +121,10 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
*/
public subscript(safeSectionIndex sectionIndex: Int, safeItemIndex itemIndex: Int) -> ObjectPublisher<O>? {
let context = self.context!
guard let context = self.context else {
return nil
}
let snapshot = self.diffableSnapshot
let sectionIDs = snapshot.sectionIdentifiers
guard sectionIDs.indices.contains(sectionIndex) else {
@@ -214,6 +220,14 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return snapshot.numberOfItems(inSection: sectionID) > 0
}
/**
Returns item identifiers for updated objects. This is mainly useful for Data Source adapters such as `UICollectionViewDiffableDataSource` or `UITableViewDiffableDataSource` which work on collection diffs when reloading. Since objects with same IDs resolve as "equal" in their old and new states, adapters may need extra heuristics to determine which row items need reloading. If your row items are all observing changes from each corresponding `ObjectPublisher`, or if you are using CoreStore's built-in `DiffableDataSource`s, there is no need to inspect this property.
*/
public var updatedItemIdentifiers: Set<NSManagedObjectID> {
return self.diffableSnapshot.updatedItemIdentifiers
}
/**
The number of items in all sections in the `ListSnapshot`
*/
@@ -340,7 +354,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return indices.map { position in
let itemID = itemIDs[position]
return ObjectPublisher<O>(objectID: itemID, context: context)
return context.objectPublisher(objectID: itemID)
}
}
@@ -354,10 +368,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
let context = self.context!
let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID)
return itemIDs.map {
return ObjectPublisher<O>(objectID: $0, context: context)
}
return itemIDs.map(context.objectPublisher(objectID:))
}
/**
@@ -374,7 +385,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return itemIndices.map { position in
let itemID = itemIDs[position]
return ObjectPublisher<O>(objectID: itemID, context: context)
return context.objectPublisher(objectID: itemID)
}
}
@@ -391,7 +402,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return indices.lazy.map { position in
let itemID = itemIDs[position]
return ObjectPublisher<O>(objectID: itemID, context: context)
return context.objectPublisher(objectID: itemID)
}
}
@@ -405,10 +416,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
let context = self.context!
let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID)
return itemIDs.lazy.map {
return ObjectPublisher<O>(objectID: $0, context: context)
}
return itemIDs.lazy.map(context.objectPublisher(objectID:))
}
/**
@@ -425,7 +433,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
return itemIndices.lazy.map { position in
let itemID = itemIDs[position]
return ObjectPublisher<O>(objectID: itemID, context: context)
return context.objectPublisher(objectID: itemID)
}
}
@@ -438,7 +446,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter itemIDs: the object identifiers for the objects to append
- parameter sectionID: the section to append the items to
*/
public mutating func appendItems(withIDs itemIDs: [ItemID], toSectionWithID sectionID: SectionID? = nil) {
public mutating func appendItems<C: Collection>(withIDs itemIDs: C, toSectionWithID sectionID: SectionID? = nil) where C.Element == ItemID {
self.diffableSnapshot.appendItems(itemIDs, toSection: sectionID)
}
@@ -449,7 +457,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter itemIDs: the object identifiers for the objects to insert
- parameter beforeItemID: an existing identifier to insert items before of. Specifying an invalid value will raise an exception.
*/
public mutating func insertItems(withIDs itemIDs: [ItemID], beforeItemID: ItemID) {
public mutating func insertItems<C: Collection>(withIDs itemIDs: C, beforeItemID: ItemID) where C.Element == ItemID {
self.diffableSnapshot.insertItems(itemIDs, beforeItem: beforeItemID)
}
@@ -460,7 +468,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter itemIDs: the object identifiers for the objects to insert
- parameter beforeItemID: an existing identifier to insert items after of. Specifying an invalid value will raise an exception.
*/
public mutating func insertItems(withIDs itemIDs: [ItemID], afterItemID: ItemID) {
public mutating func insertItems<C: Collection>(withIDs itemIDs: C, afterItemID: ItemID) where C.Element == ItemID {
self.diffableSnapshot.insertItems(itemIDs, afterItem: afterItemID)
}
@@ -470,7 +478,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter itemIDs: the object identifiers for the objects to delete
*/
public mutating func deleteItems(withIDs itemIDs: [ItemID]) {
public mutating func deleteItems<S: Sequence>(withIDs itemIDs: S) where S.Element == ItemID {
self.diffableSnapshot.deleteItems(itemIDs)
}
@@ -510,7 +518,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter itemIDs: the object identifiers to reload
*/
public mutating func reloadItems(withIDs itemIDs: [ItemID]) {
public mutating func reloadItems<S: Sequence>(withIDs itemIDs: S) where S.Element == ItemID {
self.diffableSnapshot.reloadItems(itemIDs)
}
@@ -520,7 +528,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter sectionIDs: the sections to append
*/
public mutating func appendSections(withIDs sectionIDs: [SectionID]) {
public mutating func appendSections<C: Collection>(withIDs sectionIDs: C) where C.Element == SectionID {
self.diffableSnapshot.appendSections(sectionIDs)
}
@@ -531,7 +539,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter sectionIDs: the section identifiers for the sections to insert
- parameter beforeSectionID: an existing identifier to insert items before of. Specifying an invalid value will raise an exception.
*/
public mutating func insertSections(withIDs sectionIDs: [SectionID], beforeSectionID: SectionID) {
public mutating func insertSections<C: Collection>(withIDs sectionIDs: C, beforeSectionID: SectionID) where C.Element == SectionID {
self.diffableSnapshot.insertSections(sectionIDs, beforeSection: beforeSectionID)
}
@@ -542,7 +550,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter sectionIDs: the section identifiers for the sections to insert
- parameter beforeSectionID: an existing identifier to insert items after of. Specifying an invalid value will raise an exception.
*/
public mutating func insertSections(withIDs sectionIDs: [SectionID], afterSectionID: SectionID) {
public mutating func insertSections<C: Collection>(withIDs sectionIDs: C, afterSectionID: SectionID) where C.Element == SectionID {
self.diffableSnapshot.insertSections(sectionIDs, afterSection: afterSectionID)
}
@@ -552,7 +560,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter sectionIDs: the section identifiers for the sections to delete
*/
public mutating func deleteSections(withIDs sectionIDs: [SectionID]) {
public mutating func deleteSections<S: Sequence>(withIDs sectionIDs: S) where S.Element == SectionID {
self.diffableSnapshot.deleteSections(sectionIDs)
}
@@ -584,7 +592,7 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
- parameter sectionIDs: the section identifiers to reload
*/
public mutating func reloadSections(withIDs sectionIDs: [SectionID]) {
public mutating func reloadSections<S: Sequence>(withIDs sectionIDs: S) where S.Element == SectionID {
self.diffableSnapshot.reloadSections(sectionIDs)
}
@@ -629,28 +637,14 @@ public struct ListSnapshot<O: DynamicObject>: RandomAccessCollection, Hashable {
// MARK: Internal
internal private(set) var diffableSnapshot: DiffableDataSourceSnapshotProtocol
internal private(set) var diffableSnapshot: Internals.DiffableDataSourceSnapshot
internal init() {
// if #available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) {
//
// self.diffableSnapshot = NSDiffableDataSourceSnapshot<String, NSManagedObjectID>()
// }
// else {
self.diffableSnapshot = Internals.DiffableDataSourceSnapshot()
// }
self.diffableSnapshot = Internals.DiffableDataSourceSnapshot()
self.context = nil
}
// @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
// internal init(diffableSnapshot: NSDiffableDataSourceSnapshot<String, NSManagedObjectID>, context: NSManagedObjectContext) {
//
// self.diffableSnapshot = diffableSnapshot
// self.context = context
// }
internal init(diffableSnapshot: Internals.DiffableDataSourceSnapshot, context: NSManagedObjectContext) {
self.diffableSnapshot = diffableSnapshot

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

@@ -33,29 +33,49 @@ extension NSEntityDescription {
@nonobjc
internal func cs_resolveAttributeNames() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] {
return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.name] = (attribute.description, attribute.description.versionHash)
})
return self.attributesByName.reduce(
into: [:],
{ (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.name] = (attribute.description, attribute.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveAttributeRenamingIdentities() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] {
return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash)
})
return self.attributesByName.reduce(
into: [:],
{ (result, attribute: (name: String, description: NSAttributeDescription)) in
result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRelationshipNames() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] {
return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.name] = (relationship.description, relationship.description.versionHash)
})
return self.relationshipsByName.reduce(
into: [:],
{ (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.name] = (relationship.description, relationship.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRelationshipRenamingIdentities() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] {
return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash)
})
return self.relationshipsByName.reduce(
into: [:],
{ (result, relationship: (name: String, description: NSRelationshipDescription)) in
result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash)
}
)
}
}

View File

@@ -99,7 +99,7 @@ extension NSManagedObjectContext {
return objectPublisher
}
let objectPublisher = ObjectPublisher<O>(objectID: objectID, context: self)
let objectPublisher = ObjectPublisher<O>.createUncached(objectID: objectID, context: self)
cache.setObject(objectPublisher, forKey: objectID)
return objectPublisher
}
@@ -145,6 +145,12 @@ extension NSManagedObjectContext {
)
}
}
@nonobjc
internal func objectsDidChangeObserver<U: AnyObject>(remove: U) {
_ = self.userInfo(for: .objectsChangeObserver(U.self), initialize: { nil as Any? })
}
// MARK: Private
@@ -172,9 +178,9 @@ extension NSManagedObjectContext {
private func userInfo<T>(for key: UserInfoKeys, initialize: @escaping () -> T) -> T {
let keyString = key.keyString
if let value = self.userInfo[keyString] {
if let value = self.userInfo[keyString] as? T {
return value as! T
return value
}
let value = initialize()
self.userInfo[keyString] = value

View File

@@ -33,15 +33,25 @@ extension NSManagedObjectModel {
@nonobjc
internal func cs_resolveNames() -> [String: (entity: NSEntityDescription, versionHash: Data)] {
return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.name] = (entity.description, entity.description.versionHash)
})
return self.entitiesByName.reduce(
into: [:],
{ (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.name] = (entity.description, entity.description.versionHash)
}
)
}
@nonobjc
internal func cs_resolveRenamingIdentities() -> [String: (entity: NSEntityDescription, versionHash: Data)] {
return self.entitiesByName.reduce(into: [:], { (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash)
})
return self.entitiesByName.reduce(
into: [:],
{ (result, entity: (name: String, description: NSEntityDescription)) in
result[entity.description.renamingIdentifier ?? entity.name] = (entity.description, entity.description.versionHash)
}
)
}
}

235
Sources/ObjectProxy.swift Normal file
View File

@@ -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<O>`, 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<O>`, make sure to use `ObjectProxy<O>.$property.primitiveValue` instead of `ObjectProxy<O>.$property.value`, which would execute the same accessor again recursively.
*/
@dynamicMemberLookup
public struct ObjectProxy<O: CoreStoreObject> {
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Stored<V>>) -> FieldProxy<V> {
return .init(rawObject: self.rawObject, keyPath: member)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Virtual<V>>) -> FieldProxy<V> {
return .init(rawObject: self.rawObject, keyPath: member)
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Coded<V>>) -> FieldProxy<V> {
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<V> {
// MARK: Public
/**
Returns the value for the specified property from the objects 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<OBase>(rawObject: CoreStoreManagedObject, keyPath: KeyPath<O, FieldContainer<OBase>.Stored<V>>) {
self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath])
}
internal init<OBase>(rawObject: CoreStoreManagedObject, keyPath: KeyPath<O, FieldContainer<OBase>.Virtual<V>>) {
self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath])
}
internal init<OBase>(rawObject: CoreStoreManagedObject, keyPath: KeyPath<O, FieldContainer<OBase>.Coded<V>>) {
self.init(rawObject: rawObject, field: O.meta[keyPath: keyPath])
}
internal init<OBase>(rawObject: CoreStoreManagedObject, field: FieldContainer<OBase>.Stored<V>) {
let keyPathString = field.keyPath
self.getValue = {
return type(of: field).read(field: field, for: rawObject) as! V
}
self.setValue = {
type(of: field).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<OBase>(rawObject: CoreStoreManagedObject, field: FieldContainer<OBase>.Virtual<V>) {
let keyPathString = field.keyPath
self.getValue = {
return type(of: field).read(field: field, for: rawObject) as! V
}
self.setValue = {
type(of: field).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<OBase>(rawObject: CoreStoreManagedObject, field: FieldContainer<OBase>.Coded<V>) {
let keyPathString = field.keyPath
self.getValue = {
return type(of: field).read(field: field, for: rawObject) as! V
}
self.setValue = {
type(of: field).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
}
}

View File

@@ -95,6 +95,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
Internals.Closure(callback),
forKey: observer
)
_ = self.lazySnapshot
}
/**
@@ -130,7 +131,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
return self
}
return Self.init(objectID: self.id, context: context)
return context.objectPublisher(objectID: self.id)
}
public func asReadOnly(in dataStack: DataStack) -> O? {
@@ -184,9 +185,9 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
// MARK: Internal
internal convenience init(objectID: O.ObjectID, context: NSManagedObjectContext) {
internal static func createUncached(objectID: O.ObjectID, context: NSManagedObjectContext) -> ObjectPublisher<O> {
self.init(
return self.init(
objectID: objectID,
context: context,
initializer: ObjectSnapshot<O>.init(objectID:context:)
@@ -195,6 +196,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
deinit {
self.context.objectsDidChangeObserver(remove: self)
self.observers.removeAllObjects()
}
@@ -221,30 +223,36 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
self.rawObjectWillChange = nil
}
self.$lazySnapshot.initialize({ initializer(objectID, context) })
context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (updatedIDs, deletedIDs) in
self.$lazySnapshot.initialize { [weak self] in
guard let self = self else {
return
return initializer(objectID, context)
}
if deletedIDs.contains(objectID) {
context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (updatedIDs, deletedIDs) in
self.object = nil
guard let self = self else {
self.willChange()
self.$lazySnapshot.reset({ nil })
self.didChange()
self.notifyObservers()
}
else if updatedIDs.contains(objectID) {
self.willChange()
self.$lazySnapshot.reset({ initializer(objectID, context) })
self.didChange()
self.notifyObservers()
return
}
if deletedIDs.contains(objectID) {
self.object = nil
self.willChange()
self.$lazySnapshot.reset({ nil })
self.didChange()
self.notifyObservers()
}
else if updatedIDs.contains(objectID) {
self.willChange()
self.$lazySnapshot.reset({ initializer(objectID, context) })
self.didChange()
self.notifyObservers()
}
}
return initializer(objectID, context)
}
}
@@ -329,7 +337,6 @@ extension ObjectPublisher {
// MARK: - ObjectPublisher where O: NSManagedObject
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
extension ObjectPublisher where O: NSManagedObject {
// MARK: Public
@@ -337,11 +344,21 @@ extension ObjectPublisher where O: NSManagedObject {
/**
Returns the value for the property identified by a given key.
*/
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
public subscript<V: AllowedObjectiveCKeyPathValue>(dynamicMember member: KeyPath<O, V>) -> V {
fatalError()
// return self.snapshot[dynamicMember: member]
}
/**
Returns the value for the property identified by a given key.
*/
public func value<V: AllowedObjectiveCKeyPathValue>(forKeyPath keyPath: KeyPath<O, V>) -> V! {
let key = String(keyPath: keyPath)
return self.snapshot?.dictionaryForValues()[key] as! V?
}
}
@@ -351,6 +368,87 @@ extension ObjectPublisher where O: CoreStoreObject {
// MARK: Public
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Stored<V>>) -> V? {
guard
let object = self.object,
let rawObject = object.rawObject
else {
return nil
}
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = object[keyPath: member]
return type(of: field).read(field: field, for: rawObject) as! V?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Virtual<V>>) -> V? {
guard
let object = self.object,
let rawObject = object.rawObject
else {
return nil
}
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = object[keyPath: member]
return type(of: field).read(field: field, for: rawObject) as! V?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Coded<V>>) -> V? {
guard
let object = self.object,
let rawObject = object.rawObject
else {
return nil
}
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = object[keyPath: member]
return type(of: field).read(field: field, for: rawObject) as! V?
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Relationship<V>>) -> V.PublishedType? {
guard
let object = self.object,
let rawObject = object.rawObject
else {
return nil
}
Internals.assert(
rawObject.isRunningInAllowedQueue() == true,
"Attempted to access \(Internals.typeName(O.self))'s value outside it's designated queue."
)
let field = object[keyPath: member]
let snapshotValue = V.cs_valueForSnapshot(from: rawObject.objectIDs(forRelationshipNamed: field.keyPath))
return V.cs_toPublishedType(from: snapshotValue, in: self.context)
}
/**
Returns the value for the property identified by a given key.
*/

View File

@@ -75,6 +75,29 @@ extension CoreStoreObject: ObjectRepresentation {}
extension DynamicObject where Self: ObjectRepresentation {
// MARK: Public
/**
An `ObjectPublisher` wrapper for the exact same object
*/
public func asPublisher() -> ObjectPublisher<Self>? {
return self.cs_toRaw()
.managedObjectContext
.map({ $0.objectPublisher(objectID: self.cs_id()) })
}
/**
A thread-safe `struct` that is a full-copy of the object's properties
*/
public func asSnapshot() -> ObjectSnapshot<Self>? {
return self.cs_toRaw()
.managedObjectContext
.flatMap({ ObjectSnapshot<Self>(objectID: self.cs_id(), context: $0) })
}
// MARK: ObjectRepresentation
public func objectID() -> Self.ObjectID {
@@ -85,7 +108,7 @@ extension DynamicObject where Self: ObjectRepresentation {
public func asPublisher(in dataStack: DataStack) -> ObjectPublisher<Self> {
let context = dataStack.unsafeContext()
return ObjectPublisher<Self>(objectID: self.cs_id(), context: context)
return context.objectPublisher(objectID: self.cs_id())
}
public func asReadOnly(in dataStack: DataStack) -> Self? {

View File

@@ -41,6 +41,14 @@ import AppKit
*/
@dynamicMemberLookup
public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
// MARK: Public
public func dictionaryForValues() -> [String: Any] {
return self.values
}
// MARK: ObjectRepresentation
@@ -54,7 +62,7 @@ public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
public func asPublisher(in dataStack: DataStack) -> ObjectPublisher<O> {
let context = dataStack.unsafeContext()
return ObjectPublisher<O>(objectID: self.id, context: context)
return context.objectPublisher(objectID: self.id)
}
public func asReadOnly(in dataStack: DataStack) -> O? {
@@ -107,14 +115,20 @@ public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
return nil
}
self.id = objectID
self.context = context
self.values = values
}
// MARK: FilePrivate
fileprivate var values: [String: Any]
// MARK: Private
private let id: O.ObjectID
private var values: [String: Any]
private let context: NSManagedObjectContext
private var valuesRef: NSDictionary {
@@ -125,17 +139,35 @@ public struct ObjectSnapshot<O: DynamicObject>: ObjectRepresentation, Hashable {
// MARK: - ObjectSnapshot where O: NSManagedObject
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
extension ObjectSnapshot where O: NSManagedObject {
/**
Returns the value for the property identified by a given key.
*/
@available(*, unavailable, message: "KeyPaths accessed from @dynamicMemberLookup types can't generate KVC keys yet (https://bugs.swift.org/browse/SR-11351)")
public subscript<V: AllowedObjectiveCKeyPathValue>(dynamicMember member: KeyPath<O, V>) -> V {
let key = String(keyPath: member)
return self.values[key] as! V
}
/**
Returns the value for the property identified by a given key.
*/
public func value<V: AllowedObjectiveCKeyPathValue>(forKeyPath keyPath: KeyPath<O, V>) -> V! {
let key = String(keyPath: keyPath)
return self.values[key] as! V?
}
/**
Mutates the value for the property identified by a given key.
*/
public mutating func setValue<V: AllowedObjectiveCKeyPathValue>(_ value: V!, forKeyPath keyPath: KeyPath<O, V>) {
let key = String(keyPath: keyPath)
self.values[key] = value
}
}
@@ -143,6 +175,76 @@ extension ObjectSnapshot where O: NSManagedObject {
extension ObjectSnapshot where O: CoreStoreObject {
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Stored<V>>) -> V {
get {
let key = String(keyPath: member)
return self.values[key] as! V
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Virtual<V>>) -> V {
get {
let key = String(keyPath: member)
return self.values[key] as! V
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Coded<V>>) -> V {
get {
let key = String(keyPath: member)
return self.values[key] as! V
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, V>(dynamicMember member: KeyPath<O, FieldContainer<OBase>.Relationship<V>>) -> V.PublishedType {
get {
let key = String(keyPath: member)
let context = self.context
let snapshotValue = self.values[key] as! V.SnapshotValueType
return V.cs_toPublishedType(from: snapshotValue, in: context)
}
set {
let key = String(keyPath: member)
self.values[key] = V.cs_toSnapshotType(from: newValue)
}
}
/**
Returns the value for the property identified by a given key.
*/
@@ -168,7 +270,7 @@ extension ObjectSnapshot where O: CoreStoreObject {
get {
let key = String(keyPath: member)
return self.values[key] as! V?
return self.values[key] as? V
}
set {
@@ -202,7 +304,7 @@ extension ObjectSnapshot where O: CoreStoreObject {
get {
let key = String(keyPath: member)
return self.values[key] as! V?
return self.values[key] as? V
}
set {
@@ -214,51 +316,59 @@ extension ObjectSnapshot where O: CoreStoreObject {
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToOne<D>>) -> D.ObjectID? {
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToOne<D>>) -> ObjectPublisher<D>? {
get {
let key = String(keyPath: member)
return self.values[key] as! D.ObjectID?
guard let id = self.values[key] as? D.ObjectID else {
return nil
}
return self.context.objectPublisher(objectID: id)
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
self.values[key] = newValue?.objectID()
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToManyOrdered<D>>) -> [D.ObjectID] {
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToManyOrdered<D>>) -> [ObjectPublisher<D>] {
get {
let key = String(keyPath: member)
return self.values[key] as! [D.ObjectID]
let context = self.context
let ids = self.values[key] as! [D.ObjectID]
return ids.map(context.objectPublisher(objectID:))
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
self.values[key] = newValue.map({ $0.objectID() })
}
}
/**
Returns the value for the property identified by a given key.
*/
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToManyUnordered<D>>) -> Set<D.ObjectID> {
public subscript<OBase, D>(dynamicMember member: KeyPath<O, RelationshipContainer<OBase>.ToManyUnordered<D>>) -> Set<ObjectPublisher<D>> {
get {
let key = String(keyPath: member)
return self.values[key] as! Set<D.ObjectID>
let context = self.context
let ids = self.values[key] as! Set<D.ObjectID>
return Set(ids.map(context.objectPublisher(objectID:)))
}
set {
let key = String(keyPath: member)
self.values[key] = newValue
self.values[key] = Set(newValue.map({ $0.objectID() }))
}
}
}

Some files were not shown because too many files have changed in this diff Show More