From 1f562b25a75c2e222174ed8e5d63f228c453da7b Mon Sep 17 00:00:00 2001 From: John Estropia Date: Sun, 11 Apr 2021 11:03:17 +0900 Subject: [PATCH] Updated README --- CoreStore.xcodeproj/project.pbxproj | 40 +- .../⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift | 2 +- .../⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift | 4 +- .../⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift | 6 +- .../⭐️Modern.ColorsDemo.SwiftUI.ListView.swift | 6 +- .../⭐️Modern.PlacemarksDemo.MainView.swift | 2 +- .../⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift | 2 +- README.md | 591 ++++++++++++++++-- Sources/ForEach+SwiftUI.swift | 6 +- Sources/ListReader.swift | 14 +- Sources/{LiveList.swift => ListState.swift} | 40 +- Sources/ObjectReader.swift | 21 +- .../{LiveObject.swift => ObjectState.swift} | 10 +- 13 files changed, 616 insertions(+), 128 deletions(-) rename Sources/{LiveList.swift => ListState.swift} (93%) rename Sources/{LiveObject.swift => ObjectState.swift} (95%) diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 55e0184..59de5a0 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -725,14 +725,14 @@ B5BF7FCC234D80910070E741 /* Internals.LazyNonmutating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */; }; B5BF7FCD234D80910070E741 /* Internals.LazyNonmutating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */; }; B5BF7FCE234D80910070E741 /* Internals.LazyNonmutating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */; }; - B5C7958F25D7D18000BDACC1 /* LiveList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* LiveList.swift */; }; - B5C7959025D7D18000BDACC1 /* LiveList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* LiveList.swift */; }; - B5C7959125D7D18000BDACC1 /* LiveList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* LiveList.swift */; }; - B5C7959225D7D18000BDACC1 /* LiveList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* LiveList.swift */; }; - B5C7959425D7D18700BDACC1 /* LiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* LiveObject.swift */; }; - B5C7959525D7D18700BDACC1 /* LiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* LiveObject.swift */; }; - B5C7959625D7D18700BDACC1 /* LiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* LiveObject.swift */; }; - B5C7959725D7D18700BDACC1 /* LiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* LiveObject.swift */; }; + B5C7958F25D7D18000BDACC1 /* ListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* ListState.swift */; }; + B5C7959025D7D18000BDACC1 /* ListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* ListState.swift */; }; + B5C7959125D7D18000BDACC1 /* ListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* ListState.swift */; }; + B5C7959225D7D18000BDACC1 /* ListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7958E25D7D18000BDACC1 /* ListState.swift */; }; + B5C7959425D7D18700BDACC1 /* ObjectState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* ObjectState.swift */; }; + B5C7959525D7D18700BDACC1 /* ObjectState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* ObjectState.swift */; }; + B5C7959625D7D18700BDACC1 /* ObjectState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* ObjectState.swift */; }; + B5C7959725D7D18700BDACC1 /* ObjectState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959325D7D18700BDACC1 /* ObjectState.swift */; }; B5C7959925D7D8B300BDACC1 /* ListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959825D7D8B300BDACC1 /* ListReader.swift */; }; B5C7959A25D7D8B300BDACC1 /* ListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959825D7D8B300BDACC1 /* ListReader.swift */; }; B5C7959B25D7D8B300BDACC1 /* ListReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C7959825D7D8B300BDACC1 /* ListReader.swift */; }; @@ -1169,8 +1169,8 @@ B5BF7FC0234D7B2E0070E741 /* ObjectPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectPublisher.swift; sourceTree = ""; }; B5BF7FC5234D7E460070E741 /* ObjectSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectSnapshot.swift; sourceTree = ""; }; B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.LazyNonmutating.swift; sourceTree = ""; }; - B5C7958E25D7D18000BDACC1 /* LiveList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveList.swift; sourceTree = ""; }; - B5C7959325D7D18700BDACC1 /* LiveObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveObject.swift; sourceTree = ""; }; + B5C7958E25D7D18000BDACC1 /* ListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListState.swift; sourceTree = ""; }; + B5C7959325D7D18700BDACC1 /* ObjectState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectState.swift; sourceTree = ""; }; B5C7959825D7D8B300BDACC1 /* ListReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListReader.swift; sourceTree = ""; }; B5C795A025D7EB2200BDACC1 /* ForEach+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ForEach+SwiftUI.swift"; sourceTree = ""; }; B5C795C225DD651F00BDACC1 /* DataStack+Reactive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataStack+Reactive.swift"; sourceTree = ""; }; @@ -1745,8 +1745,8 @@ B5C7959D25D7E89B00BDACC1 /* PropertyWrappers */ = { isa = PBXGroup; children = ( - B5C7958E25D7D18000BDACC1 /* LiveList.swift */, - B5C7959325D7D18700BDACC1 /* LiveObject.swift */, + B5C7958E25D7D18000BDACC1 /* ListState.swift */, + B5C7959325D7D18700BDACC1 /* ObjectState.swift */, ); name = PropertyWrappers; sourceTree = ""; @@ -2421,7 +2421,7 @@ B5E84F2F1AFF849C0064E85B /* Internals.NotificationObserver.swift in Sources */, B5F1DA8D1B9AA97D007C5CBB /* ImportableObject.swift in Sources */, B56965241B356B820075EE4A /* MigrationResult.swift in Sources */, - B5C7958F25D7D18000BDACC1 /* LiveList.swift in Sources */, + B5C7958F25D7D18000BDACC1 /* ListState.swift in Sources */, B5FE4DAC1C85D44E00FA6A91 /* SQLiteStore.swift in Sources */, B501FDE71CA8D20500BE22EF /* CSListObserver.swift in Sources */, B5E41EC01EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, @@ -2494,7 +2494,7 @@ B5C795D225E0DD1B00BDACC1 /* ListSnapshot.SectionInfo.swift in Sources */, B51B5C2B22D43931009FA3BA /* String+KeyPaths.swift in Sources */, B512607F1E97A18000402229 /* CoreStoreObject+Convenience.swift in Sources */, - B5C7959425D7D18700BDACC1 /* LiveObject.swift in Sources */, + B5C7959425D7D18700BDACC1 /* ObjectState.swift in Sources */, B509D7CE23C8492800F42824 /* Relationship.ToManyUnordered.swift in Sources */, B5E84F301AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift in Sources */, B5D4A6B723A236DC00D7373F /* DiffableDataSource.BaseAdapter.swift in Sources */, @@ -2673,7 +2673,7 @@ B501FDE41CA8D1F500BE22EF /* CSListMonitor.swift in Sources */, B5A1DAC91F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, B5FE4DA31C8481E100FA6A91 /* StorageInterface.swift in Sources */, - B5C7959025D7D18000BDACC1 /* LiveList.swift in Sources */, + B5C7959025D7D18000BDACC1 /* ListState.swift in Sources */, B5ECDC131CA816E500C7F112 /* CSTweak.swift in Sources */, B56923C51EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, 82BA18C91C4BBD5900A0916E /* MigrationType.swift in Sources */, @@ -2743,7 +2743,7 @@ B546F96A1C9AF26D00D5AC55 /* CSInMemoryStore.swift in Sources */, B5277673234F1AEB0056BE9F /* NSManagedObjectContext+Logging.swift in Sources */, B509D7D923C84E2600F42824 /* Transformable.Optional.swift in Sources */, - B5C7959525D7D18700BDACC1 /* LiveObject.swift in Sources */, + B5C7959525D7D18700BDACC1 /* ObjectState.swift in Sources */, B5C795D325E0DD1B00BDACC1 /* ListSnapshot.SectionInfo.swift in Sources */, B5831B7B1F34ACBA00A9F647 /* Transformable.swift in Sources */, 82BA18A81C4BBD2900A0916E /* CoreStoreLogger.swift in Sources */, @@ -2926,7 +2926,7 @@ B52DD19D1BE1F92C00949AFE /* BaseDataTransaction.swift in Sources */, B5220E131D1305ED009BC71E /* SectionBy.swift in Sources */, B559CD4D1CAA8C6D00E4D58B /* CSStorageInterface.swift in Sources */, - B5C7959225D7D18000BDACC1 /* LiveList.swift in Sources */, + B5C7959225D7D18000BDACC1 /* ListState.swift in Sources */, B5E41EC31EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5ECDBE91CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, B5A1DACB1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, @@ -2999,7 +2999,7 @@ B52DD1BB1BE1F94000949AFE /* MigrationType.swift in Sources */, B5C795D525E0DD1B00BDACC1 /* ListSnapshot.SectionInfo.swift in Sources */, B509D7DB23C84E2600F42824 /* Transformable.Optional.swift in Sources */, - B5C7959725D7D18700BDACC1 /* LiveObject.swift in Sources */, + B5C7959725D7D18700BDACC1 /* ObjectState.swift in Sources */, B5831B7D1F34ACBA00A9F647 /* Transformable.swift in Sources */, B5277675234F1AEB0056BE9F /* NSManagedObjectContext+Logging.swift in Sources */, B52DD1C91BE1F94600949AFE /* NSManagedObjectContext+Transaction.swift in Sources */, @@ -3178,7 +3178,7 @@ B501FDE51CA8D1F500BE22EF /* CSListMonitor.swift in Sources */, B5E41EC21EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5ECDC141CA816E500C7F112 /* CSTweak.swift in Sources */, - B5C7959125D7D18000BDACC1 /* LiveList.swift in Sources */, + B5C7959125D7D18000BDACC1 /* ListState.swift in Sources */, B5A1DACA1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, B56321AE1BD6521C006C9394 /* Internals.NotificationObserver.swift in Sources */, B56321931BD65216006C9394 /* DataStack+Querying.swift in Sources */, @@ -3251,7 +3251,7 @@ B5277674234F1AEB0056BE9F /* NSManagedObjectContext+Logging.swift in Sources */, B509D7DA23C84E2600F42824 /* Transformable.Optional.swift in Sources */, B5C795D425E0DD1B00BDACC1 /* ListSnapshot.SectionInfo.swift in Sources */, - B5C7959625D7D18700BDACC1 /* LiveObject.swift in Sources */, + B5C7959625D7D18700BDACC1 /* ObjectState.swift in Sources */, B5831B7C1F34ACBA00A9F647 /* Transformable.swift in Sources */, B563218B1BD65216006C9394 /* UnsafeDataTransaction.swift in Sources */, B549F6601E569C7400FBAB2D /* QueryableAttributeType.swift in Sources */, diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift index 6f5eff2..5e78cc8 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -71,7 +71,7 @@ extension Modern.ColorsDemo { // MARK: Private - @LiveList(Modern.ColorsDemo.palettesPublisher) + @ListState(Modern.ColorsDemo.palettesPublisher) private var palettes: ListSnapshot private let listView: ( diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift index 7068502..b83e1b8 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.DetailView.swift @@ -15,9 +15,9 @@ extension Modern.ColorsDemo.SwiftUI { struct DetailView: View { /** - ⭐️ Sample 1: Using a `LiveObject` to observe object changes. Note that the `ObjectSnapshot` is always `Optional` + ⭐️ Sample 1: Using a `ObjectState` to observe object changes. Note that the `ObjectSnapshot` is always `Optional` */ - @LiveObject + @ObjectState private var palette: ObjectSnapshot? /** diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift index ea79711..496edd4 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ItemView.swift @@ -14,13 +14,13 @@ extension Modern.ColorsDemo.SwiftUI { struct ItemView: View { /** - ⭐️ Sample 1: Using a `LiveObject` to observe object changes. Note that the `ObjectSnapshot` is always `Optional` + ⭐️ Sample 1: Using a `ObjectState` to observe object changes. Note that the `ObjectSnapshot` is always `Optional` */ - @LiveObject + @ObjectState private var palette: ObjectSnapshot? /** - ⭐️ Sample 2: Initializing a `LiveObject` from an existing `ObjectPublisher` + ⭐️ Sample 2: Initializing a `ObjectState` from an existing `ObjectPublisher` */ internal init(_ palette: ObjectPublisher) { diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift index 17114bf..ab57c19 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️ColorsDemo/⭐️Modern.ColorsDemo.SwiftUI.ListView.swift @@ -14,13 +14,13 @@ extension Modern.ColorsDemo.SwiftUI { struct ListView: View { /** - ⭐️ Sample 1: Using a `LiveList` to observe list changes + ⭐️ Sample 1: Using a `ListState` to observe list changes */ - @LiveList + @ListState private var palettes: ListSnapshot /** - ⭐️ Sample 2: Initializing a `LiveList` from an existing `ListPublisher` + ⭐️ Sample 2: Initializing a `ListState` from an existing `ListPublisher` */ init( listPublisher: ListPublisher, diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift index f6a2c27..dcc896f 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PlacemarksDemo/⭐️Modern.PlacemarksDemo.MainView.swift @@ -71,7 +71,7 @@ extension Modern.PlacemarksDemo { // MARK: Internal - @LiveObject(Modern.PlacemarksDemo.placePublisher) + @ObjectState(Modern.PlacemarksDemo.placePublisher) var place: ObjectSnapshot? init() { diff --git a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift index 49082a0..c84f5d3 100644 --- a/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/⭐️Sources/⭐️Demos/⭐️Modern/⭐️PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -62,7 +62,7 @@ extension Modern.PokedexDemo { // MARK: Private - @LiveList( + @ListState( From() .orderBy(.ascending(\.$index)), in: Modern.PokedexDemo.dataStack diff --git a/README.md b/README.md index 94a9044..607ff62 100644 --- a/README.md +++ b/README.md @@ -24,38 +24,12 @@ Unleashing the real power of Core Data with the elegance and safety of Swift Upgrading from previous CoreStore versions? Check out the [🆕 features](#features) and make sure to read the [Change logs](https://github.com/JohnEstropia/CoreStore/releases). -CoreStore is now part of the [Swift Source Compatibility projects](https://swift.org/source-compatibility/#current-list-of-projects). - - -## Why use CoreStore? - -CoreStore was (and is) heavily shaped by real-world needs of developing data-dependent apps. It enforces safe and convenient Core Data usage while letting you take advantage of the industry's encouraged best practices. - -### Features - -- **🆕SwiftUI and Combine API utilities.** `ListPublisher`s and `ObjectPublisher`s now have their `@LiveList` and `@LiveObject` SwiftUI property wrappers. Combine `Publisher` s are also available through the `ListPublisher.reactive`, `ObjectPublisher.reactive`, and `DataStack.reactive` namespaces. -- **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 Swift’s 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))* -- **🔭Type-safe, easy to configure observers.** You don't have to deal with the burden of setting up `NSFetchedResultsController`s and KVO. As an added bonus, list and object observable types all support multiple observers. This means you can have multiple view controllers efficiently share a single resource! *(See [Observing changes and notifications](#observing-changes-and-notifications))* -- **📥Efficient importing utilities.** Map your entities once with their corresponding import source (JSON for example), and importing from *transactions* becomes elegant. Uniquing is also done with an efficient find-and-replace algorithm. *(See [Importing data](#importing-data))* -- **🗑Say goodbye to *.xcdatamodeld* files!** While CoreStore supports `NSManagedObject`s, it offers `CoreStoreObject` whose subclasses can declare type-safe properties all in Swift code without the need to maintain separate resource files for the models. As bonus, these special properties support custom types, and can be used to create type-safe keypaths and queries. *(See [Type-safe `CoreStoreObject`s](#type-safe-corestoreobjects))* -- **🔗Progressive migrations.** No need to think how to migrate from all previous model versions to your latest model. Just tell the `DataStack` the sequence of version strings (`MigrationChain`s) and CoreStore will automatically use progressive migrations when needed. *(See [Migrations](#migrations))* -- **Easier custom migrations.** Say goodbye to *.xcmappingmodel* files; CoreStore can now infer entity mappings when possible, while still allowing an easy way to write custom mappings. *(See [Migrations](#migrations))* -- **📝Plug-in your own logging framework.** Although a default logger is built-in, all logging, asserting, and error reporting can be funneled to `CoreStoreLogger` protocol implementations. *(See [Logging and error reporting](#logging-and-error-reporting))* -- **⛓Heavy support for multiple persistent stores per data stack.** CoreStore lets you manage separate stores in a single `DataStack`, just the way *.xcdatamodeld* configurations are designed to. CoreStore will also manage one stack by default, but you can create and manage as many as you need. *(See [Setting up](#setting-up))* -- **🎯Free to name entities and their class names independently.** CoreStore gets around a restriction with other Core Data wrappers where the entity name should be the same as the `NSManagedObject` subclass name. CoreStore loads entity-to-class mappings from the managed object model file, so you can assign independent names for the entities and their class names. -- **📙Full Documentation.** No magic here; all public classes, functions, properties, etc. have detailed *Apple Docs*. This *README* also introduces a lot of concepts and explains a lot of CoreStore's behavior. -- **ℹ️Informative (and pretty) logs.** All CoreStore and Core Data-related types now have very informative and pretty print outputs! *(See [Logging and error reporting](#logging-and-error-reporting))* -- **🎗Objective-C support!** Is your project transitioning from Objective-C to Swift but still can't quite fully convert some huge classes to Swift yet? CoreStore adjusts to the ever-increasing Swift adoption. While still written in pure Swift, all CoreStore types have their corresponding Objective-C-visible "bridging classes". *(See [Objective-C support](#objective-c-support))* -- **🛡More extensive Unit Tests.** Extending CoreStore is safe without having to worry about breaking old behavior. - -*Have ideas that may benefit other Core Data users? [Feature Request](https://github.com/JohnEstropia/CoreStore/issues)s are welcome!* +CoreStore is part of the [Swift Source Compatibility projects](https://swift.org/source-compatibility/#current-list-of-projects). ## Contents - [TL;DR (a.k.a. sample codes)](#tldr-aka-sample-codes) +- [Why use CoreStore?](#why-use-corestore) - [Architecture](#architecture) - CoreStore Tutorials (All of these have demos in the **Demo** app project!) - [Setting up](#setting-up) @@ -95,13 +69,28 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep - [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) + - [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) + - [Reactive Programming](#reactive-programming) + - [RxSwift](#rxswift) + - 🆕[Combine](#combine) + - 🆕[`DataStack.reactive`](#datastackreactive) + - 🆕[`ListPublisher.reactive`](#listpublisherreactive) + - 🆕[`ObjectPublisher.reactive`](#objectpublisherreactive) + - 🆕[SwiftUI Utilities](#swiftui-utilities) + - 🆕[SwiftUI Views`](#swiftui-views) + - 🆕[`ListReader`](#listreader) + - 🆕[`ObjectReader`](#objectreader) + - 🆕[SwiftUI Property Wrappers](#swiftui-property-wrappers) + - 🆕[`ListState`](#liststate) + - 🆕[`ObjectState`](#objectstate) + - 🆕[SwiftUI Extensions](#swiftui-extensions) + - 🆕[`ForEach`](#foreach) - [Roadmap](#roadmap) - [Installation](#installation) - [Changesets](#changesets) @@ -113,6 +102,18 @@ CoreStore was (and is) heavily shaped by real-world needs of developing data-dep ## TL;DR (a.k.a. sample codes) +Pure-Swift models: +```swift +class Person: CoreStoreObject { + @Field.Stored("name") + var name: String = "" + + @Field.Relationship("pets", inverse: \Dog.$master) + var pets: Set +} +``` +(Classic `NSManagedObject`s also supported) + Setting-up with progressive migration support: ```swift dataStack = DataStack( @@ -135,7 +136,7 @@ Starting transactions: ```swift dataStack.perform( asynchronous: { (transaction) -> Void in - let person = transaction.create(Into()) + let person = transaction.create(Into()) person.name = "John Smith" person.age = 42 }, @@ -150,13 +151,13 @@ dataStack.perform( Fetching objects (simple): ```swift -let people = try dataStack.fetchAll(From()) +let people = try dataStack.fetchAll(From()) ``` Fetching objects (complex): ```swift let people = try dataStack.fetchAll( - From() + From() .where(\.age > 30), .orderBy(.ascending(\.name), .descending(.\age)), .tweak({ $0.includesPendingChanges = false }) @@ -166,7 +167,7 @@ let people = try dataStack.fetchAll( Querying values: ```swift let maxAge = try dataStack.queryValue( - From() + From() .select(Int.self, .maximum(\.age)) ) ``` @@ -176,6 +177,33 @@ But really, there's a reason I wrote this huge *README*. Read up on the details! Check out the **Demo** app project for sample codes as well! +## Why use CoreStore? + +CoreStore was (and is) heavily shaped by real-world needs of developing data-dependent apps. It enforces safe and convenient Core Data usage while letting you take advantage of the industry's encouraged best practices. + +### Features + +- **🆕SwiftUI and Combine API utilities.** `ListPublisher`s and `ObjectPublisher`s now have their `@ListState` and `@ObjectState` SwiftUI property wrappers. Combine `Publisher` s are also available through the `ListPublisher.reactive`, `ObjectPublisher.reactive`, and `DataStack.reactive` namespaces. +- **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 Swift’s 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))* +- **🔭Type-safe, easy to configure observers.** You don't have to deal with the burden of setting up `NSFetchedResultsController`s and KVO. As an added bonus, list and object observable types all support multiple observers. This means you can have multiple view controllers efficiently share a single resource! *(See [Observing changes and notifications](#observing-changes-and-notifications))* +- **📥Efficient importing utilities.** Map your entities once with their corresponding import source (JSON for example), and importing from *transactions* becomes elegant. Uniquing is also done with an efficient find-and-replace algorithm. *(See [Importing data](#importing-data))* +- **🗑Say goodbye to *.xcdatamodeld* files!** While CoreStore supports `NSManagedObject`s, it offers `CoreStoreObject` whose subclasses can declare type-safe properties all in Swift code without the need to maintain separate resource files for the models. As bonus, these special properties support custom types, and can be used to create type-safe keypaths and queries. *(See [Type-safe `CoreStoreObject`s](#type-safe-corestoreobjects))* +- **🔗Progressive migrations.** No need to think how to migrate from all previous model versions to your latest model. Just tell the `DataStack` the sequence of version strings (`MigrationChain`s) and CoreStore will automatically use progressive migrations when needed. *(See [Migrations](#migrations))* +- **Easier custom migrations.** Say goodbye to *.xcmappingmodel* files; CoreStore can now infer entity mappings when possible, while still allowing an easy way to write custom mappings. *(See [Migrations](#migrations))* +- **📝Plug-in your own logging framework.** Although a default logger is built-in, all logging, asserting, and error reporting can be funneled to `CoreStoreLogger` protocol implementations. *(See [Logging and error reporting](#logging-and-error-reporting))* +- **⛓Heavy support for multiple persistent stores per data stack.** CoreStore lets you manage separate stores in a single `DataStack`, just the way *.xcdatamodeld* configurations are designed to. CoreStore will also manage one stack by default, but you can create and manage as many as you need. *(See [Setting up](#setting-up))* +- **🎯Free to name entities and their class names independently.** CoreStore gets around a restriction with other Core Data wrappers where the entity name should be the same as the `NSManagedObject` subclass name. CoreStore loads entity-to-class mappings from the managed object model file, so you can assign independent names for the entities and their class names. +- **📙Full Documentation.** No magic here; all public classes, functions, properties, etc. have detailed *Apple Docs*. This *README* also introduces a lot of concepts and explains a lot of CoreStore's behavior. +- **ℹ️Informative (and pretty) logs.** All CoreStore and Core Data-related types now have very informative and pretty print outputs! *(See [Logging and error reporting](#logging-and-error-reporting))* +- **🎗Objective-C support!** Is your project transitioning from Objective-C to Swift but still can't quite fully convert some huge classes to Swift yet? CoreStore adjusts to the ever-increasing Swift adoption. While still written in pure Swift, all CoreStore types have their corresponding Objective-C-visible "bridging classes". *(See [Objective-C support](#objective-c-support))* +- **🛡More extensive Unit Tests.** Extending CoreStore is safe without having to worry about breaking old behavior. + +*Have ideas that may benefit other Core Data users? [Feature Request](https://github.com/JohnEstropia/CoreStore/issues)s are welcome!* + + ## Architecture For maximum safety and performance, CoreStore will enforce coding patterns and practices it was designed for. (Don't worry, it's not as scary as it sounds.) But it is advisable to understand the "magic" of CoreStore before you use it in your apps. @@ -271,6 +299,19 @@ try dataStack.addStorageAndWait( ) ) ``` +Asynchronous variant: +```swift +try dataStack.addStorage( + InMemoryStore( + configuration: "Config2 + ), + completion: { storage in + // ... + } +) +``` + +(A reactive-programming variant of this method is explained in detail in the section on [`DataStack` Combine publishers](#datastackreactive)) ### Local Store The most common `StorageInterface` you will probably use is the `SQLiteStore`, which saves data in a local SQLite file. @@ -292,7 +333,9 @@ CoreStore can decide the default values for these properties, so `SQLiteStore`s try dataStack.addStorageAndWait(SQLiteStore()) ``` -The file-related properties above are actually requirements of another protocol that `SQLiteStore` implements, the `LocalStorage` protocol: +(The asynchronous variant of this method is explained further in the next section on [Migrations](#starting-migrations), and a reactive-programming variant in the section on [`DataStack` Combine publishers](#datastackreactive)) + +The file-related properties of `SQLiteStore` are actually requirements of another protocol that it implements, the `LocalStorage` protocol: ```swift public protocol LocalStorage: StorageInterface { var fileURL: NSURL { get } @@ -463,6 +506,8 @@ let migrationProgress: Progress? = try dataStack.addStorage( ``` The `completion` block reports a `SetupResult` that indicates success or failure. +(A reactive-programming variant of this method is explained further in the section on [`DataStack` Combine publishers](#datastackreactive)) + Notice that this method also returns an optional `Progress`. If `nil`, no migrations are needed, thus progress reporting is unnecessary as well. If not `nil`, you can use this to track migration progress by using standard KVO on the `"fractionCompleted"` key, or by using a closure-based utility exposed in *Progress+Convenience.swift*: ```swift migrationProgress?.setProgressHandler { [weak self] (progress) -> Void in @@ -1460,6 +1505,8 @@ Note that the owner instance will not be retained. You may call `ObjectPublisher The `ObjectSnapshot` returned from the `ObjectPublisher.snapshot` property returns a full-copy `struct` of all properties of the object. This is ideal for managing states as they are thread-safe and are not affected by further changes to the actual object. `ObjectPublisher` automatically updates its `snapshot` value to the latest state of the object. +(A reactive-programming variant of this method is explained in detail in the section on [`ObjectPublisher` Combine publishers](#objectpublisherreactive)) + ### Observe a single object's per-property updates @@ -1513,6 +1560,8 @@ 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. +(A reactive-programming variant of this method is explained in detail in the section on [`ListPublisher` Combine publishers](#listpublisherreactive)) + 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.CollectionViewAdapter( @@ -1815,13 +1864,13 @@ Starting CoreStore 7.1.0, `CoreStoreObject` properties may be converted to `@Fie The `@Field.Stored` property wrapper is used for persisted value types. This is the replacement for "non-transient" `Value.Required` and `Value.Optional` properties. - +
Before`@Field.Stored`
Before
@Field.Stored
 class Person: CoreStoreObject {
     
- let title = Value.Required("title", initial: "Mr.") - let nickname = Value.Optional("nickname") + let title = Value.Required<String>("title", initial: "Mr.") + let nickname = Value.Optional<String>("nickname") }
@@ -1846,20 +1895,20 @@ class Person: CoreStoreObject {
 The `@Field.Virtual` property wrapper is used for unsaved, computed value types. This is the replacement for "transient" `Value.Required` and `Value.Optional` properties.
 
 
-
+
Before`@Field.Virtual`
Before
@Field.Virtual
 class Animal: CoreStoreObject {
     
- let speciesPlural = Value.Required( + let speciesPlural = Value.Required<String>( "speciesPlural", transient: true, customGetter: Animal.getSpeciesPlural(_:) )
- let species = Value.Required("species", initial: "") + let species = Value.Required<String>("species", initial: "")
- static func getSpeciesPlural(_ partialObject: PartialObject) -> String? { + static func getSpeciesPlural(_ partialObject: PartialObject<Animal>) -> String? { let species = partialObject.value(for: { $0.species }) return species + "s" } @@ -1894,12 +1943,12 @@ The `@Field.Coded` property wrapper is used for binary-codable values. This is t > ‼️ 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. - +
Before`@Field.Coded`
Before
@Field.Coded
 class Vehicle: CoreStoreObject {
     
- let color = Transformable.Optional("color", initial: .white) + let color = Transformable.Optional<UIColor>("color", initial: .white) }
@@ -1947,16 +1996,16 @@ The type of relationship is determined by the `@Field.Relationship`  generic typ
 - `Set` : To-many unordered relationship
 
 
-
+
@@ -2060,6 +2109,438 @@ Once the version lock is set, any changes in the properties or to the model will
 
 VersionLock failure
 
+## Reactive Programming
+### RxSwift
+RxSwift utilities are available through the [RxCoreStore](https://github.com/JohnEstropia/RxCoreStore) external module.
+
+### Combine
+
+Combine publishers are available from the `DataStack`, `ListPublisher`, and `ObjectPublisher`'s `.reactive` namespace property.
+
+#### `DataStack.reactive`
+
+Adding a storage through `DataStack.reactive.addStorage(_:)` returns a publisher that reports a `MigrationProgress` `enum` value. The `.migrating` value is only emitted if the storage goes through a migration. Refer to the [Setting up](#setting-up) section for details on the storage setup process itself.
+
+```swift
+dataStack.reactive
+    .addStorage(
+        SQLiteStore(fileName: "core_data.sqlite")
+    )
+    .sink(
+        receiveCompletion: { result in
+            // ...
+        },
+        receiveValue: { (progress) in
+            print("\(round(progress.fractionCompleted * 100)) %") // 0.0 ~ 1.0
+            switch progress {
+            case .migrating(let storage, let nsProgress):
+                // ...
+            case .finished(let storage, let migrationRequired):
+                // ...
+            }
+        }
+    )
+    .store(in: &cancellables)
+```
+
+[Transactions](#saving-and-processing-transactions) are also available as publishers through `DataStack.reactive.perform(_:)`, which returns a Combine `Future` that emits any type returned from the closure parameter:
+```swift
+dataStack.reactive
+    .perform(
+        asynchronous: { (transaction) -> (inserted: Set, deleted: Set) in
+
+            // ...
+            return (
+                transaction.insertedObjects(),
+                transaction.deletedObjects()
+            )
+        }
+    )
+    .sink(
+        receiveCompletion: { result in
+            // ...
+        },
+        receiveValue: { value in
+            let inserted = dataStack.fetchExisting(value0.inserted)
+            let deleted = dataStack.fetchExisting(value0.deleted)
+            // ...
+        }
+    )
+    .store(in: &cancellables)
+```
+
+For importing convenience, `ImportableObject` and `ImportableUniqueObjects` can be imported directly through `DataStack.reactive.import[Unique]Object(_:source:)` and `DataStack.reactive.import[Unique]Objects(_:sourceArray:)` without having to create a transaction block. In this case the publisher emits objects that are already usable directly from the main queue:
+```swift
+dataStack.reactive
+    .importUniqueObjects(
+        Into(),
+        sourceArray: [
+            ["name": "John"],
+            ["name": "Bob"],
+            ["name": "Joe"]
+        ]
+    )
+    .sink(
+        receiveCompletion: { result in
+            // ...
+        },
+        receiveValue: { (people) in
+            XCTAssertEqual(people?.count, 3)
+            // ...
+        }
+    )
+    .store(in: &cancellables)
+```
+
+#### `ListPublisher.reactive`
+
+`ListPublisher`s can be used to emit `ListSnapshot`s through Combine using `ListPublisher.reactive.snapshot(emitInitialValue:)`. The snapshot values are emitted in the main queue:
+```swift
+listPublisher.reactive
+    .snapshot(emitInitialValue: true)
+    .sink(
+        receiveCompletion: { result in
+            // ...
+        },
+        receiveValue: { (listSnapshot) in
+            dataSource.apply(
+                listSnapshot,
+                animatingDifferences: true
+            )
+        }
+    )
+    .store(in: &cancellables)
+```
+
+#### `ObjectPublisher.reactive`
+
+`ObjectPublisher`s can be used to emit `ObjectSnapshot`s through Combine using `ObjectPublisher.reactive.snapshot(emitInitialValue:)`. The snapshot values are emitted in the main queue:
+```swift
+objectPublisher.reactive
+    .snapshot(emitInitialValue: true)
+    .sink(
+        receiveCompletion: { result in
+            // ...
+        },
+        receiveValue: { (objectSnapshot) in
+            tableViewCell.setObject(objectSnapshot)
+        }
+    )
+    .store(in: &tableViewCell.cancellables)
+```
+
+
+## SwiftUI Utilities
+
+Observing list and object changes in SwiftUI can be done through a couple of approaches. One is by creating [views that autoupdates their contents](#swiftui-views), or by declaring [property wrappers that trigger view updates](#swiftui-property-wrappers). Both approaches are implemented almost the same internally, but this lets you be flexible depending on the structure of your custom `View`s.
+
+### SwiftUI Views
+
+CoreStore provides `View` containers that automatically update their contents when data changes.
+
+#### `ListReader`
+
+A `ListReader` observes changes to a `ListPublisher` and creates its content views dynamically. The builder closure receives a `ListSnapshot` value that can be used to create the contents:
+```swift
+let people: ListPublisher
+
+var body: some View {
+   List {
+       ListReader(self.people) { listSnapshot in
+           ForEach(objectIn: listSnapshot) { person in
+               // ...
+           }
+       }
+   }
+   .animation(.default)
+}
+```
+As shown above, a typical use case is to use it together with CoreStore's [`ForEach` extensions](#foreach).
+
+A `KeyPath` can also be optionally provided to extract specific properties of the `ListSnapshot`:
+```swift
+let people: ListPublisher
+
+var body: some View {
+    ListReader(self.people, keyPath: \.count) { count in
+        Text("Number of members: \(count)")
+    }
+}
+```
+
+#### `ObjectReader`
+
+An `ObjectReader` observes changes to an `ObjectPublisher` and creates its content views dynamically. The builder closure receives an `ObjectSnapshot` value that can be used to create the contents:
+```swift
+let person: ObjectPublisher
+
+var body: some View {
+   ObjectReader(self.person) { objectSnapshot in
+       // ...
+   }
+   .animation(.default)
+}
+```
+
+A `KeyPath` can also be optionally provided to extract specific properties of the `ObjectSnapshot`:
+```swift
+let person: ObjectPublisher
+
+var body: some View {
+    ObjectReader(self.person, keyPath: \.fullName) { fullName in
+        Text("Name: \(fullName)")
+    }
+}
+```
+
+By default, an `ObjectReader` does not create its views wheen the object observed is deleted from the store. In those cases, the `placeholder:` argument can be used to provide a custom `View` to display when the object is deleted:
+```swift
+let person: ObjectPublisher
+
+var body: some View {
+   ObjectReader(
+       self.person,
+       content: { objectSnapshot in
+           // ...
+       },
+       placeholder: { Text("Record not found") }
+   )
+}
+```
+
+### SwiftUI Property Wrappers
+
+As an alternative to `ListReader` and `ObjectReader`, CoreStore also provides property wrappers that trigger view updates when the data changes.
+
+#### `ListState`
+
+A `@ListState` property exposes a `ListSnapshot` value that automatically updates to the latest changes.
+```swift
+@ListState
+var people: ListSnapshot
+
+init(listPublisher: ListPublisher) {
+   self._people = .init(listPublisher)
+}
+
+var body: some View {
+   List {
+       ForEach(objectIn: self.people) { objectSnapshot in
+           // ...
+       }
+   }
+   .animation(.default)
+}
+```
+As shown above, a typical use case is to use it together with CoreStore's [`ForEach` extensions](#foreach).
+
+If a `ListPublisher` instance is not available yet, the fetch can be done inline by providing the fetch clauses and the `DataStack` instance. By doing so the property can be declared without an initial value:
+```swift
+@ListState(
+    From()
+        .sectionBy(\.age)
+        .where(\.isMember == true)
+        .orderBy(.ascending(\.lastName))
+)
+var people: ListSnapshot
+
+var body: some View {
+    List {
+        ForEach(sectionIn: self.people) { section in
+            Section(header: Text(section.sectionID)) {
+                ForEach(objectIn: section) { person in
+                    // ...
+                }
+            }
+        }
+    }
+    .animation(.default)
+}
+```
+
+For other initialization variants, refer to the *ListState.swift*  source documentations.
+
+
+#### `ObjectState`
+
+An `@ObjectState` property exposes an optional `ObjectSnapshot` value that automatically updates to the latest changes.
+```swift
+@ObjectState
+var person: ObjectSnapshot?
+
+init(objectPublisher: ObjectPublisher) {
+   self._person = .init(objectPublisher)
+}
+
+var body: some View {
+   HStack {
+       if let person = self.person {
+           AsyncImage(person.$avatarURL)
+           Text(person.$fullName)
+       }
+       else {
+           Text("Record removed")
+       }
+   }
+}
+```
+As shown above, the property's value will be `nil` if the object has been deleted, so this can be used to display placeholders if needed.
+
+### SwiftUI Extensions
+
+For convenience, CoreStore provides extensions to the standard SwiftUI types.
+
+#### `ForEach`
+
+Several `ForEach` initializer overloads are available. Choose depending on your input data and the expected closure data. Refer to the table below (Take note of the argument labels as they are important):
+
+
Before`@Field.Stored`
Before
@Field.Stored
 class Pet: CoreStoreObject {
     
- let master = Relationship.ToOne("master") + let master = Relationship.ToOne<Person>("master") } class Person: CoreStoreObject {
- let pets: Relationship.ToManyUnordered("pets", inverse: \.$master) + let pets: Relationship.ToManyUnordered<Pet>("pets", inverse: \.$master) }
@@ -1968,7 +2017,7 @@ class Pet: CoreStoreObject {
 class Person: CoreStoreObject {
     
@Field.Relationship("pets", inverse: \.$master) - var pets: Set + var pets: Set<Pet> }
+ + + + + + + + + + + + + + + + + + + + + +
DataExample
+Signature: +
+ForEach(_: [ObjectSnapshot<O>])
+
+Closure: +
+ObjectSnapshot<O>
+
+
+let array: [ObjectSnapshot<Person>]
+
+var body: some View { +
+ List { +
+ ForEach(self.array) { objectSnapshot in +
+ // ... + } + } +} +
+Signature: +
+ForEach(objectIn: ListSnapshot<O>)
+
+Closure: +
+ObjectPublisher<O>
+
+
+let listSnapshot: ListSnapshot<Person>
+
+var body: some View { +
+ List { +
+ ForEach(objectIn: self.listSnapshot) { objectPublisher in +
+ // ... + } + } +} +
+Signature: +
+ForEach(objectIn: [ObjectSnapshot<O>])
+
+Closure: +
+ObjectPublisher<O>
+
+
+let array: [ObjectSnapshot<Person>]
+
+var body: some View { +
+ List { +
+ ForEach(objectIn: self.array) { objectPublisher in +
+ // ... + } + } +} +
+Signature: +
+ForEach(sectionIn: ListSnapshot<O>)
+
+Closure: +
+[ListSnapshot<O>.SectionInfo]
+
+
+let listSnapshot: ListSnapshot<Person>
+
+var body: some View { +
+ List { +
+ ForEach(sectionIn: self.listSnapshot) { sectionInfo in +
+ // ... + } + } +} +
+Signature: +
+ForEach(objectIn: ListSnapshot<O>.SectionInfo)
+
+Closure: +
+ObjectPublisher<O>
+
+
+let listSnapshot: ListSnapshot<Person>
+
+var body: some View { +
+ List { +
+ ForEach(sectionIn: self.listSnapshot) { sectionInfo in +
+ ForEach(objectIn: sectionInfo) { objectPublisher in +
+ // ... + } + } + } +} +
+ + +# Roadmap + +### Prototyping stage +- [ ] Widget/Extensions storage-sharing support +- [ ] CloudKit support + +### Under consideration +- [ ] Derived attributes +- [ ] Cross-storage relationships (via Fetched Attributes) # Installation - Requires: @@ -2074,7 +2555,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.2' +pod 'CoreStore', '~> 8.0' ``` and run ``` @@ -2085,7 +2566,7 @@ This installs CoreStore as a framework. Declare `import CoreStore` in your swift ### Install with Carthage In your `Cartfile`, add ``` -github "JohnEstropia/CoreStore" >= 7.3.0 +github "JohnEstropia/CoreStore" >= 8.0.0 ``` and run ``` @@ -2096,7 +2577,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.3.1")) + .package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "8.0.0")) ] ``` Declare `import CoreStore` in your swift file to use the library. diff --git a/Sources/ForEach+SwiftUI.swift b/Sources/ForEach+SwiftUI.swift index d91027a..703aef9 100644 --- a/Sources/ForEach+SwiftUI.swift +++ b/Sources/ForEach+SwiftUI.swift @@ -68,7 +68,7 @@ extension ForEach where Content: View { /** Creates an instance that creates views for each object in a `ListSnapshot`. ``` - @LiveList + @ListState var people: ListSnapshot var body: some View { @@ -127,7 +127,7 @@ extension ForEach where Content: View { /** Creates an instance that creates views for `ListSnapshot` sections. ``` - @LiveList + @ListState var people: ListSnapshot var body: some View { @@ -164,7 +164,7 @@ extension ForEach where Content: View { /** Creates an instance that creates views for each object in a `ListSnapshot.SectionInfo`. ``` - @LiveList + @ListState var people: ListSnapshot var body: some View { diff --git a/Sources/ListReader.swift b/Sources/ListReader.swift index 5dfaa28..1a8f85e 100644 --- a/Sources/ListReader.swift +++ b/Sources/ListReader.swift @@ -70,7 +70,6 @@ public struct ListReader: View { self._list = .init(listPublisher) self.content = content - self.keyPath = \.self } /** @@ -98,8 +97,10 @@ public struct ListReader: View { ) { self._list = .init(listPublisher) - self.content = content - self.keyPath = keyPath + self.content = { + + content($0[keyPath: keyPath]) + } } @@ -107,17 +108,16 @@ public struct ListReader: View { public var body: some View { - self.content(self.list[keyPath: self.keyPath]) + self.content(self.list) } // MARK: Private - @LiveList + @ListState private var list: ListSnapshot - private let content: (Value) -> Content - private let keyPath: KeyPath, Value> + private let content: (ListSnapshot) -> Content } #endif diff --git a/Sources/LiveList.swift b/Sources/ListState.swift similarity index 93% rename from Sources/LiveList.swift rename to Sources/ListState.swift index c6557c8..f165947 100644 --- a/Sources/LiveList.swift +++ b/Sources/ListState.swift @@ -1,5 +1,5 @@ // -// LiveList.swift +// ListState.swift // CoreStore // // Copyright © 2021 John Rommel Estropia @@ -29,21 +29,21 @@ import Combine import SwiftUI -// MARK: - LiveList +// MARK: - ListState /** A property wrapper type that can read `ListPublisher` changes. */ @propertyWrapper @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -public struct LiveList: DynamicProperty { +public struct ListState: DynamicProperty { // MARK: Public /** Creates an instance that observes `ListPublisher` changes and exposes a `ListSnapshot` value. ``` - @LiveList + @ListState var people: ListSnapshot init(listPublisher: ListPublisher) { @@ -64,7 +64,7 @@ public struct LiveList: DynamicProperty { } ``` - - parameter listPublisher: The `ListPublisher` that the `LiveList` will observe changes for + - parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for */ public init( _ listPublisher: ListPublisher @@ -76,10 +76,11 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `FetchChainableBuilderType` and exposes a `ListSnapshot` value. ``` - @LiveList( + @ListState( From() .where(\.isMember == true) - .orderBy(.ascending(\.lastName)) + .orderBy(.ascending(\.lastName)), + in: Globals.dataStack ) var people: ListSnapshot @@ -109,11 +110,12 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `SectionMonitorBuilderType` and exposes a `ListSnapshot` value. ``` - @LiveList( + @ListState( From() .sectionBy(\.age) .where(\.isMember == true) - .orderBy(.ascending(\.lastName)) + .orderBy(.ascending(\.lastName)), + in: Globals.dataStack ) var people: ListSnapshot @@ -149,10 +151,11 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `From` and `FetchClause`s and exposes a `ListSnapshot` value. ``` - @LiveList( + @ListState( From(), Where(\.isMember == true), - OrderBy(.ascending(\.lastName)) + OrderBy(.ascending(\.lastName)), + in: Globals.dataStack ) var people: ListSnapshot @@ -184,12 +187,13 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `From` and `FetchClause`s and exposes a `ListSnapshot` value. ``` - @LiveList( + @ListState( From(), [ Where(\.isMember == true), OrderBy(.ascending(\.lastName)) - ] + ], + in: Globals.dataStack ) var people: ListSnapshot @@ -221,11 +225,12 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `From`, `SectionBy`, and `FetchClause`s and exposes a sectioned `ListSnapshot` value. ``` - @LiveList( + @ListState( From(), SectionBy(\.age), Where(\.isMember == true), - OrderBy(.ascending(\.lastName)) + OrderBy(.ascending(\.lastName)), + in: Globals.dataStack ) var people: ListSnapshot @@ -265,13 +270,14 @@ public struct LiveList: DynamicProperty { /** Creates an instance that observes the specified `From`, `SectionBy`, and `FetchClause`s and exposes a sectioned `ListSnapshot` value. ``` - @LiveList( + @ListState( From(), SectionBy(\.age), [ Where(\.isMember == true), OrderBy(.ascending(\.lastName)) - ] + ], + in: Globals.dataStack ) var people: ListSnapshot diff --git a/Sources/ObjectReader.swift b/Sources/ObjectReader.swift index c0ec228..a66f682 100644 --- a/Sources/ObjectReader.swift +++ b/Sources/ObjectReader.swift @@ -53,7 +53,6 @@ public struct ObjectReader? - private let content: (Value) -> Content + private let content: (ObjectSnapshot) -> Content private let placeholder: () -> Placeholder - private let keyPath: KeyPath, Value> } #endif diff --git a/Sources/LiveObject.swift b/Sources/ObjectState.swift similarity index 95% rename from Sources/LiveObject.swift rename to Sources/ObjectState.swift index b415baf..5e00c56 100644 --- a/Sources/LiveObject.swift +++ b/Sources/ObjectState.swift @@ -1,5 +1,5 @@ // -// LiveObject.swift +// ObjectState.swift // CoreStore // // Copyright © 2021 John Rommel Estropia @@ -29,21 +29,21 @@ import Combine import SwiftUI -// MARK: - LiveObject +// MARK: - ObjectState /** A property wrapper type that can read `ObjectPublisher` changes. */ @propertyWrapper @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) -public struct LiveObject: DynamicProperty { +public struct ObjectState: DynamicProperty { // MARK: Public /** Creates an instance that observes `ObjectPublisher` changes and exposes an `Optional>` value. ``` - @LiveObject + @ObjectState var person: ObjectSnapshot? init(objectPublisher: ObjectPublisher) { @@ -61,7 +61,7 @@ public struct LiveObject: DynamicProperty { } ``` - - parameter objectPublisher: The `ObjectPublisher` that the `LiveObject` will observe changes for + - parameter objectPublisher: The `ObjectPublisher` that the `ObjectState` will observe changes for */ public init(_ objectPublisher: ObjectPublisher?) {