This commit is contained in:
John Estropia
2026-07-15 11:50:43 +09:00
parent 7db1cfecfb
commit 890e150b95
135 changed files with 2895 additions and 2526 deletions
+22 -2
View File
@@ -19,6 +19,14 @@
53EB03E72A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; };
53EB03E82A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; };
53EB03E92A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; };
5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; };
5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; };
5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; };
5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; };
5F17507E2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; };
5F17507F2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; };
5F1750802FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; };
5F1750812FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; };
82BA18931C4BBCBA00A0916E /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82BA18891C4BBCBA00A0916E /* CoreStore.framework */; };
82BA18A01C4BBD1400A0916E /* CoreStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F03A53519C5C6DA005002A5 /* CoreStore.h */; settings = {ATTRIBUTES = (Public, ); }; };
82BA18A21C4BBD1D00A0916E /* CoreStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D1E22B19FA9FBC003B2874 /* CoreStoreError.swift */; };
@@ -883,6 +891,8 @@
2F03A53E19C5C6DA005002A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2F03A54C19C5C872005002A5 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
53EB03E52A3AD5D400179430 /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = "<group>"; };
5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicObjectID.swift; sourceTree = "<group>"; };
5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.Mutext.swift; sourceTree = "<group>"; };
82BA18891C4BBCBA00A0916E /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
82BA18921C4BBCBA00A0916E /* CoreStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreStoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
82BA18DE1C4BBE2600A0916E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.1.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
@@ -1453,6 +1463,7 @@
B5831B6E1F3355C300A9F647 /* Legacy Properties */,
B52F74391E9B8724005F3DAC /* Dynamic Schema */,
B5D339DC1E9489C700C880DE /* DynamicObject.swift */,
5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */,
B52F742E1E9B50D0005F3DAC /* SchemaHistory.swift */,
B5D339E61E9493A500C880DE /* Entity.swift */,
B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */,
@@ -1725,6 +1736,7 @@
B54A6A541BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift */,
B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */,
B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */,
5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */,
B5E84F2B1AFF849C0064E85B /* Internals.NotificationObserver.swift */,
B5277676234F265F0056BE9F /* Internals.SharedNotificationObserver.swift */,
B50E17602351FA66004F033C /* Internals.Closure.swift */,
@@ -2131,6 +2143,7 @@
B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */,
B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */,
B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */,
5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */,
B50C3EFE23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */,
B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */,
B5C7959925D7D8B300BDACC1 /* ListReader.swift in Sources */,
@@ -2148,6 +2161,7 @@
B5944EF625E269F9001D1D81 /* ObjectPublisher+Reactive.swift in Sources */,
B50392F91C478FF3009900CA /* NSManagedObject+Transaction.swift in Sources */,
B5202CFA1C04688100DED140 /* NSFetchedResultsController+Convenience.swift in Sources */,
5F1750802FFE35AF00BA577F /* Internals.Mutext.swift in Sources */,
B5E84F251AFF84860064E85B /* ObjectObserver.swift in Sources */,
B5E84F2F1AFF849C0064E85B /* Internals.NotificationObserver.swift in Sources */,
B5F1DA8D1B9AA97D007C5CBB /* ImportableObject.swift in Sources */,
@@ -2348,6 +2362,7 @@
B5831F432212700400D8604C /* Where.Expression.swift in Sources */,
B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */,
B5FE4DA81C84FB4400FA6A91 /* InMemoryStore.swift in Sources */,
5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */,
B50C3EFF23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */,
B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */,
B5C7959A25D7D8B300BDACC1 /* ListReader.swift in Sources */,
@@ -2365,6 +2380,7 @@
82BA18D51C4BBD7100A0916E /* NSManagedObjectContext+Setup.swift in Sources */,
B5E41EC11EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */,
B5A1DAC91F111BFA003CF369 /* KeyPath+Querying.swift in Sources */,
5F17507F2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */,
B5FE4DA31C8481E100FA6A91 /* StorageInterface.swift in Sources */,
B5C7959025D7D18000BDACC1 /* ListState.swift in Sources */,
B56923C51EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */,
@@ -2600,6 +2616,7 @@
B52DD1AD1BE1F93900949AFE /* Where.swift in Sources */,
B5F8496F234898240029D57B /* ListSnapshot.swift in Sources */,
B5831F452212700500D8604C /* Where.Expression.swift in Sources */,
5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */,
B5B866DE25E9012F00335476 /* ListPublisher+Reactive.swift in Sources */,
B514EF1423A8DB1E0093DBA4 /* DiffableDataSource.BaseAdapter.swift in Sources */,
B51260961E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */,
@@ -2617,6 +2634,7 @@
B5220E131D1305ED009BC71E /* SectionBy.swift in Sources */,
B5C7959225D7D18000BDACC1 /* ListState.swift in Sources */,
B5E41EC31EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */,
5F1750812FFE35AF00BA577F /* Internals.Mutext.swift in Sources */,
B5A1DACB1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */,
B52DD1B81BE1F94000949AFE /* DataStack+Migration.swift in Sources */,
B5D8CA792346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */,
@@ -2817,6 +2835,7 @@
B5831F442212700500D8604C /* Where.Expression.swift in Sources */,
B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */,
B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */,
5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */,
B5C7959B25D7D8B300BDACC1 /* ListReader.swift in Sources */,
B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */,
B51260951E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */,
@@ -2834,6 +2853,7 @@
B56321B31BD6521C006C9394 /* NSManagedObjectContext+Setup.swift in Sources */,
B5E41EC21EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */,
B5C7959125D7D18000BDACC1 /* ListState.swift in Sources */,
5F17507E2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */,
B5A1DACA1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */,
B56321AE1BD6521C006C9394 /* Internals.NotificationObserver.swift in Sources */,
B56321931BD65216006C9394 /* DataStack+Querying.swift in Sources */,
@@ -3027,7 +3047,7 @@
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 6.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -3092,7 +3112,7 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 6.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 17.0;
VALIDATE_PRODUCT = YES;
+3 -3
View File
@@ -24,11 +24,11 @@
//
import XCTest
import os
@testable
import CoreStore
#if !SWIFT_PACKAGE
extension Bundle {
@@ -196,7 +196,7 @@ final class TestLogger: CoreStoreLogger {
init(_ expectations: [Expectation: XCTestExpectation]) {
self.expectations = .init(initialState: expectations)
self.expectations = .init(expectations)
}
@@ -236,7 +236,7 @@ final class TestLogger: CoreStoreLogger {
// MARK: Private
private let expectations: OSAllocatedUnfairLock<[Expectation: XCTestExpectation]>
private let expectations: Internals.Mutex<[Expectation: XCTestExpectation]>
private func fulfill(_ expectation: Expectation) {
+1
View File
@@ -201,6 +201,7 @@ class Person: CoreStoreObject {
class DynamicModelTests: BaseTestDataTestCase {
@MainActor
@objc
dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() {
+32 -12
View File
@@ -33,6 +33,7 @@ import CoreStore
final class FetchTests: BaseTestDataTestCase {
@MainActor
@objc
dynamic func test_ThatDataStacksAndTransactions_CanFetchOneExisting() {
@@ -103,14 +104,14 @@ final class FetchTests: BaseTestDataTestCase {
}
do {
let persistentID = object.persistentID()
let fetchExpectation = self.expectation(description: "fetch")
var existing1: TestEntity1?
stack.perform(
asynchronous: { (transaction) in
existing1 = transaction.fetchExisting(object)
let existing1 = transaction.fetchExisting(persistentID)
XCTAssertNotNil(existing1)
XCTAssertEqual(existing1!.objectID, object.objectID)
XCTAssertEqual(existing1!.persistentID(), persistentID)
XCTAssertEqual(existing1!.managedObjectContext, transaction.context)
try transaction.cancel()
@@ -123,9 +124,9 @@ final class FetchTests: BaseTestDataTestCase {
XCTAssertEqual(error, CoreStoreError.userCancelled)
let existing2 = stack.fetchExisting(existing1!)
let existing2 = stack.fetchExisting(persistentID)
XCTAssertNotNil(existing2)
XCTAssertEqual(existing2!.objectID, object.objectID)
XCTAssertEqual(existing2!.persistentID(), persistentID)
XCTAssertEqual(existing2!.managedObjectContext, stack.mainContext)
fetchExpectation.fulfill()
@@ -136,6 +137,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacksAndTransactions_CanFetchAllExisting() {
@@ -230,15 +232,15 @@ final class FetchTests: BaseTestDataTestCase {
}
do {
let persistentIDs = objects.map({ $0.persistentID() })
let fetchExpectation = self.expectation(description: "fetch")
var existing1 = [TestEntity1]()
stack.perform(
asynchronous: { (transaction) in
existing1 = transaction.fetchExisting(objects)
let existing1 = transaction.fetchExisting(persistentIDs)
XCTAssertEqual(
existing1.map { $0.objectID },
objects.map { $0.objectID }
existing1.map { $0.persistentID() },
persistentIDs
)
for object in existing1 {
@@ -254,10 +256,10 @@ final class FetchTests: BaseTestDataTestCase {
XCTAssertEqual(error, CoreStoreError.userCancelled)
let existing2 = stack.fetchExisting(existing1)
let existing2 = stack.fetchExisting(persistentIDs)
XCTAssertEqual(
existing2.map { $0.objectID },
objects.map { $0.objectID }
existing2.map { $0.persistentID() },
persistentIDs
)
for object in existing2 {
@@ -271,6 +273,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchOneFromDefaultConfiguration() {
@@ -404,6 +407,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchOneFromSingleConfiguration() {
@@ -581,6 +585,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchOneFromMultipleConfigurations() {
@@ -724,6 +729,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchAllFromDefaultConfiguration() {
@@ -912,6 +918,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchAllFromSingleConfiguration() {
@@ -1125,6 +1132,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchAllFromMultipleConfigurations() {
@@ -1306,6 +1314,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchCountFromDefaultConfiguration() {
@@ -1420,6 +1429,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchCountFromSingleConfiguration() {
@@ -1567,6 +1577,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatDataStacks_CanFetchCountFromMultipleConfigurations() {
@@ -1677,6 +1688,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchOneFromDefaultConfiguration() {
@@ -1818,6 +1830,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchOneFromSingleConfiguration() {
@@ -2006,6 +2019,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchOneFromMultipleConfigurations() {
@@ -2157,6 +2171,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchAllFromDefaultConfiguration() {
@@ -2353,6 +2368,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchAllFromSingleConfiguration() {
@@ -2589,6 +2605,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchAllFromMultipleConfigurations() {
@@ -2790,6 +2807,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchCountFromDefaultConfiguration() {
@@ -2918,6 +2936,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchCountFromSingleConfiguration() {
@@ -3076,6 +3095,7 @@ final class FetchTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatTransactions_CanFetchCountFromMultipleConfigurations() {
+4
View File
@@ -67,6 +67,7 @@ final class FromTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatFromClauses_ApplyToFetchRequestsCorrectlyForDefaultConfigurations() {
self.prepareStack { (dataStack) in
@@ -106,6 +107,7 @@ final class FromTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatFromClauses_ApplyToFetchRequestsCorrectlyForSingleConfigurations() {
self.prepareStack(configurations: ["Config1"]) { (dataStack) in
@@ -212,6 +214,7 @@ final class FromTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatFromClauses_ApplyToFetchRequestsCorrectlyForDefaultAndCustomConfigurations() {
self.prepareStack(configurations: [nil, "Config1"]) { (dataStack) in
@@ -316,6 +319,7 @@ final class FromTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatFromClauses_ApplyToFetchRequestsCorrectlyForMultipleConfigurations() {
self.prepareStack(configurations: ["Config1", "Config2"]) { (dataStack) in
+19 -5
View File
@@ -72,6 +72,7 @@ class ImportTests: BaseTestDataTestCase {
XCTAssertEqual(Double(MAXFLOAT).cs_toQueryableNativeType(), NSNumber(value: Double(MAXFLOAT)))
}
@MainActor
@objc
dynamic func test_ThatImportObject_CanSkipImport() {
@@ -107,6 +108,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportObject_CanThrowError() {
@@ -160,6 +162,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportObject_CanImportCorrectly() {
@@ -220,6 +223,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportObjects_CanSkipImport() {
@@ -275,6 +279,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportObjects_CanThrowError() {
@@ -339,6 +344,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportObjects_CanImportCorrectly() {
@@ -397,6 +403,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObject_CanSkipImport() {
@@ -466,6 +473,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObjects_ImportsLastOfImportSourcesWithSameIDs() {
@@ -523,7 +531,8 @@ class ImportTests: BaseTestDataTestCase {
}
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObjects_MaintainsOrderOfInputSourceArray() {
@@ -585,6 +594,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObject_CanThrowError() {
@@ -680,7 +690,8 @@ class ImportTests: BaseTestDataTestCase {
}
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObject_CanImportCorrectly() {
@@ -757,7 +768,8 @@ class ImportTests: BaseTestDataTestCase {
}
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObjects_CanSkipImport() {
@@ -815,7 +827,8 @@ class ImportTests: BaseTestDataTestCase {
}
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObjects_CanThrowError() {
@@ -977,6 +990,7 @@ class ImportTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatImportUniqueObjects_CanImportCorrectly() {
@@ -1065,7 +1079,7 @@ extension TestEntity1: ImportableUniqueObject {
// MARK: ImportableObject
typealias ImportSource = [String: Any]
typealias ImportSource = [String: any Sendable]
static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool {
+230 -184
View File
@@ -34,6 +34,7 @@ import CoreStore
class ListObserverTests: BaseTestDataTestCase {
@MainActor
@objc
dynamic func test_ThatListObservers_CanReceiveInsertNotifications() {
@@ -51,20 +52,23 @@ class ListObserverTests: BaseTestDataTestCase {
XCTAssertFalse(monitor.hasObjects())
XCTAssertTrue(monitor.objectsInAllSections().isEmpty)
var events = 0
let events: Internals.Mutex<Int> = .init(0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
events += 1
}
return events == 0
}
return events == 0
}
)
_ = self.expectation(
@@ -72,51 +76,57 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 1)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
[
"sectionInfo": monitor.sectionInfo(at: 0),
"sectionIndex": 0
] as NSDictionary
)
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 1)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
[
"sectionInfo": monitor.sectionInfo(at: 0),
"sectionIndex": 0
] as NSDictionary
)
defer {
events += 1
}
return events == 1
}
return events == 1
}
)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didInsertObject:toIndexPath:"),
object: observer,
handler: { (note) -> Bool in
handler: { [dateFormatter] (note) -> Bool in
XCTAssertEqual(events, 2)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 1))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "1"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:1")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:1" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, self.dateFormatter.date(from: "2000-01-01T00:00:00Z")!)
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 2)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 1))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "1"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:1")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:1" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, dateFormatter.date(from: "2000-01-01T00:00:00Z")!)
defer {
events += 1
}
return events == 2
}
return events == 2
}
)
_ = self.expectation(
@@ -125,16 +135,19 @@ class ListObserverTests: BaseTestDataTestCase {
handler: { (note) -> Bool in
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
defer {
events += 1
}
return events == 3
}
return events == 3
}
)
let saveExpectation = self.expectation(description: "save")
stack.perform(
asynchronous: { (transaction) -> Bool in
asynchronous: { [dateFormatter] (transaction) -> Bool in
let object = transaction.create(Into<TestEntity1>())
object.testBoolean = NSNumber(value: true)
@@ -142,7 +155,7 @@ class ListObserverTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "1")
object.testString = "nil:TestEntity1:1"
object.testData = ("nil:TestEntity1:1" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-01T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-01T00:00:00Z")!
return transaction.hasChanges
},
@@ -160,6 +173,7 @@ class ListObserverTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatListObservers_CanReceiveUpdateNotifications() {
@@ -182,72 +196,78 @@ class ListObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.numberOfObjects(in: 0), 2)
XCTAssertEqual(monitor.numberOfObjects(in: 1), 3)
var events = 0
let events: Internals.Mutex<Int> = .init(0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
events += 1
}
return events == 0
}
return events == 0
}
)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitor:didUpdateObject:atIndexPath:"),
object: observer,
handler: { (note) -> Bool in
handler: { [dateFormatter] (note) -> Bool in
XCTAssert(events == 1 || events == 2)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
let object = userInfo?["object"] as? TestEntity1
switch object?.testEntityID {
return events.withLock { events in
case NSNumber(value: 101)?:
XCTAssertEqual(indexPath?.section, 1)
XCTAssertEqual(indexPath?.item, 0)
XCTAssert(events == 1 || events == 2)
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 11))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "11"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:11")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:11" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, self.dateFormatter.date(from: "2000-01-11T00:00:00Z")!)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
case NSNumber(value: 102)?:
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
let indexPath = userInfo?["indexPath"] as? IndexPath
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testBoolean, NSNumber(value: false))
XCTAssertEqual(object?.testNumber, NSNumber(value: 22))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "22"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:22")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:22" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, self.dateFormatter.date(from: "2000-01-22T00:00:00Z")!)
default:
XCTFail()
switch object?.testEntityID {
case NSNumber(value: 101)?:
XCTAssertEqual(indexPath?.section, 1)
XCTAssertEqual(indexPath?.item, 0)
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 11))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "11"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:11")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:11" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, dateFormatter.date(from: "2000-01-11T00:00:00Z")!)
case NSNumber(value: 102)?:
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
XCTAssertEqual(object?.testBoolean, NSNumber(value: false))
XCTAssertEqual(object?.testNumber, NSNumber(value: 22))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "22"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:22")
XCTAssertEqual(object?.testData, ("nil:TestEntity1:22" as NSString).data(using: String.Encoding.utf8.rawValue)!)
XCTAssertEqual(object?.testDate, dateFormatter.date(from: "2000-01-22T00:00:00Z")!)
default:
XCTFail()
}
defer {
events += 1
}
return events == 1 || events == 2
}
defer {
events += 1
}
return events == 1 || events == 2
}
)
_ = self.expectation(
@@ -255,18 +275,21 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 3)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 3)
defer {
events += 1
}
return events == 3
}
return events == 3
}
)
let saveExpectation = self.expectation(description: "save")
stack.perform(
asynchronous: { (transaction) -> Bool in
asynchronous: { [dateFormatter] (transaction) -> Bool in
if let object = try transaction.fetchOne(
From<TestEntity1>(),
@@ -276,7 +299,7 @@ class ListObserverTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "11")
object.testString = "nil:TestEntity1:11"
object.testData = ("nil:TestEntity1:11" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-11T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-11T00:00:00Z")!
}
else {
@@ -290,7 +313,7 @@ class ListObserverTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "22")
object.testString = "nil:TestEntity1:22"
object.testData = ("nil:TestEntity1:22" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-22T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-22T00:00:00Z")!
}
else {
@@ -312,6 +335,7 @@ class ListObserverTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatListObservers_CanReceiveMoveNotifications() {
@@ -327,20 +351,23 @@ class ListObserverTests: BaseTestDataTestCase {
)
monitor.addObserver(observer)
var events = 0
let events: Internals.Mutex<Int> = .init(0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
events += 1
}
return events == 0
}
return events == 0
}
)
_ = self.expectation(
@@ -348,32 +375,35 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 1)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["fromIndexPath", "toIndexPath", "object"]
)
let fromIndexPath = userInfo?["fromIndexPath"] as? IndexPath
XCTAssertEqual(fromIndexPath?.section, 0)
XCTAssertEqual(fromIndexPath?.item, 0)
let toIndexPath = userInfo?["toIndexPath"] as? IndexPath
XCTAssertEqual(toIndexPath?.section, 1)
XCTAssertEqual(toIndexPath?.item, 1)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testEntityID, NSNumber(value: 102))
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 1)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["fromIndexPath", "toIndexPath", "object"]
)
let fromIndexPath = userInfo?["fromIndexPath"] as? IndexPath
XCTAssertEqual(fromIndexPath?.section, 0)
XCTAssertEqual(fromIndexPath?.item, 0)
let toIndexPath = userInfo?["toIndexPath"] as? IndexPath
XCTAssertEqual(toIndexPath?.section, 1)
XCTAssertEqual(toIndexPath?.item, 1)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testEntityID, NSNumber(value: 102))
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
defer {
events += 1
}
return events == 1
}
return events == 1
}
)
_ = self.expectation(
@@ -381,13 +411,16 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 2)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 2)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
events += 1
}
return events == 2
}
return events == 2
}
)
let saveExpectation = self.expectation(description: "save")
@@ -420,6 +453,7 @@ class ListObserverTests: BaseTestDataTestCase {
}
}
@MainActor
@objc
dynamic func test_ThatListObservers_CanReceiveDeleteNotifications() {
@@ -435,20 +469,23 @@ class ListObserverTests: BaseTestDataTestCase {
)
monitor.addObserver(observer)
var events = 0
let events: Internals.Mutex<Int> = .init(0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 0)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
events += 1
}
return events == 0
}
return events == 0
}
)
_ = self.expectation(
@@ -456,28 +493,31 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssert(events == 1 || events == 2)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssert(indexPath?.item == 0 || indexPath?.item == 1)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.isDeleted, true)
defer {
return events.withLock { events in
events += 1
XCTAssert(events == 1 || events == 2)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssert(indexPath?.item == 0 || indexPath?.item == 1)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.isDeleted, true)
defer {
events += 1
}
return events == 1 || events == 2
}
return events == 1 || events == 2
}
)
_ = self.expectation(
@@ -485,27 +525,30 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 3)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["sectionInfo", "sectionIndex"]
)
let sectionInfo = userInfo?["sectionInfo"] as? NSFetchedResultsSectionInfo
XCTAssertNotNil(sectionInfo)
XCTAssertEqual(sectionInfo?.name, "0")
let sectionIndex = userInfo?["sectionIndex"]
XCTAssertEqual(sectionIndex as? NSNumber, NSNumber(value: 0))
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 3)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(userInfo?.keys.map({ $0 as! String }) ?? []),
["sectionInfo", "sectionIndex"]
)
let sectionInfo = userInfo?["sectionInfo"] as? NSFetchedResultsSectionInfo
XCTAssertNotNil(sectionInfo)
XCTAssertEqual(sectionInfo?.name, "0")
let sectionIndex = userInfo?["sectionIndex"]
XCTAssertEqual(sectionIndex as? NSNumber, NSNumber(value: 0))
defer {
events += 1
}
return events == 3
}
return events == 3
}
)
_ = self.expectation(
@@ -513,13 +556,16 @@ class ListObserverTests: BaseTestDataTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 4)
XCTAssertEqual((note.userInfo as NSDictionary?) ?? [:], NSDictionary())
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 4)
defer {
events += 1
}
return events == 4
}
return events == 4
}
)
let saveExpectation = self.expectation(description: "save")
@@ -551,7 +597,7 @@ class ListObserverTests: BaseTestDataTestCase {
// MARK: TestListObserver
class TestListObserver: ListSectionObserver {
final class TestListObserver: ListSectionObserver {
// MARK: ListObserver
+21 -19
View File
@@ -24,7 +24,6 @@
//
import XCTest
import os
@testable
import CoreStore
@@ -56,7 +55,8 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted)
let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
let events: Internals.Mutex<Int> = .init(0)
let persistentID = object.persistentID()
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"),
@@ -68,8 +68,8 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(events, 0)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
["object": object] as NSDictionary
(note.userInfo?["object"] as? TestEntity1)?.persistentID(),
persistentID
)
defer {
@@ -89,16 +89,17 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(events, 1)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
[
"object": object,
"changedPersistentKeys": Set(
[
#keyPath(TestEntity1.testNumber),
#keyPath(TestEntity1.testString)
]
)
] as NSDictionary
(note.userInfo?["object"] as? TestEntity1)?.persistentID(),
persistentID
)
XCTAssertEqual(
note.userInfo?["changedPersistentKeys"] as? Set<String>,
Set(
[
#keyPath(TestEntity1.testNumber),
#keyPath(TestEntity1.testString)
]
)
)
let object = note.userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testNumber, NSNumber(value: 10))
@@ -116,7 +117,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
stack.perform(
asynchronous: { (transaction) -> Bool in
guard let object = transaction.edit(object) else {
guard let object = transaction.edit(persistentID) else {
XCTFail()
try transaction.cancel()
@@ -162,7 +163,8 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted)
let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
let events: Internals.Mutex<Int> = .init(0)
let persistentID = object.persistentID()
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"),
@@ -174,8 +176,8 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(events, 0)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
["object": object] as NSDictionary
(note.userInfo?["object"] as? TestEntity1)?.persistentID(),
persistentID
)
defer {
@@ -189,7 +191,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
stack.perform(
asynchronous: { (transaction) -> Bool in
guard let object = transaction.edit(object) else {
guard let object = transaction.edit(persistentID) else {
XCTFail()
try transaction.cancel()
+4 -2
View File
@@ -63,10 +63,11 @@ class ObjectPublisherTests: BaseTestDataTestCase {
}
let saveExpectation = self.expectation(description: "save")
let persistentID = object.persistentID()
stack.perform(
asynchronous: { (transaction) -> Bool in
guard let object = transaction.edit(object) else {
guard let object = transaction.edit(persistentID) else {
XCTFail()
try transaction.cancel()
@@ -123,10 +124,11 @@ class ObjectPublisherTests: BaseTestDataTestCase {
}
let saveExpectation = self.expectation(description: "save")
let persistentID = object.persistentID()
stack.perform(
asynchronous: { (transaction) -> Bool in
guard let object = transaction.edit(object) else {
guard let object = transaction.edit(persistentID) else {
XCTFail()
try transaction.cancel()
+9
View File
@@ -35,6 +35,7 @@ import CoreStore
class QueryTests: BaseTestDataTestCase {
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryAttributeValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -219,6 +220,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryAverageValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -398,6 +400,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryCountValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -575,6 +578,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryMaximumValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -759,6 +763,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryMinimumValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -943,6 +948,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQuerySumValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -1121,6 +1127,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryObjectIDValue() {
let configurations: [ModelConfiguration] = [nil]
@@ -1289,6 +1296,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryAttributes() {
let configurations: [ModelConfiguration] = [nil]
@@ -1343,6 +1351,7 @@ class QueryTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatDataStacks_CanQueryAggregates() {
let configurations: [ModelConfiguration] = [nil]
+1
View File
@@ -27,6 +27,7 @@ import CoreData
import XCTest
@testable
@_spi(Internals)
import CoreStore
+51 -32
View File
@@ -35,6 +35,7 @@ import CoreStore
final class TransactionTests: BaseTestCase {
@objc
@MainActor
dynamic func test_ThatSynchronousTransactions_CanPerformCRUDs() {
self.prepareStack { (stack) in
@@ -151,6 +152,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatSynchronousTransactions_CanPerformCRUDsInCorrectConfiguration() {
self.prepareStack(configurations: [nil, "Config1"]) { (stack) in
@@ -270,6 +272,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatSynchronousTransactions_CanDiscardUncommittedChanges() {
self.prepareStack { (stack) in
@@ -385,6 +388,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatSynchronousTransactions_CanCommitWithoutWaitingForMerges() {
self.prepareStack { (stack) in
@@ -398,19 +402,22 @@ final class TransactionTests: BaseTestCase {
XCTAssertFalse(monitor.hasObjects())
var events = 0
let events: Internals.Mutex<Int> = .init(0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "listMonitorWillChange:"),
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 0)
XCTAssertTrue(note.userInfo?.isEmpty != false)
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 0)
XCTAssertTrue(note.userInfo?.isEmpty != false)
defer {
events += 1
}
return events == 0
}
return events == 0
}
)
_ = self.expectation(
@@ -418,29 +425,32 @@ final class TransactionTests: BaseTestCase {
object: observer,
handler: { (note) -> Bool in
XCTAssertEqual(events, 1)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(((userInfo as? [String: AnyObject]) ?? [:]).keys),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 1))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "1"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:1")
defer {
return events.withLock { events in
events += 1
XCTAssertEqual(events, 1)
let userInfo = note.userInfo
XCTAssertNotNil(userInfo)
XCTAssertEqual(
Set(((userInfo as? [String: AnyObject]) ?? [:]).keys),
["indexPath", "object"]
)
let indexPath = userInfo?["indexPath"] as? IndexPath
XCTAssertEqual(indexPath?.section, 0)
XCTAssertEqual(indexPath?.item, 0)
let object = userInfo?["object"] as? TestEntity1
XCTAssertEqual(object?.testBoolean, NSNumber(value: true))
XCTAssertEqual(object?.testNumber, NSNumber(value: 1))
XCTAssertEqual(object?.testDecimal, NSDecimalNumber(string: "1"))
XCTAssertEqual(object?.testString, "nil:TestEntity1:1")
defer {
events += 1
}
return events == 1
}
return events == 1
}
)
_ = self.expectation(
@@ -449,11 +459,14 @@ final class TransactionTests: BaseTestCase {
handler: { (note) -> Bool in
XCTAssertTrue(note.userInfo?.isEmpty != false)
defer {
events += 1
return events.withLock { events in
defer {
events += 1
}
return events == 2
}
return events == 2
}
)
let saveExpectation = self.expectation(description: "save")
@@ -479,7 +492,7 @@ final class TransactionTests: BaseTestCase {
XCTFail()
}
XCTAssertEqual(events, 0)
XCTAssertEqual(events.withLock({ $0 }), 0)
XCTAssertEqual(monitor.numberOfObjects(), 0)
self.waitAndCheckExpectations()
}
@@ -487,6 +500,7 @@ final class TransactionTests: BaseTestCase {
@objc
@MainActor
dynamic func test_ThatAsynchronousTransactions_CanPerformCRUDs() {
self.prepareStack { (stack) in
@@ -626,6 +640,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatAsynchronousTransactions_CanPerformCRUDsInCorrectConfiguration() {
self.prepareStack(configurations: [nil, "Config1"]) { (stack) in
@@ -759,6 +774,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatAsynchronousTransactions_CanDiscardUncommittedChanges() {
self.prepareStack { (stack) in
@@ -900,6 +916,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatUnsafeTransactions_CanPerformCRUDs() {
self.prepareStack { (stack) in
@@ -995,6 +1012,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatUnsafeTransactions_CanPerformCRUDsInCorrectConfiguration() {
self.prepareStack(configurations: [nil, "Config1"]) { (stack) in
@@ -1083,6 +1101,7 @@ final class TransactionTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatUnsafeTransactions_CanRollbackChanges() {
self.prepareStack { (stack) in
+2 -3
View File
@@ -37,18 +37,17 @@ final class TweakTests: XCTestCase {
@objc
dynamic func test_ThatTweakClauses_ApplyToFetchRequestsCorrectly() {
let predicate = NSPredicate(format: "%K == %@", "key", "value")
let tweak = Tweak {
$0.fetchOffset = 100
$0.fetchLimit = 200
$0.predicate = predicate
$0.predicate = NSPredicate(format: "%K == %@", "key", "value")
}
let request = Internals.CoreStoreFetchRequest<NSFetchRequestResult>()
tweak.applyToFetchRequest(request)
XCTAssertEqual(request.fetchOffset, 100)
XCTAssertEqual(request.fetchLimit, 200)
XCTAssertNotNil(request.predicate)
XCTAssertEqual(request.predicate, predicate)
XCTAssertEqual(request.predicate, NSPredicate(format: "%K == %@", "key", "value"))
}
}
+10 -16
View File
@@ -25,8 +25,7 @@
B5931B4825124940007DA58E /* Advanced.EvolutionDemo.V4.FromV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4725124940007DA58E /* Advanced.EvolutionDemo.V4.FromV3.swift */; };
B5931B4A25124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4925124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift */; };
B5931B4C251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4B251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift */; };
B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* AppDelegate.swift */; };
B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */; };
B5A3911D24E5429200E7E8BD /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* DemoApp.swift */; };
B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3912024E5429200E7E8BD /* Menu.MainView.swift */; };
B5A3912324E5429300E7E8BD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912224E5429300E7E8BD /* Images.xcassets */; };
B5A3912624E5429300E7E8BD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */; };
@@ -44,7 +43,6 @@
B5A3917524E6990200E7E8BD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917424E6990200E7E8BD /* MapKit.framework */; };
B5A3917724E6990700E7E8BD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917624E6990700E7E8BD /* CoreLocation.framework */; };
B5A3917924E6991600E7E8BD /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917824E6991600E7E8BD /* SwiftUI.framework */; };
B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917B24E6A76C00E7E8BD /* LazyView.swift */; };
B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */; };
B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917F24E787D900E7E8BD /* InstructionsView.swift */; };
B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */; };
@@ -136,8 +134,7 @@
B5931B4925124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.EvolutionDemo.V3.FromV4.swift; sourceTree = "<group>"; };
B5931B4B251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.EvolutionDemo.V2.FromV3.swift; sourceTree = "<group>"; };
B5A3911924E5429200E7E8BD /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
B5A3911C24E5429200E7E8BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
B5A3911C24E5429200E7E8BD /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = "<group>"; };
B5A3912024E5429200E7E8BD /* Menu.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.MainView.swift; sourceTree = "<group>"; };
B5A3912224E5429300E7E8BD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@@ -159,7 +156,6 @@
B5A3917424E6990200E7E8BD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; };
B5A3917624E6990700E7E8BD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; };
B5A3917824E6991600E7E8BD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; };
B5A3917B24E6A76C00E7E8BD /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.Geocoder.swift; sourceTree = "<group>"; };
B5A3917F24E787D900E7E8BD /* InstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsView.swift; sourceTree = "<group>"; };
B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.swift; sourceTree = "<group>"; };
@@ -257,8 +253,7 @@
B5A3911B24E5429200E7E8BD /* Sources */ = {
isa = PBXGroup;
children = (
B5A3911C24E5429200E7E8BD /* AppDelegate.swift */,
B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */,
B5A3911C24E5429200E7E8BD /* DemoApp.swift */,
B5A3915524E6858A00E7E8BD /* Demos */,
B5A3917A24E6A75F00E7E8BD /* Helpers */,
);
@@ -371,7 +366,6 @@
B5A3917A24E6A75F00E7E8BD /* Helpers */ = {
isa = PBXGroup;
children = (
B5A3917B24E6A76C00E7E8BD /* LazyView.swift */,
B5A3917F24E787D900E7E8BD /* InstructionsView.swift */,
B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */,
B5A3915424E6857F00E7E8BD /* Menu */,
@@ -668,14 +662,12 @@
buildActionMask = 2147483647;
files = (
B5A543F624FBF13A000DC5E3 /* Advanced.swift in Sources */,
B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */,
B5A3911D24E5429200E7E8BD /* DemoApp.swift in Sources */,
B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */,
B5E32C9024FA41F9003F46AD /* ImageDownloader.swift in Sources */,
B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */,
B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */,
B5A3913424E6170500E7E8BD /* Menu.swift in Sources */,
B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */,
B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */,
B5A543FB2504840E000DC5E3 /* Advanced.EvolutionDemo.swift in Sources */,
B5A543D724FB7478000DC5E3 /* Classic.ColorsDemo.swift in Sources */,
B5A543DB24FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld in Sources */,
@@ -801,7 +793,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
ONLY_ACTIVE_ARCH = YES;
};
name = Debug;
@@ -837,7 +829,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
};
name = Release;
};
@@ -897,6 +889,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -909,7 +902,7 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -964,6 +957,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -975,7 +969,7 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
-21
View File
@@ -20,29 +20,8 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
-33
View File
@@ -1,33 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import UIKit
// MARK: - AppDelegate
@UIApplicationMain
@objc final class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: UIApplicationDelegate
@objc dynamic func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
return true
}
@objc dynamic func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}
}
+15
View File
@@ -0,0 +1,15 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
@main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
Menu.MainView()
}
}
}
@@ -20,6 +20,7 @@ protocol Advanced_EvolutionDemo_CreatureType: DynamicObject, CustomStringConvert
var dnaCode: Int64 { get set }
@MainActor
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource
static func count(in transaction: BaseDataTransaction) throws -> Int
@@ -3,27 +3,29 @@
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import Combine
import Observation
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.CreaturesDataSource
/**
A type-erasing adapter to support different `ListPublisher` types
*/
final class CreaturesDataSource: ObservableObject {
@MainActor
@Observable
final class CreaturesDataSource {
// MARK: Internal
init<T: NSManagedObject & Advanced.EvolutionDemo.CreatureType>(
init<T: DynamicObject & Advanced.EvolutionDemo.CreatureType>(
listPublisher: ListPublisher<T>,
dataStack: DataStack
) {
self.numberOfItems = {
listPublisher.snapshot.numberOfItems
}
@@ -31,13 +33,13 @@ extension Advanced.EvolutionDemo {
listPublisher.snapshot[index].object?.description
}
self.addItems = { count in
dataStack.perform(
asynchronous: { transaction in
let nextDNACode = try transaction.fetchCount(From<T>())
for offset in 0 ..< count {
let object = transaction.create(Into<T>())
object.dnaCode = .init(nextDNACode + offset)
object.mutate(in: transaction)
@@ -47,11 +49,11 @@ extension Advanced.EvolutionDemo {
)
}
self.mutateItemAtIndex = { index in
let object = listPublisher.snapshot[index]
dataStack.perform(
asynchronous: { transaction in
object
.asEditable(in: transaction)?
.mutate(in: transaction)
@@ -60,109 +62,68 @@ extension Advanced.EvolutionDemo {
)
}
self.deleteAllItems = {
dataStack.perform(
asynchronous: { transaction in
try transaction.deleteAll(From<T>())
},
completion: { _ in }
)
}
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.objectWillChange.send()
listPublisher.addObserver(self) { [weak self] _ in
Task { @MainActor in
self?.refreshID += 1
}
}
}
init<T: CoreStoreObject & Advanced.EvolutionDemo.CreatureType>(
listPublisher: ListPublisher<T>,
dataStack: DataStack
) {
self.numberOfItems = {
listPublisher.snapshot.numberOfItems
}
self.itemDescriptionAtIndex = { index in
listPublisher.snapshot[index].object?.description
}
self.addItems = { count in
dataStack.perform(
asynchronous: { transaction in
let nextDNACode = try transaction.fetchCount(From<T>())
for offset in 0 ..< count {
let object = transaction.create(Into<T>())
object.dnaCode = .init(nextDNACode + offset)
object.mutate(in: transaction)
}
},
completion: { _ in }
)
}
self.mutateItemAtIndex = { index in
let object = listPublisher.snapshot[index]
dataStack.perform(
asynchronous: { transaction in
object
.asEditable(in: transaction)?
.mutate(in: transaction)
},
completion: { _ in }
)
}
self.deleteAllItems = {
dataStack.perform(
asynchronous: { transaction in
try transaction.deleteAll(From<T>())
},
completion: { _ in }
)
}
listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.objectWillChange.send()
}
}
func numberOfCreatures() -> Int {
_ = self.refreshID
return self.numberOfItems()
}
func creatureDescription(at index: Int) -> String? {
_ = self.refreshID
return self.itemDescriptionAtIndex(index)
}
func mutate(at index: Int) {
self.mutateItemAtIndex(index)
}
func add(count: Int) {
self.addItems(count)
}
func clear() {
self.deleteAllItems()
}
// MARK: Private
private var refreshID: Int = 0
@ObservationIgnored
private let numberOfItems: () -> Int
@ObservationIgnored
private let itemDescriptionAtIndex: (Int) -> String?
@ObservationIgnored
private let mutateItemAtIndex: (Int) -> Void
@ObservationIgnored
private let addItems: (Int) -> Void
@ObservationIgnored
private let deleteAllItems: () -> Void
}
}
@@ -7,69 +7,50 @@ import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.ItemView
struct ItemView: View {
// MARK: Internal
init(description: String?, mutate: @escaping () -> Void) {
self.description = description
self.mutate = mutate
}
// MARK: View
var body: some View {
HStack {
Text(self.description ?? "")
.font(.footnote)
.foregroundColor(.primary)
Spacer()
Button(
action: self.mutate,
label: {
Text("Mutate")
.foregroundColor(.accentColor)
.fontWeight(.bold)
}
)
.buttonStyle(PlainButtonStyle())
.buttonStyle(.plain)
}
.disabled(self.description == nil)
}
// MARK: FilePrivate
fileprivate let description: String?
fileprivate let mutate: () -> Void
}
}
#if DEBUG
struct _Demo_Advanced_EvolutionDemo_ItemView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Advanced.EvolutionDemo.ItemView(
description: """
dnaCode: 123
numberOfLimbs: 4
hasVertebrae: true
hasHead: true
hasTail: true
habitat: land
hasWings: false
""",
mutate: {}
)
}
}
#endif
@@ -8,92 +8,51 @@ import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.ListView
struct ListView: View {
// MARK: View
var body: some View {
let dataSource = self.dataSource
return List {
ForEach(0 ..< dataSource.numberOfCreatures(), id: \.self) { (index) in
Advanced.EvolutionDemo.ItemView(
description: dataSource.creatureDescription(at: index),
mutate: {
dataSource.mutate(at: index)
}
)
}
}
.listStyle(PlainListStyle())
}
// MARK: Internal
init(
period: Advanced.EvolutionDemo.GeologicalPeriod,
dataStack: DataStack,
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
) {
self.period = period
self.dataStack = dataStack
self.dataSource = dataSource
}
// MARK: Private
private let period: Advanced.EvolutionDemo.GeologicalPeriod
private let dataStack: DataStack
@ObservedObject
private var dataSource: Advanced.EvolutionDemo.CreaturesDataSource
}
}
#if DEBUG
struct _Demo_Advanced_EvolutionDemo_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let dataStack = DataStack(
CoreStoreSchema(
modelVersion: Advanced.EvolutionDemo.V4.name,
entities: [
Entity<Advanced.EvolutionDemo.V4.Creature>("Creature")
]
)
)
try! dataStack.addStorageAndWait(
SQLiteStore(fileName: "Advanced.EvolutionDemo.ListView.Preview.sqlite")
)
try! dataStack.perform(
synchronous: { transaction in
for dnaCode in 0 ..< 10 as Range<Int64> {
let object = transaction.create(Into<Advanced.EvolutionDemo.V4.Creature>())
object.dnaCode = dnaCode
object.mutate(in: transaction)
// MARK: View
var body: some View {
List {
ForEach(0 ..< self.dataSource.numberOfCreatures(), id: \.self) { index in
Advanced.EvolutionDemo.ItemView(
description: self.dataSource.creatureDescription(at: index),
mutate: {
self.dataSource.mutate(at: index)
}
)
}
}
)
return Advanced.EvolutionDemo.ListView(
period: .ageOfMammals,
dataStack: dataStack,
dataSource: Advanced.EvolutionDemo.V4.Creature.dataSource(in: dataStack)
)
.listStyle(.plain)
}
// MARK: Private
private let period: Advanced.EvolutionDemo.GeologicalPeriod
private let dataStack: DataStack
private let dataSource: Advanced.EvolutionDemo.CreaturesDataSource
}
}
#endif
@@ -8,71 +8,54 @@ import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.MainView
struct MainView: View {
@State
private var migrator = Advanced.EvolutionDemo.Migrator()
// MARK: View
var body: some View {
let migrator = self.migrator
let listView: AnyView
if let current = migrator.current {
listView = AnyView(
VStack(spacing: 0) {
HStack(alignment: .center, spacing: 0) {
Text("Age of")
.padding(.trailing)
Picker(selection: $migrator.currentPeriod, label: EmptyView()) {
ForEach(Advanced.EvolutionDemo.GeologicalPeriod.allCases, id: \.self) { period in
Text(period.description).tag(period)
}
}
.pickerStyle(.segmented)
}
.padding()
if let current = migrator.current {
Advanced.EvolutionDemo.ListView(
period: current.period,
dataStack: current.dataStack,
dataSource: current.dataSource
)
)
}
else {
listView = AnyView(
Advanced.EvolutionDemo.ProgressView(progress: migrator.progress)
)
}
return VStack(spacing: 0) {
HStack(alignment: .center, spacing: 0) {
Text("Age of")
.padding(.trailing)
Picker(selection: self.$migrator.currentPeriod, label: EmptyView()) {
ForEach(Advanced.EvolutionDemo.GeologicalPeriod.allCases, id: \.self) { period in
Text(period.description).tag(period)
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
listView
.edgesIgnoringSafeArea(.vertical)
.ignoresSafeArea(.container, edges: .vertical)
}
.navigationBarTitle("Evolution")
.disabled(migrator.isBusy || migrator.current == nil)
else {
Advanced.EvolutionDemo.ProgressView(progress: migrator.progress)
.ignoresSafeArea(.container, edges: .vertical)
}
}
.navigationTitle("Evolution")
.disabled(migrator.isBusy || migrator.current == nil)
}
// MARK: Private
@ObservedObject
private var migrator: Advanced.EvolutionDemo.Migrator = .init()
}
}
#if DEBUG
struct _Demo_Advanced_EvolutionDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Advanced.EvolutionDemo.MainView()
}
}
#endif
@@ -4,7 +4,7 @@
import CoreStore
import Foundation
import Combine
import Observation
// MARK: - Advanced.EvolutionDemo
@@ -13,8 +13,10 @@ extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.Migrator
final class Migrator: ObservableObject {
@MainActor
@Observable
final class Migrator {
/**
Sample 1: Creating a complex `DataStack` that contains all schema histories. The `exactCurrentModelVersion` will specify the target version (if required), and `migrationChain` will provide the upgrade/downgrade progressive migration path.
*/
@@ -22,7 +24,7 @@ extension Advanced.EvolutionDemo {
exactCurrentModelVersion: ModelVersion?,
migrationChain: MigrationChain
) -> DataStack {
let xcodeV1ToV2ModelSchema = XcodeDataModelSchema.from(
modelName: "Advanced.EvolutionDemo.V1",
bundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self)
@@ -49,12 +51,12 @@ extension Advanced.EvolutionDemo {
)
)
}
/**
Sample 2: Creating a complex `SQLiteStore` that contains all schema mappings for both upgrade and downgrade cases.
*/
private func accessSQLiteStore() -> SQLiteStore {
let upgradeMappings: [SchemaMappingProvider] = [
Advanced.EvolutionDemo.V2.FromV1.mapping,
Advanced.EvolutionDemo.V3.FromV2.mapping,
@@ -72,15 +74,15 @@ extension Advanced.EvolutionDemo {
localStorageOptions: []
)
}
/**
Sample 3: Find the model version used by an existing `SQLiteStore`, or just return the latest version if the store is not created yet.
*/
private func findCurrentVersion() -> ModelVersion {
let allVersions = Advanced.EvolutionDemo.GeologicalPeriod.allCases
.map({ $0.version })
.map(\.version)
// Since we are only interested in finding current version, we'll assume an upgrading `MigrationChain`
let dataStack = self.createDataStack(
exactCurrentModelVersion: nil,
@@ -89,42 +91,36 @@ extension Advanced.EvolutionDemo {
let migrations = try! dataStack.requiredMigrationsForStorage(
self.accessSQLiteStore()
)
// If no migrations are needed, it means either the store is not created yet, or the store is already at the latest model version. In either case, we already know that the store will use the latest version
return migrations.first?.sourceVersion
?? allVersions.last!
?? allVersions.last!
}
// MARK: Internal
var currentPeriod: Advanced.EvolutionDemo.GeologicalPeriod = Advanced.EvolutionDemo.GeologicalPeriod.allCases.last! {
didSet {
self.selectModelVersion(self.currentPeriod)
}
}
private(set) var current: (
period: Advanced.EvolutionDemo.GeologicalPeriod,
dataStack: DataStack,
dataSource: Advanced.EvolutionDemo.CreaturesDataSource
)? {
willSet {
self.objectWillChange.send()
}
}
)?
private(set) var isBusy: Bool = false
private(set) var progress: Progress?
init() {
self.synchronizeCurrentVersion()
}
@@ -132,11 +128,11 @@ extension Advanced.EvolutionDemo {
// MARK: Private
private func synchronizeCurrentVersion() {
guard
let currentPeriod = Advanced.EvolutionDemo.GeologicalPeriod(rawValue: self.findCurrentVersion())
else {
self.selectModelVersion(self.currentPeriod)
return
}
@@ -144,106 +140,106 @@ extension Advanced.EvolutionDemo {
}
private func selectModelVersion(_ period: Advanced.EvolutionDemo.GeologicalPeriod) {
let currentPeriod = self.current?.period
guard period != currentPeriod else {
return
}
self.objectWillChange.send()
self.isBusy = true
// explicitly trigger `NSPersistentStore` cleanup by deallocating the `DataStack`
self.current = nil
let migrationChain: MigrationChain
switch (currentPeriod?.version, period.version) {
case (nil, let newVersion):
migrationChain = [newVersion]
case (let currentVersion?, let newVersion):
let upgradeMigrationChain = Advanced.EvolutionDemo.GeologicalPeriod.allCases
.map({ $0.version })
.map(\.version)
let currentVersionIndex = upgradeMigrationChain.firstIndex(of: currentVersion)!
let newVersionIndex = upgradeMigrationChain.firstIndex(of: newVersion)!
migrationChain = MigrationChain(
currentVersionIndex > newVersionIndex
? upgradeMigrationChain.reversed()
: upgradeMigrationChain
? upgradeMigrationChain.reversed()
: upgradeMigrationChain
)
}
let dataStack = self.createDataStack(
exactCurrentModelVersion: period.version,
migrationChain: migrationChain
)
let completion = { [weak self] () -> Void in
guard let self = self else {
return
}
self.objectWillChange.send()
defer {
self.isBusy = false
}
self.current = (
period: period,
dataStack: dataStack,
dataSource: period.creatureType.dataSource(in: dataStack)
)
self.currentPeriod = period
}
self.progress = dataStack.addStorage(
self.accessSQLiteStore(),
completion: { [weak self] result in
guard let self = self else {
guard let self else {
return
}
guard case .success = result else {
self.objectWillChange.send()
self.isBusy = false
return
}
if self.progress == nil {
self.spawnCreatures(in: dataStack, period: period, completion: completion)
self.spawnCreatures(in: dataStack, period: period) { [weak self] in
self?.completeSelection(period: period, dataStack: dataStack)
}
}
else {
completion()
self.completeSelection(period: period, dataStack: dataStack)
}
}
)
}
private func completeSelection(
period: Advanced.EvolutionDemo.GeologicalPeriod,
dataStack: DataStack
) {
defer {
self.isBusy = false
}
self.current = (
period: period,
dataStack: dataStack,
dataSource: period.creatureType.dataSource(in: dataStack)
)
self.currentPeriod = period
self.progress = nil
}
private func spawnCreatures(
in dataStack: DataStack,
period: Advanced.EvolutionDemo.GeologicalPeriod,
completion: @escaping () -> Void
completion: @escaping @MainActor @Sendable () -> Void
) {
dataStack.perform(
asynchronous: { (transaction) in
asynchronous: { transaction in
let creatureType = period.creatureType
for dnaCode in try creatureType.count(in: transaction) ..< 10000 {
let object = creatureType.create(in: transaction)
object.dnaCode = Int64(dnaCode)
object.mutate(in: transaction)
}
},
completion: { _ in completion() }
completion: { _ in
completion()
}
)
}
@@ -2,125 +2,131 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Observation
import SwiftUI
// MARK: - Advanced.EvolutionDemo
extension Advanced.EvolutionDemo {
// MARK: - Advanced.EvolutionDemo.ProgressView
struct ProgressView: View {
// MARK: Internal
init(progress: Progress?) {
self.progressObserver = .init(progress)
self.progress = progress
self._progressObserver = State(initialValue: .init(progress))
}
// MARK: View
var body: some View {
guard self.progressObserver.isMigrating else {
return AnyView(
Group {
if self.progressObserver.isMigrating {
VStack(alignment: .leading) {
Text("Migrating: \(self.progressObserver.localizedDescription)")
.font(.headline)
.padding([.top, .horizontal])
Text("Progressive step: \(self.progressObserver.localizedAdditionalDescription)")
.font(.subheadline)
.padding(.horizontal)
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.gray.opacity(0.2))
.frame(width: geometry.size.width, height: 8)
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.blue)
.frame(
width: geometry.size.width
* self.progressObserver.fractionCompleted,
height: 8
)
}
}
.fixedSize(horizontal: false, vertical: true)
.padding()
Spacer()
}
.padding()
}
else {
VStack(alignment: .center) {
Text("Preparing creatures...")
.padding()
Spacer()
}
.padding()
)
}
return AnyView(
VStack(alignment: .leading) {
Text("Migrating: \(self.progressObserver.localizedDescription)")
.font(.headline)
.padding([.top, .horizontal])
Text("Progressive step: \(self.progressObserver.localizedAdditionalDescription)")
.font(.subheadline)
.padding(.horizontal)
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.gray.opacity(0.2))
.frame(width: geometry.size.width, height: 8)
RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(Color.blue)
.frame(
width: geometry.size.width
* self.progressObserver.fractionCompleted,
height: 8
)
}
}
.fixedSize(horizontal: false, vertical: true)
.padding()
Spacer()
}
.padding()
)
}
.task(id: self.progress.map { ObjectIdentifier($0) }) {
self.progressObserver = .init(self.progress)
}
}
// MARK: FilePrivate
@ObservedObject
// MARK: Private
private let progress: Progress?
@State
private var progressObserver: ProgressObserver
// MARK: - ProgressObserver
fileprivate final class ProgressObserver: ObservableObject {
@MainActor
@Observable
fileprivate final class ProgressObserver {
private(set) var fractionCompleted: CGFloat = 0
private(set) var localizedDescription: String = ""
private(set) var localizedAdditionalDescription: String = ""
var isMigrating: Bool {
return self.progress != nil
self.progress != nil
}
init(_ progress: Progress?) {
self.progress = progress
progress?.setProgressHandler { [weak self] (progess) in
guard let self = self else {
return
}
self.objectWillChange.send()
self.fractionCompleted = CGFloat(progress?.fractionCompleted ?? 0)
self.localizedDescription = progress?.localizedDescription ?? ""
self.localizedAdditionalDescription = progress?.localizedAdditionalDescription ?? ""
self.syncValues(from: progress)
progress?.setProgressHandler { [weak self] progress in
self?.syncValues(from: progress)
}
}
// MARK: Private
@ObservationIgnored
private let progress: Progress?
private func syncValues(from progress: Progress?) {
self.fractionCompleted = CGFloat(progress?.fractionCompleted ?? 0)
self.localizedDescription = progress?.localizedDescription ?? ""
self.localizedAdditionalDescription = progress?.localizedAdditionalDescription ?? ""
}
}
}
}
#if DEBUG
struct _Demo_Advanced_EvolutionDemo_ProgressView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let progress = Progress(totalUnitCount: 10)
progress.completedUnitCount = 3
return Advanced.EvolutionDemo.ProgressView(
progress: progress
)
}
}
#endif
@@ -10,7 +10,7 @@ import CoreStore
@objc(Advanced_EvolutionDemo_V1_Creature)
final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
@NSManaged
dynamic var dnaCode: Int64
@@ -30,9 +30,10 @@ final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.Evolut
// MARK: Advanced.EvolutionDemo.CreatureType
@MainActor
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V1.Creature>()
@@ -41,16 +42,16 @@ final class Advanced_EvolutionDemo_V1_Creature: NSManagedObject, Advanced.Evolut
dataStack: dataStack
)
}
static func count(in transaction: BaseDataTransaction) throws -> Int {
return try transaction.fetchCount(
From<Advanced.EvolutionDemo.V1.Creature>()
)
}
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V1.Creature {
return transaction.create(
Into<Advanced.EvolutionDemo.V1.Creature>()
)
@@ -10,7 +10,7 @@ import CoreStore
@objc(Advanced_EvolutionDemo_V2_Creature)
final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.EvolutionDemo.CreatureType {
@NSManaged
dynamic var dnaCode: Int64
@@ -42,9 +42,10 @@ final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.Evolut
// MARK: Advanced.EvolutionDemo.CreatureType
@MainActor
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V2.Creature>()
@@ -53,16 +54,16 @@ final class Advanced_EvolutionDemo_V2_Creature: NSManagedObject, Advanced.Evolut
dataStack: dataStack
)
}
static func count(in transaction: BaseDataTransaction) throws -> Int {
return try transaction.fetchCount(
From<Advanced.EvolutionDemo.V2.Creature>()
)
}
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V2.Creature {
return transaction.create(
Into<Advanced.EvolutionDemo.V2.Creature>()
)
@@ -8,31 +8,31 @@ import CoreStore
// MARK: - Advanced.EvolutionDemo.V3
extension Advanced.EvolutionDemo.V3 {
// MARK: - Advanced.EvolutionDemo.V3.Creature
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
// MARK: Internal
@Field.Stored("dnaCode")
var dnaCode: Int64 = 0
@Field.Stored("numberOfLimbs")
var numberOfLimbs: Int32 = 0
@Field.Stored("hasVertebrae")
var hasVertebrae: Bool = false
@Field.Stored("hasHead")
var hasHead: Bool = true
@Field.Stored("hasTail")
var hasTail: Bool = true
@Field.Stored("hasWings")
var hasWings: Bool = false
@Field.Stored("habitat")
var habitat: Habitat = .water
@@ -64,9 +64,10 @@ extension Advanced.EvolutionDemo.V3 {
// MARK: Advanced.EvolutionDemo.CreatureType
@MainActor
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V3.Creature>()
@@ -75,16 +76,16 @@ extension Advanced.EvolutionDemo.V3 {
dataStack: dataStack
)
}
static func count(in transaction: BaseDataTransaction) throws -> Int {
return try transaction.fetchCount(
From<Advanced.EvolutionDemo.V3.Creature>()
)
}
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V3.Creature {
return transaction.create(
Into<Advanced.EvolutionDemo.V3.Creature>()
)
@@ -8,37 +8,37 @@ import CoreStore
// MARK: - Advanced.EvolutionDemo.V4
extension Advanced.EvolutionDemo.V4 {
// MARK: - Advanced.EvolutionDemo.V4.Creature
final class Creature: CoreStoreObject, Advanced.EvolutionDemo.CreatureType {
// MARK: Internal
@Field.Stored("dnaCode")
var dnaCode: Int64 = 0
@Field.Stored("numberOfLimbs")
var numberOfLimbs: Int32 = 0
@Field.Stored("hasVertebrae")
var hasVertebrae: Bool = false
@Field.Stored("hasHead")
var hasHead: Bool = true
@Field.Stored("hasTail")
var hasTail: Bool = false
@Field.Stored("hasWings")
var hasWings: Bool = false
typealias Habitat = Advanced.EvolutionDemo.V3.Creature.Habitat
@Field.Stored("habitat")
var habitat: Habitat = .water
@Field.Stored("isWarmBlooded")
var isWarmBlooded: Bool = true
@@ -60,9 +60,10 @@ extension Advanced.EvolutionDemo.V4 {
// MARK: Advanced.EvolutionDemo.CreatureType
@MainActor
static func dataSource(in dataStack: DataStack) -> Advanced.EvolutionDemo.CreaturesDataSource {
return .init(
listPublisher: dataStack.publishList(
From<Advanced.EvolutionDemo.V4.Creature>()
@@ -71,16 +72,16 @@ extension Advanced.EvolutionDemo.V4 {
dataStack: dataStack
)
}
static func count(in transaction: BaseDataTransaction) throws -> Int {
return try transaction.fetchCount(
From<Advanced.EvolutionDemo.V4.Creature>()
)
}
static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V4.Creature {
return transaction.create(
Into<Advanced.EvolutionDemo.V4.Creature>()
)
@@ -2,7 +2,6 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
@@ -22,19 +21,19 @@ extension Classic.ColorsDemo {
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Classic.ColorsDemo.DetailViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(self.palette)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.palette = self.palette
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
func makeCoordinator() -> ObjectMonitor<Classic.ColorsDemo.Palette> {
@@ -48,32 +47,3 @@ extension Classic.ColorsDemo {
private let palette: ObjectMonitor<Classic.ColorsDemo.Palette>
}
}
#if DEBUG
struct _Demo_Classic_ColorsDemo_DetailView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
return
}
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
)
return Classic.ColorsDemo.DetailView(
Classic.ColorsDemo.dataStack.monitorObject(
Classic.ColorsDemo.palettesMonitor[0, 0]
)
)
}
}
#endif
@@ -55,35 +55,29 @@ extension Classic.ColorsDemo {
}
}
#if DEBUG
struct _Demo_Classic_ColorsDemo_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
// MARK: - Preview
let missing = minimumSamples
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
palette.setRandomHue()
}
#Preview {
let minimumSamples = 10
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
)
return Classic.ColorsDemo.ListView(
listMonitor: Classic.ColorsDemo.palettesMonitor,
onPaletteTapped: { _ in }
)
}
}
for _ in 0..<missing {
#endif
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Classic.ColorsDemo.ListView(
listMonitor: Classic.ColorsDemo.palettesMonitor,
onPaletteTapped: { _ in }
)
}
@@ -3,6 +3,7 @@
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreStore
import Observation
import SwiftUI
// MARK: - Classic.ColorsDemo
@@ -12,97 +13,85 @@ extension Classic.ColorsDemo {
// MARK: - Classic.ColorsDemo.MainView
struct MainView: View {
// MARK: Internal
init() {
let listMonitor = Classic.ColorsDemo.palettesMonitor
self.listMonitor = listMonitor
self.listHelper = .init(listMonitor: listMonitor)
self._listHelper = State(initialValue: .init(listMonitor: listMonitor))
self._filter = Binding(
get: { Classic.ColorsDemo.filter },
set: { Classic.ColorsDemo.filter = $0 }
)
}
// MARK: View
var body: some View {
let detailView: AnyView
if let selectedObject = self.listHelper.selectedObject() {
detailView = AnyView(
Classic.ColorsDemo.DetailView(selectedObject)
VStack(spacing: 0) {
Classic.ColorsDemo.ListView(
listMonitor: self.listMonitor,
onPaletteTapped: {
self.listHelper.setSelectedPalette($0)
}
)
}
else {
detailView = AnyView(EmptyView())
}
let listMonitor = self.listMonitor
return VStack(spacing: 0) {
Classic.ColorsDemo.ListView
.init(
listMonitor: listMonitor,
onPaletteTapped: {
self.listHelper.setSelectedPalette($0)
}
)
.navigationBarTitle(
Text("Colors (\(self.listHelper.count) objects)")
)
.frame(minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.vertical)
detailView
.edgesIgnoringSafeArea(.all)
.frame(minHeight: 0, maxHeight: .infinity)
}
.navigationBarItems(
leading: HStack {
EditButton()
Button(
action: { self.clearColors() },
label: { Text("Clear") }
)
},
trailing: HStack {
Button(
action: { self.changeFilter() },
label: { Text(self.filter.rawValue) }
)
Button(
action: { self.shuffleColors() },
label: { Text("Shuffle") }
)
Button(
action: { self.addColor() },
label: { Text("Add") }
)
.frame(minHeight: 0, maxHeight: .infinity)
.ignoresSafeArea(.container, edges: .vertical)
if let selectedObject = self.listHelper.selectedObject() {
Classic.ColorsDemo.DetailView(selectedObject)
.ignoresSafeArea()
.frame(minHeight: 0, maxHeight: .infinity)
}
)
}
.navigationTitle("Colors (\(self.listHelper.count) objects)")
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
EditButton()
Button("Clear") {
self.clearColors()
}
}
ToolbarItemGroup(placement: .topBarTrailing) {
Button(self.filter.rawValue) {
self.changeFilter()
}
Button("Shuffle") {
self.shuffleColors()
}
Button("Add") {
self.addColor()
}
}
}
}
// MARK: Private
private let listMonitor: ListMonitor<Classic.ColorsDemo.Palette>
@ObservedObject
@State
private var listHelper: ListHelper
@Binding
private var filter: Classic.ColorsDemo.Filter
private func changeFilter() {
Classic.ColorsDemo.filter = Classic.ColorsDemo.filter.next()
}
private func clearColors() {
Classic.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
@@ -111,7 +100,7 @@ extension Classic.ColorsDemo {
completion: { _ in }
)
}
private func addColor() {
Classic.ColorsDemo.dataStack.perform(
@@ -122,7 +111,7 @@ extension Classic.ColorsDemo {
completion: { _ in }
)
}
private func shuffleColors() {
Classic.ColorsDemo.dataStack.perform(
@@ -136,112 +125,93 @@ extension Classic.ColorsDemo {
completion: { _ in }
)
}
// MARK: - Classic.ColorsDemo.MainView.ListHelper
fileprivate final class ListHelper: ObservableObject, ListObjectObserver {
@MainActor
@Observable
fileprivate final class ListHelper: ListObjectObserver {
// MARK: FilePrivate
fileprivate private(set) var count: Int = 0
fileprivate init(listMonitor: ListMonitor<Classic.ColorsDemo.Palette>) {
listMonitor.addObserver(self)
self.count = listMonitor.numberOfObjects()
}
fileprivate func selectedObject() -> ObjectMonitor<Classic.ColorsDemo.Palette>? {
return self.selectedPalette.flatMap {
self.selectedPalette.flatMap {
guard !$0.isDeleted else {
return nil
}
return Classic.ColorsDemo.dataStack.monitorObject($0)
}
}
fileprivate func setSelectedPalette(_ palette: Classic.ColorsDemo.Palette?) {
guard self.selectedPalette != palette else {
return
}
self.objectWillChange.send()
if let palette = palette, !palette.isDeleted {
if let palette, !palette.isDeleted {
self.selectedPalette = palette
}
else {
self.selectedPalette = nil
}
}
// MARK: ListObserver
typealias ListEntityType = Classic.ColorsDemo.Palette
func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
self.objectWillChange.send()
self.count = monitor.numberOfObjects()
}
func listMonitorDidRefetch(_ monitor: ListMonitor<ListEntityType>) {
self.objectWillChange.send()
self.count = monitor.numberOfObjects()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>, didDeleteObject object: Classic.ColorsDemo.Palette, fromIndexPath indexPath: IndexPath) {
if self.selectedPalette == object {
self.setSelectedPalette(nil)
// MARK: ListObserver
typealias ListEntityType = Classic.ColorsDemo.Palette
nonisolated func listMonitorDidChange(_ monitor: ListMonitor<Classic.ColorsDemo.Palette>) {
let count = monitor.numberOfObjects()
Task { @MainActor in
self.count = count
}
}
nonisolated func listMonitorDidRefetch(_ monitor: ListMonitor<ListEntityType>) {
let count = monitor.numberOfObjects()
Task { @MainActor in
self.count = count
}
}
// MARK: ListObjectObserver
nonisolated func listMonitor(
_ monitor: ListMonitor<Classic.ColorsDemo.Palette>,
didDeleteObject object: Classic.ColorsDemo.Palette,
fromIndexPath indexPath: IndexPath
) {
let deletedObjectURI = object.objectID.uriRepresentation()
Task { @MainActor in
if self.selectedPalette?.objectID.uriRepresentation() == deletedObjectURI {
self.setSelectedPalette(nil)
}
}
}
// MARK: Private
private var selectedPalette: Classic.ColorsDemo.Palette?
}
}
}
#if DEBUG
struct _Demo_Classic_ColorsDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Classic.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Classic.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Classic.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Classic.ColorsDemo.MainView()
}
}
#endif
@@ -38,6 +38,7 @@ extension Classic {
return dataStack
}()
@MainActor
static let palettesMonitor: ListMonitor<Classic.ColorsDemo.Palette> = Classic.ColorsDemo.dataStack.monitorSectionedList(
From<Classic.ColorsDemo.Palette>()
.sectionBy(\.colorGroup)
@@ -45,6 +46,7 @@ extension Classic {
.orderBy(.ascending(\.hue))
)
@MainActor
static var filter: Classic.ColorsDemo.Filter = .all {
didSet {
@@ -10,7 +10,7 @@ import SwiftUI
extension Modern.ColorsDemo {
// MARK: - Modern.ColorsDemo.MainView
struct MainView<ListView: View, DetailView: View>: View {
// MARK: Internal
@@ -20,7 +20,8 @@ extension Modern.ColorsDemo {
_ listPublisher: ListPublisher<Modern.ColorsDemo.Palette>,
_ onPaletteTapped: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> Void
) -> ListView,
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView) {
detailView: @escaping (ObjectPublisher<Modern.ColorsDemo.Palette>) -> DetailView
) {
self.listView = listView
self.detailView = detailView
@@ -30,42 +31,51 @@ extension Modern.ColorsDemo {
// MARK: View
var body: some View {
return VStack(spacing: 0) {
self.listView(self.$palettes, { self.selectedPalette = $0 })
.navigationBarTitle(
Text("Colors (\(self.palettes.count) objects)")
)
.frame(minHeight: 0, maxHeight: .infinity)
self.selectedPalette.map {
VStack(spacing: 0) {
self.listView(
self.$palettes,
{
self.selectedPalette = $0
}
)
.frame(minHeight: 0, maxHeight: .infinity)
if let selectedPalette = self.selectedPalette {
self.detailView($0)
.edgesIgnoringSafeArea(.all)
self.detailView(selectedPalette)
.ignoresSafeArea()
.frame(minHeight: 0, maxHeight: .infinity)
}
}
.navigationBarItems(
leading: HStack {
.navigationTitle("Colors (\(self.palettes.count) objects)")
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
EditButton()
Button(
action: { self.clearColors() },
label: { Text("Clear") }
)
},
trailing: HStack {
Button(
action: { self.changeFilter() },
label: { Text(self.filter.rawValue) }
)
Button(
action: { self.shuffleColors() },
label: { Text("Shuffle") }
)
Button(
action: { self.addColor() },
label: { Text("Add") }
)
Button("Clear") {
self.clearColors()
}
}
)
ToolbarItemGroup(placement: .topBarTrailing) {
Button(self.filter.rawValue) {
self.changeFilter()
}
Button("Shuffle") {
self.shuffleColors()
}
Button("Add") {
self.addColor()
}
}
}
}
@@ -99,7 +109,7 @@ extension Modern.ColorsDemo {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
try transaction.deleteAll(From<Modern.ColorsDemo.Palette>())
},
sourceIdentifier: TransactionSource.clear,
@@ -108,10 +118,10 @@ extension Modern.ColorsDemo {
}
private func addColor() {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
_ = transaction.create(Into<Modern.ColorsDemo.Palette>())
},
sourceIdentifier: TransactionSource.add,
@@ -120,12 +130,12 @@ extension Modern.ColorsDemo {
}
private func shuffleColors() {
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
for palette in try transaction.fetchAll(From<Modern.ColorsDemo.Palette>()) {
palette.setRandomHue()
}
},
@@ -136,42 +146,38 @@ extension Modern.ColorsDemo {
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
// MARK: - Preview
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
#Preview {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
)
return Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
},
detailView: { objectPublisher in
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
)
}
}
)
return Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
},
detailView: { objectPublisher in
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
}
)
}
#endif
@@ -2,7 +2,6 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
@@ -31,9 +30,9 @@ extension Modern.ColorsDemo.SwiftUI {
@Binding
private var brightness: Float
init(_ palette: ObjectPublisher<Modern.ColorsDemo.Palette>) {
self._palette = .init(palette)
self._hue = Binding(
get: { palette.hue ?? 0 },
@@ -78,7 +77,7 @@ extension Modern.ColorsDemo.SwiftUI {
}
)
}
// MARK: View
@@ -87,12 +86,17 @@ extension Modern.ColorsDemo.SwiftUI {
if let palette = self.palette {
ZStack(alignment: .center) {
Color(palette.$color)
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.white)
.shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1)
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("H: \(Int(palette.$hue * 359))°")
.frame(width: 80)
@@ -102,6 +106,7 @@ extension Modern.ColorsDemo.SwiftUI {
step: 1 / 359
)
}
HStack {
Text("S: \(Int(palette.$saturation * 100))%")
.frame(width: 80)
@@ -111,6 +116,7 @@ extension Modern.ColorsDemo.SwiftUI {
step: 1 / 100
)
}
HStack {
Text("B: \(Int(palette.$brightness * 100))%")
.frame(width: 80)
@@ -131,30 +137,3 @@ extension Modern.ColorsDemo.SwiftUI {
}
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_SwiftUI_DetailView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
return
}
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
)
return Modern.ColorsDemo.SwiftUI.DetailView(
Modern.ColorsDemo.palettesPublisher.snapshot.first!
)
}
}
#endif
@@ -34,41 +34,15 @@ extension Modern.ColorsDemo.SwiftUI {
if let palette = self.palette {
Color(palette.$color).overlay(
Text(palette.$colorText)
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
.padding(),
alignment: .leading
)
.animation(.default, value: palette)
Color(palette.$color)
.overlay(
Text(palette.$colorText)
.foregroundColor(palette.$brightness > 0.6 ? .black : .white)
.padding(),
alignment: .leading
)
.animation(.default, value: palette)
}
}
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_SwiftUI_ItemView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
return
}
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
)
return Modern.ColorsDemo.SwiftUI.ItemView(
Modern.ColorsDemo.palettesPublisher.snapshot.first!
)
}
}
#endif
@@ -10,7 +10,7 @@ import SwiftUI
extension Modern.ColorsDemo.SwiftUI {
// MARK: - Modern.ColorsDemo.SwiftUI.ListView
struct ListView: View {
/**
@@ -40,7 +40,7 @@ extension Modern.ColorsDemo.SwiftUI {
ForEach(sectionIn: self.palettes) { section in
Section(header: Text(section.sectionID)) {
Section(section.sectionID) {
ForEach(objectIn: section) { palette in
@@ -64,8 +64,7 @@ extension Modern.ColorsDemo.SwiftUI {
}
}
// .animation(.default) // breaks layout
.listStyle(PlainListStyle())
.edgesIgnoringSafeArea([])
.listStyle(.plain)
}
@@ -81,7 +80,7 @@ extension Modern.ColorsDemo.SwiftUI {
)
Modern.ColorsDemo.dataStack.perform(
asynchronous: { transaction in
transaction.delete(objectIDs: objectIDsToDelete)
},
completion: { _ in }
@@ -90,35 +89,29 @@ extension Modern.ColorsDemo.SwiftUI {
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_SwiftUI_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
// MARK: - Preview
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
#Preview {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
)
return Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
#endif
@@ -2,7 +2,6 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
@@ -22,21 +21,21 @@ extension Modern.ColorsDemo.UIKit {
}
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.DetailViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(self.palette)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.palette = Modern.ColorsDemo.dataStack.monitorObject(
self.palette.object!
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
@@ -45,30 +44,3 @@ extension Modern.ColorsDemo.UIKit {
private var palette: ObjectPublisher<Modern.ColorsDemo.Palette>
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_UIKit_DetailView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
guard (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>())) <= 0 else {
return
}
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
)
return Modern.ColorsDemo.UIKit.DetailView(
Modern.ColorsDemo.palettesPublisher.snapshot.first!
)
}
}
#endif
@@ -49,7 +49,7 @@ extension Modern.ColorsDemo.UIKit {
}
}
deinit {
isolated deinit {
self.palette.removeObserver(self)
}
@@ -88,14 +88,17 @@ extension Modern.ColorsDemo.UIKit {
// MARK: ObjectObserver
func objectMonitor(
nonisolated func objectMonitor(
_ monitor: ObjectMonitor<Modern.ColorsDemo.Palette>,
didUpdateObject object: Modern.ColorsDemo.Palette,
didUpdateObject object: sending Modern.ColorsDemo.Palette,
changedPersistentKeys: Set<KeyPathString>,
sourceIdentifier: Any?
) {
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
MainActor.assumeIsolated {
self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys)
}
}
@@ -245,6 +248,7 @@ extension Modern.ColorsDemo.UIKit {
private let saturationSlider: UISlider = .init()
private let brightnessSlider: UISlider = .init()
@MainActor
@objc
private dynamic func hueSliderValueDidChange(_ sender: UISlider) {
@@ -259,6 +263,7 @@ extension Modern.ColorsDemo.UIKit {
)
}
@MainActor
@objc
private dynamic func saturationSliderValueDidChange(_ sender: UISlider) {
@@ -273,6 +278,7 @@ extension Modern.ColorsDemo.UIKit {
)
}
@MainActor
@objc
private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) {
@@ -10,7 +10,7 @@ import SwiftUI
extension Modern.ColorsDemo.UIKit {
// MARK: - Modern.ColorsDemo.UIKit.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
@@ -26,9 +26,9 @@ extension Modern.ColorsDemo.UIKit {
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = Modern.ColorsDemo.UIKit.ListViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIViewControllerType(
@@ -36,7 +36,7 @@ extension Modern.ColorsDemo.UIKit {
onPaletteTapped: self.onPaletteTapped
)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {
uiViewController.setEditing(
@@ -44,7 +44,7 @@ extension Modern.ColorsDemo.UIKit {
animated: true
)
}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
@@ -55,35 +55,29 @@ extension Modern.ColorsDemo.UIKit {
}
}
#if DEBUG
struct _Demo_Modern_ColorsDemo_UIKit_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
// MARK: - Preview
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
#Preview {
let minimumSamples = 10
try! Modern.ColorsDemo.dataStack.perform(
synchronous: { transaction in
let missing = minimumSamples
- (try transaction.fetchCount(From<Modern.ColorsDemo.Palette>()))
guard missing > 0 else {
return
}
)
return Modern.ColorsDemo.UIKit.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
for _ in 0..<missing {
let palette = transaction.create(Into<Modern.ColorsDemo.Palette>())
palette.setRandomHue()
}
}
)
return Modern.ColorsDemo.UIKit.ListView(
listPublisher: Modern.ColorsDemo.palettesPublisher,
onPaletteTapped: { _ in }
)
}
#endif
@@ -42,13 +42,13 @@ extension Modern.ColorsDemo.UIKit {
switch transactionSource as? Modern.ColorsDemo.TransactionSource {
case .add,
.delete,
.shuffle,
.clear:
.delete,
.shuffle,
.clear:
dataSource.apply(listPublisher.snapshot, animatingDifferences: true)
case nil,
.refetch:
.refetch:
dataSource.apply(listPublisher.snapshot, animatingDifferences: false)
}
}
@@ -57,22 +57,22 @@ extension Modern.ColorsDemo.UIKit {
/**
Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically.
*/
deinit {
isolated deinit {
self.listPublisher.removeObserver(self)
}
/**
Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures and section index titles on the `UITableView`.
*/
final class CustomDataSource: DiffableDataSource.TableViewAdapter<Modern.ColorsDemo.Palette> {
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
guard let itemID = self.itemID(for: indexPath) else {
@@ -80,13 +80,13 @@ extension Modern.ColorsDemo.UIKit {
}
self.dataStack.perform(
asynchronous: { (transaction) in
transaction.delete(objectIDs: [itemID])
},
sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete,
completion: { _ in }
)
default:
break
}
@@ -116,10 +116,10 @@ extension Modern.ColorsDemo.UIKit {
super.init(style: .plain)
}
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
@@ -128,7 +128,7 @@ extension Modern.ColorsDemo.UIKit {
Modern.ColorsDemo.UIKit.ItemCell.self,
forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier
)
self.startObservingList()
}
@@ -18,6 +18,7 @@ extension Modern {
// MARK: Internal
@MainActor
static let dataStack: DataStack = {
let dataStack = DataStack(
@@ -44,6 +45,7 @@ extension Modern {
return dataStack
}()
@MainActor
static let palettesPublisher: ListPublisher<Modern.ColorsDemo.Palette> = Modern.ColorsDemo.dataStack.publishList(
From<Modern.ColorsDemo.Palette>()
.sectionBy(
@@ -54,6 +56,7 @@ extension Modern {
.orderBy(.ascending(\.$hue))
)
@MainActor
static var filter: Modern.ColorsDemo.Filter = .all {
didSet {
@@ -13,51 +13,58 @@ extension Modern.PlacemarksDemo {
// MARK: Geocoder
@MainActor
final class Geocoder {
// MARK: Internal
func geocode(
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>,
completion: @escaping (_ title: String?, _ subtitle: String?) -> Void
) {
place: ObjectSnapshot<Modern.PlacemarksDemo.Place>
) async -> (title: String?, subtitle: String?) {
self.geocoder?.cancelGeocode()
let geocoder = CLGeocoder()
self.geocoder = geocoder
geocoder.reverseGeocodeLocation(
CLLocation(latitude: place.$latitude, longitude: place.$longitude),
completionHandler: { (placemarks, error) -> Void in
defer {
self.geocoder = nil
}
guard let placemark = placemarks?.first else {
return
}
let address = CNMutablePostalAddress()
address.street = placemark.thoroughfare ?? ""
address.subLocality = placemark.subThoroughfare ?? ""
address.city = placemark.locality ?? ""
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
address.state = placemark.administrativeArea ?? ""
address.postalCode = placemark.postalCode ?? ""
address.country = placemark.country ?? ""
address.isoCountryCode = placemark.isoCountryCode ?? ""
completion(
placemark.name,
CNPostalAddressFormatter.string(
from: address,
style: .mailingAddress
)
)
defer {
if self.geocoder === geocoder {
self.geocoder = nil
}
)
}
do {
let placemarks = try await geocoder.reverseGeocodeLocation(
CLLocation(latitude: place.$latitude, longitude: place.$longitude)
)
guard let placemark = placemarks.first else {
return (nil, nil)
}
let address = CNMutablePostalAddress()
address.street = placemark.thoroughfare ?? ""
address.subLocality = placemark.subThoroughfare ?? ""
address.city = placemark.locality ?? ""
address.subAdministrativeArea = placemark.subAdministrativeArea ?? ""
address.state = placemark.administrativeArea ?? ""
address.postalCode = placemark.postalCode ?? ""
address.country = placemark.country ?? ""
address.isoCountryCode = placemark.isoCountryCode ?? ""
return (
placemark.name,
CNPostalAddressFormatter.string(
from: address,
style: .mailingAddress
)
)
}
catch {
return (nil, nil)
}
}
// MARK: Private
@@ -3,7 +3,6 @@
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import CoreLocation
import Combine
import CoreStore
import Foundation
import MapKit
@@ -14,7 +13,7 @@ import SwiftUI
extension Modern.PlacemarksDemo {
// MARK: - Modern.PlacemarksDemo.MainView
struct MainView: View {
/**
@@ -38,7 +37,7 @@ extension Modern.PlacemarksDemo {
- Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks.
*/
private func demoSynchronousTransaction() {
_ = try? Modern.PlacemarksDemo.dataStack.perform(
synchronous: { (transaction) in
@@ -68,41 +67,12 @@ extension Modern.PlacemarksDemo {
print("Commit failed: \(error as Any)")
}
}
// MARK: Internal
@ObjectState(Modern.PlacemarksDemo.placePublisher)
var place: ObjectSnapshot<Modern.PlacemarksDemo.Place>?
init() {
self.sinkCancellable = self.$place?.reactive.snapshot().sink(
receiveCompletion: { _ in
// Deleted, do nothing
},
receiveValue: { [self] (snapshot) in
guard let snapshot = snapshot else {
return
}
self.geocoder.geocode(place: snapshot) { (title, subtitle) in
guard self.place == snapshot else {
return
}
self.demoUnsafeTransaction(
title: title,
subtitle: subtitle,
for: snapshot
)
}
}
)
}
// MARK: View
@@ -130,34 +100,41 @@ extension Modern.PlacemarksDemo {
)
}
}
.navigationBarTitle("Placemarks")
.navigationBarItems(
trailing: Button("Random") {
.task(id: self.place.map({ "\($0.$latitude),\($0.$longitude)" })) {
guard let place = self.place else {
return
}
let geocoded = await self.geocoder.geocode(place: place)
guard self.place?.objectID() == place.objectID() else {
return
}
guard geocoded.title != nil || geocoded.subtitle != nil else {
return
}
self.demoUnsafeTransaction(
title: geocoded.title,
subtitle: geocoded.subtitle,
for: place
)
}
.navigationTitle("Placemarks")
.toolbar {
Button("Random") {
self.demoSynchronousTransaction()
}
)
}
}
// MARK: Private
private var sinkCancellable: AnyCancellable?
private let geocoder = Modern.PlacemarksDemo.Geocoder()
@State
private var geocoder = Modern.PlacemarksDemo.Geocoder()
}
}
#if DEBUG
struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.PlacemarksDemo.MainView()
}
}
#endif
@@ -11,8 +11,8 @@ extension Modern {
// MARK: - Modern.PlacemarksDemo
/**
Sample usages for `CoreStoreObject` transactions
*/
Sample usages for `CoreStoreObject` transactions
*/
enum PlacemarksDemo {
// MARK: Internal
@@ -20,6 +20,7 @@ extension Modern {
/**
Sample 1: Setting up the `DataStack` and storage
*/
@MainActor
static let dataStack: DataStack = {
let dataStack = DataStack(
@@ -45,7 +46,8 @@ extension Modern {
)
return dataStack
}()
@MainActor
static let placePublisher: ObjectPublisher<Modern.PlacemarksDemo.Place> = {
let dataStack = Modern.PlacemarksDemo.dataStack
@@ -2,18 +2,17 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.MainView
struct MainView<ListView: View>: View {
// MARK: Internal
init(
@@ -22,16 +21,16 @@ extension Modern.PokedexDemo {
self.listView = listView
}
// MARK: View
var body: some View {
ZStack {
self.listView()
.frame(minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.vertical)
.frame(minHeight: 0, maxHeight: .infinity)
.ignoresSafeArea(.container, edges: .vertical)
if self.pokedexEntries.isEmpty {
@@ -56,10 +55,10 @@ extension Modern.PokedexDemo {
.padding()
}
}
.navigationBarTitle("Pokedex")
.navigationTitle("Pokedex")
}
// MARK: Private
@ListState(
@@ -68,28 +67,10 @@ extension Modern.PokedexDemo {
in: Modern.PokedexDemo.dataStack
)
private var pokedexEntries
@ObservedObject
private var service: Modern.PokedexDemo.Service = .init()
@State
private var service = Modern.PokedexDemo.Service()
private let listView: () -> ListView
}
}
#if DEBUG
@available(iOS 14.0, *)
struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.PokedexDemo.MainView(
listView: Modern.PokedexDemo.UIKit.ListView.init
)
}
}
#endif
@@ -2,64 +2,48 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
import Combine
import CoreData
import CoreStore
import UIKit
import Foundation
import Observation
// MARK: - Modern.PokedexDemo
extension Modern.PokedexDemo {
// MARK: - Modern.PokedexDemo.Service
final class Service: ObservableObject {
@MainActor
@Observable
final class Service {
/**
Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples
*/
private static func importPokedexEntries(
from output: URLSession.DataTaskPublisher.Output
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
private static func importPokedexEntries(from data: Data) async throws {
return .init { promise in
do {
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Void in
let json: Dictionary<String, Any> = try self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
let results: [Dictionary<String, Any>] = try self.parseJSON(
json["results"]
)
_ = try transaction.importUniqueObjects(
Into<Modern.PokedexDemo.PokedexEntry>(),
sourceArray: results.enumerated().map { (index, json) in
(index: index, json: json)
}
)
},
success: { result in
promise(.success(result))
},
failure: { error in
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
let json: Dictionary<String, Any> = try self.parseJSON(
try JSONSerialization.jsonObject(with: data, options: [])
)
let results: [Dictionary<String, Any>] = try self.parseJSON(
json["results"]
)
_ = try transaction.importUniqueObjects(
Into<Modern.PokedexDemo.PokedexEntry>(),
sourceArray: results.enumerated().map { index, json in
(index: index, json: json)
}
}
)
)
}
}
catch {
throw self.mapError(error)
}
}
@@ -67,134 +51,93 @@ extension Modern.PokedexDemo {
Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary`
*/
private static func importSpecies(
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
from output: URLSession.DataTaskPublisher.Output
) -> Future<ObjectSnapshot<Modern.PokedexDemo.Species>, Modern.PokedexDemo.Service.Error> {
for detailsObjectID: NSManagedObjectID,
from data: Data
) async throws -> ObjectSnapshot<Modern.PokedexDemo.Species> {
return .init { promise in
let speciesObjectID = try await Modern.PokedexDemo.dataStack.async.perform { transaction -> NSManagedObjectID in
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Modern.PokedexDemo.Species in
let json: Dictionary<String, Any> = try self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
guard
let species = try transaction.importUniqueObject(
Into<Modern.PokedexDemo.Species>(),
source: json
)
else {
throw Modern.PokedexDemo.Service.Error.unexpected
}
details.asEditable(in: transaction)?.species = species
return species
},
success: { species in
promise(.success(species.asSnapshot(in: Modern.PokedexDemo.dataStack)!))
},
failure: { error in
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
}
}
let json: Dictionary<String, Any> = try self.parseJSON(
try JSONSerialization.jsonObject(with: data, options: [])
)
guard
let species = try transaction.importUniqueObject(
Into<Modern.PokedexDemo.Species>(),
source: json
)
else {
throw Modern.PokedexDemo.Service.Error.unexpected
}
transaction
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
.species = species
return species.objectID()
}
guard
let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesObjectID),
let snapshot = species.asSnapshot()
else {
throw Modern.PokedexDemo.Service.Error.unexpected
}
return snapshot
}
/**
Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s
*/
private static func importForms(
for details: ObjectSnapshot<Modern.PokedexDemo.Details>,
from outputs: [URLSession.DataTaskPublisher.Output]
) -> Future<Void, Modern.PokedexDemo.Service.Error> {
for detailsObjectID: NSManagedObjectID,
from dataArray: [Data]
) async throws {
return .init { promise in
do {
Modern.PokedexDemo.dataStack.perform(
asynchronous: { transaction -> Void in
let forms = try transaction.importUniqueObjects(
Into<Modern.PokedexDemo.Form>(),
sourceArray: outputs.map { output in
return try self.parseJSON(
try JSONSerialization.jsonObject(with: output.data, options: [])
)
}
)
guard !forms.isEmpty else {
try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in
let forms = try transaction.importUniqueObjects(
Into<Modern.PokedexDemo.Form>(),
sourceArray: dataArray.map { data in
throw Modern.PokedexDemo.Service.Error.unexpected
try self.parseJSON(
try JSONSerialization.jsonObject(with: data, options: [])
) as [String: Any]
}
details.asEditable(in: transaction)?.forms = forms
},
success: {
)
guard !forms.isEmpty else {
promise(.success(()))
},
failure: { error in
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
promise(.failure(error))
case .userError(let error):
promise(.failure(.otherError(error)))
case let error:
promise(.failure(.saveError(error)))
}
throw Modern.PokedexDemo.Service.Error.unexpected
}
)
transaction
.edit(Into<Modern.PokedexDemo.Details>(), detailsObjectID)?
.forms = forms
}
}
catch {
throw self.mapError(error)
}
}
// MARK: Internal
private(set) var isLoading: Bool = false {
willSet {
self.objectWillChange.send()
}
}
private(set) var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? {
willSet {
self.objectWillChange.send()
}
}
private(set) var isLoading: Bool = false
init() {}
static func parseJSON<Output>(
static nonisolated func parseJSON<Output>(
_ json: Any?,
file: StaticString = #file,
line: Int = #line
) throws -> Output {
switch json {
case let json as Output:
return json
case let any:
throw Modern.PokedexDemo.Service.Error.parseError(
expected: Output.self,
@@ -203,20 +146,20 @@ extension Modern.PokedexDemo {
)
}
}
static func parseJSON<JSONType, Output>(
static nonisolated func parseJSON<JSONType, Output>(
_ json: Any?,
transformer: (JSONType) throws -> Output?,
file: StaticString = #file,
line: Int = #line
) throws -> Output {
switch json {
case let json as JSONType:
let transformed = try transformer(json)
if let json = transformed {
return json
}
throw Modern.PokedexDemo.Service.Error.parseError(
@@ -224,7 +167,7 @@ extension Modern.PokedexDemo {
actual: type(of: transformed),
file: "\(file):\(line)"
)
case let any:
throw Modern.PokedexDemo.Service.Error.parseError(
expected: Output.self,
@@ -233,153 +176,235 @@ extension Modern.PokedexDemo {
)
}
}
func fetchPokedexEntries() {
self.cancellable["pokedexEntries"] = self.pokedexEntries
.receive(on: DispatchQueue.main)
.handleEvents(
receiveSubscription: { [weak self] _ in
guard let self = self else {
return
}
self.lastError = nil
self.isLoading = true
}
)
.sink(
receiveCompletion: { [weak self] completion in
guard let self = self else {
return
}
self.isLoading = false
switch completion {
case .finished:
self.lastError = nil
case .failure(let error):
print(error)
self.lastError = (
error: error,
retry: { [weak self] in
self?.fetchPokedexEntries()
}
)
}
},
receiveValue: {}
)
self.pokedexEntriesTask?.cancel()
self.pokedexEntriesTask = Task { [weak self] in
await self?.runFetchPokedexEntries()
}
}
func fetchDetails(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
self.fetchSpeciesIfNeeded(for: pokedexEntry)
}
// MARK: Private
private var cancellable: Dictionary<String, AnyCancellable> = [:]
private lazy var pokedexEntries: AnyPublisher<Void, Modern.PokedexDemo.Service.Error> = URLSession.shared
.dataTaskPublisher(
for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")!
)
.mapError({ .networkError($0) })
.flatMap(Self.importPokedexEntries(from:))
.eraseToAnyPublisher()
private func fetchSpeciesIfNeeded(for pokedexEntry: ObjectSnapshot<Modern.PokedexDemo.PokedexEntry>) {
guard let details = pokedexEntry.$details?.snapshot else {
return
}
if let species = details.$species?.snapshot {
self.fetchFormsIfNeeded(for: species)
self.fetchFormsIfNeeded(
key: species.$id,
detailsObjectID: details.objectID(),
species: species
)
return
}
self.cancellable["species.\(pokedexEntry.$id)"] = URLSession.shared
.dataTaskPublisher(for: pokedexEntry.$speciesURL)
.mapError({ .networkError($0) })
.flatMap({ Self.importSpecies(for: details, from: $0) })
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
},
receiveValue: { species in
self.fetchFormsIfNeeded(for: species)
}
)
}
private func fetchFormsIfNeeded(for species: ObjectSnapshot<Modern.PokedexDemo.Species>) {
guard
let details = species.$details?.snapshot,
details.$forms.isEmpty
else {
let key = pokedexEntry.$id
guard self.detailTasks[key] == nil else {
return
}
self.cancellable["forms.\(species.$id)"] = species
.$formsURLs
.map(
{
URLSession.shared
.dataTaskPublisher(for: $0)
.mapError({ Modern.PokedexDemo.Service.Error.networkError($0) })
.eraseToAnyPublisher()
}
)
.reduce(
into: Just<[URLSession.DataTaskPublisher.Output]>([])
.setFailureType(to: Modern.PokedexDemo.Service.Error.self)
.eraseToAnyPublisher(),
{ (result, publisher) in
result = result
.zip(publisher, { $0 + [$1] })
.eraseToAnyPublisher()
}
)
.flatMap({ Self.importForms(for: details, from: $0) })
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
},
receiveValue: { _ in }
let speciesURL = pokedexEntry.$speciesURL
let detailsObjectID = details.objectID()
self.detailTasks[key] = Task { [weak self] in
guard let self else {
return
}
defer {
self.detailTasks.removeValue(forKey: key)
}
await self.fetchSpecies(
key: key,
detailsObjectID: detailsObjectID,
speciesURL: speciesURL
)
}
}
// MARK: Private
@ObservationIgnored
private static let pokedexURL = URL(
string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0"
)!
@ObservationIgnored
private var pokedexEntriesTask: Task<Void, Never>?
@ObservationIgnored
private var detailTasks: [String: Task<Void, Never>] = [:]
private static func mapError(_ error: CoreStoreError) -> Modern.PokedexDemo.Service.Error {
switch error {
case .userError(let error as Modern.PokedexDemo.Service.Error):
return error
case .userError(let error):
return .otherError(error)
case let error:
return .saveError(error)
}
}
private func runFetchPokedexEntries() async {
self.isLoading = true
defer {
self.isLoading = false
self.pokedexEntriesTask = nil
}
do {
let (data, _) = try await URLSession.shared.data(from: Self.pokedexURL)
try Task.checkCancellation()
try await Self.importPokedexEntries(from: data)
}
catch is CancellationError {
return
}
catch let error as Modern.PokedexDemo.Service.Error {
print(error)
}
catch let error as URLError {
print(Modern.PokedexDemo.Service.Error.networkError(error))
}
catch {
print(Modern.PokedexDemo.Service.Error.otherError(error))
}
}
private func fetchSpecies(
key: String,
detailsObjectID: NSManagedObjectID,
speciesURL: URL
) async {
do {
let (data, _) = try await URLSession.shared.data(from: speciesURL)
try Task.checkCancellation()
let species = try await Self.importSpecies(
for: detailsObjectID,
from: data
)
guard species.$details?.snapshot?.$forms.isEmpty == true else {
return
}
await self.fetchForms(
detailsObjectID: detailsObjectID,
formsURLs: species.$formsURLs
)
}
catch is CancellationError {
return
}
catch let error as Modern.PokedexDemo.Service.Error {
print(error)
}
catch let error as URLError {
print(Modern.PokedexDemo.Service.Error.networkError(error))
}
catch {
print(Modern.PokedexDemo.Service.Error.otherError(error))
}
}
private func fetchFormsIfNeeded(
key: String,
detailsObjectID: NSManagedObjectID,
species: ObjectSnapshot<Modern.PokedexDemo.Species>
) {
guard species.$details?.snapshot?.$forms.isEmpty == true else {
return
}
guard self.detailTasks[key] == nil else {
return
}
let formsURLs = species.$formsURLs
self.detailTasks[key] = Task { [weak self] in
guard let self else {
return
}
defer {
self.detailTasks.removeValue(forKey: key)
}
await self.fetchForms(
detailsObjectID: detailsObjectID,
formsURLs: formsURLs
)
}
}
private func fetchForms(
detailsObjectID: NSManagedObjectID,
formsURLs: [URL]
) async {
do {
var dataArray: [Data] = []
dataArray.reserveCapacity(formsURLs.count)
for url in formsURLs {
let (data, _) = try await URLSession.shared.data(from: url)
try Task.checkCancellation()
dataArray.append(data)
}
try await Self.importForms(
for: detailsObjectID,
from: dataArray
)
}
catch is CancellationError {
return
}
catch let error as Modern.PokedexDemo.Service.Error {
print(error)
}
catch let error as URLError {
print(Modern.PokedexDemo.Service.Error.networkError(error))
}
catch {
print(Modern.PokedexDemo.Service.Error.otherError(error))
}
}
// MARK: - Modern.PokedexDemo.Service.Error
enum Error: Swift.Error {
case networkError(URLError)
case parseError(expected: Any.Type, actual: Any.Type, file: String)
case saveError(CoreStoreError)
@@ -12,12 +12,11 @@ extension Modern.PokedexDemo.UIKit {
// MARK: - Modern.PokedexDemo.ListView
struct ListView: UIViewControllerRepresentable {
// MARK: Internal
init() {
self.service = Modern.PokedexDemo.Service.init()
self._service = State(initialValue: Modern.PokedexDemo.Service())
self.listPublisher = Modern.PokedexDemo.dataStack
.publishList(
From<Modern.PokedexDemo.PokedexEntry>()
@@ -41,30 +40,20 @@ extension Modern.PokedexDemo.UIKit {
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
// MARK: Private
@ObservedObject
@State
private var service: Modern.PokedexDemo.Service
private let listPublisher: ListPublisher<Modern.PokedexDemo.PokedexEntry>
}
}
#if DEBUG
struct _Demo_Modern_PokedexDemo_UIKit_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
let service = Modern.PokedexDemo.Service()
service.fetchPokedexEntries()
return Modern.PokedexDemo.UIKit.ListView()
}
// MARK: - Preview
#Preview {
Modern.PokedexDemo.UIKit.ListView()
}
#endif
@@ -11,12 +11,13 @@ extension Modern {
// MARK: - Modern.PokedexDemo
/**
Sample usages for importing external data into `CoreStoreObject` attributes
*/
Sample usages for importing external data into `CoreStoreObject` attributes
*/
enum PokedexDemo {
// MARK: Internal
@MainActor
static let dataStack: DataStack = {
let dataStack = DataStack(
@@ -49,6 +50,7 @@ extension Modern {
return dataStack
}()
@MainActor
static let pokedexEntries: ListPublisher<Modern.PokedexDemo.PokedexEntry> = Modern.PokedexDemo.dataStack.publishList(
From<Modern.PokedexDemo.PokedexEntry>()
.orderBy(.ascending(\.$index))
@@ -15,18 +15,22 @@ extension Modern.TimeZonesDemo {
// MARK: Internal
init(title: String, subtitle: String) {
self.title = title
self.subtitle = subtitle
}
// MARK: View
var body: some View {
VStack(alignment: .leading) {
Text(self.title)
.font(.headline)
.foregroundColor(.primary)
Text(self.subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
@@ -40,19 +44,3 @@ extension Modern.TimeZonesDemo {
fileprivate let subtitle: String
}
}
#if DEBUG
struct _Demo_Modern_TimeZonesDemo_ItemView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.TimeZonesDemo.ItemView(
title: "Item Title",
subtitle: "A subtitle caption for this item"
)
}
}
#endif
@@ -10,7 +10,7 @@ import SwiftUI
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.ListView
struct ListView: View {
// MARK: Internal
@@ -53,15 +53,18 @@ extension Modern.TimeZonesDemo {
// MARK: View
var body: some View {
List {
ForEach(self.values, id: \.title) { item in
Modern.TimeZonesDemo.ItemView(
title: item.title,
subtitle: item.subtitle
)
}
}
.navigationBarTitle(self.title)
.navigationTitle(self.title)
}
@@ -71,24 +74,3 @@ extension Modern.TimeZonesDemo {
private let values: [(title: String, subtitle: String)]
}
}
#if DEBUG
struct _Demo_Modern_TimeZonesDemo_ListView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.TimeZonesDemo.ListView(
title: "Title",
objects: try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.orderBy(.ascending(\.$name))
)
)
}
}
#endif
@@ -10,7 +10,7 @@ import SwiftUI
extension Modern.TimeZonesDemo {
// MARK: - Modern.TimeZonesDemo.MainView
struct MainView: View {
/**
@@ -61,11 +61,11 @@ extension Modern.TimeZonesDemo {
return try! Modern.TimeZonesDemo.dataStack.fetchAll(
From<Modern.TimeZonesDemo.TimeZone>()
.where((-secondsIn3Hours ... secondsIn3Hours) ~= \.$secondsFromGMT)
/// equivalent to:
/// ```
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
/// && \.$secondsFromGMT <= secondsIn3Hours)
/// ```
/// equivalent to:
/// ```
/// .where(\.$secondsFromGMT >= -secondsIn3Hours
/// && \.$secondsFromGMT <= secondsIn3Hours)
/// ```
.orderBy(.ascending(\.$secondsFromGMT))
)
}
@@ -137,36 +137,54 @@ extension Modern.TimeZonesDemo {
// MARK: View
var body: some View {
List {
Section(header: Text("Fetching objects")) {
Section("Fetching objects") {
ForEach(self.fetchingItems, id: \.title) { item in
Menu.ItemView(
title: item.title,
NavigationLink(
destination: {
Modern.TimeZonesDemo.ListView(
title: item.title,
objects: item.objects()
)
},
label: {
Menu.ItemView(
title: item.title
)
}
)
}
}
Section(header: Text("Querying raw values")) {
Section("Querying raw values") {
ForEach(self.queryingItems, id: \.title) { item in
Menu.ItemView(
title: item.title,
NavigationLink(
destination: {
Modern.TimeZonesDemo.ListView(
title: item.title,
value: item.value()
)
},
label: {
Menu.ItemView(
title: item.title
)
}
)
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Time Zones")
.listStyle(.grouped)
.navigationTitle("Time Zones")
}
@@ -217,18 +235,3 @@ extension Modern.TimeZonesDemo {
}
}
}
#if DEBUG
struct _Demo_Modern_TimeZonesDemo_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Modern.TimeZonesDemo.MainView()
}
}
#endif
@@ -11,8 +11,8 @@ extension Modern {
// MARK: - Modern.TimeZonesDemo
/**
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
*/
Sample usages for creating Fetch and Query clauses for `CoreStoreObject`s
*/
enum TimeZonesDemo {
// MARK: Internal
+46 -37
View File
@@ -4,66 +4,75 @@
import Foundation
import UIKit
import Combine
// MARK: - ImageDownloader
final class ImageDownloader: ObservableObject {
// MARK: FilePrivate
@MainActor
final class ImageDownloader {
// MARK: Internal
private(set) var image: UIImage?
let url: URL?
init(url: URL?) {
self.url = url
guard let url = url else {
guard let url else {
return
}
if let image = Self.cache[url] {
self.image = image
}
}
func fetchImage(completion: @escaping (UIImage) -> Void = { _ in }) {
guard let url = url else {
guard let url else {
return
}
if let image = Self.cache[url] {
self.objectWillChange.send()
self.image = image
completion(image)
return
}
self.cancellable = URLSession.shared
.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { output in
if let image = UIImage(data: output.data) {
Self.cache[url] = image
self.objectWillChange.send()
self.image = image
completion(image)
}
self.task = Task { [weak self] in
guard let self else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
guard
!Task.isCancelled,
let image = UIImage(data: data)
else {
return
}
)
Self.cache[url] = image
self.image = image
completion(image)
}
catch {
return
}
}
}
// MARK: Private
private static var cache: [URL: UIImage] = [:]
private var cancellable: AnyCancellable?
private var task: Task<Void, Never>?
}
-29
View File
@@ -1,29 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
// MARK: - LazyView
struct LazyView<Content: View>: View {
// MARK: Internal
init(_ load: @escaping () -> Content) {
self.load = load
}
// MARK: View
var body: Content {
self.load()
}
// MARK: Private
private let load: () -> Content
}
+17 -34
View File
@@ -10,34 +10,36 @@ extension Menu {
// MARK: - Menu.ItemView
struct ItemView<Destination: View>: View {
struct ItemView: View {
// MARK: Internal
init(
title: String,
subtitle: String? = nil,
destination: @escaping () -> Destination
isEnabled: Bool = true
) {
self.title = title
self.subtitle = subtitle
self.destination = destination
self.isEnabled = isEnabled
}
// MARK: View
var body: some View {
NavigationLink(destination: LazyView(self.destination)) {
VStack(alignment: .leading) {
Text(self.title)
.font(.headline)
.foregroundColor(.primary)
self.subtitle.map {
Text($0)
.font(.subheadline)
.foregroundColor(.secondary)
}
VStack(alignment: .leading) {
Text(self.title)
.font(.headline)
.foregroundStyle(self.isEnabled ? .primary : .secondary)
self.subtitle.map {
Text($0)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
@@ -47,25 +49,6 @@ extension Menu {
fileprivate let title: String
fileprivate let subtitle: String?
fileprivate let destination: () -> Destination
fileprivate let isEnabled: Bool
}
}
#if DEBUG
struct _Demo_Menu_ItemView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Menu.ItemView(
title: "Item Title",
subtitle: "A subtitle caption for this item",
destination: {
Color.blue
}
)
}
}
#endif
+38 -108
View File
@@ -11,124 +11,54 @@ import SwiftUI
extension Menu {
// MARK: - Menu.MainView
struct MainView: View {
@State
private var selection: Menu.Route?
// MARK: View
var body: some View {
NavigationView {
List {
Section(header: Text("Modern (CoreStoreObject subclasses)")) {
Menu.ItemView(
title: "Placemarks",
subtitle: "Making changes using Transactions",
destination: {
Modern.PlacemarksDemo.MainView()
NavigationSplitView(
sidebar: {
List(selection: self.$selection) {
ForEach(Menu.Section.allCases, id: \.self) { section in
Section(section.rawValue) {
ForEach(section.routes) { route in
Menu.ItemView(
title: route.title,
subtitle: route.subtitle,
isEnabled: route.isEnabled
)
.tag(route as Menu.Route?)
.disabled(!route.isEnabled)
}
}
)
Menu.ItemView(
title: "Time Zones",
subtitle: "Fetching objects and Querying raw values",
destination: {
Modern.TimeZonesDemo.MainView()
}
)
Menu.ItemView(
title: "Colors (UIKit)",
subtitle: "Observing list changes and single-object changes using DiffableDataSources",
destination: {
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.UIKit.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
.edgesIgnoringSafeArea(.all)
},
detailView: { objectPublisher in
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
}
)
}
)
Menu.ItemView(
title: "Colors (SwiftUI)",
subtitle: "Observing list changes and single-object changes using SwiftUI bindings",
destination: {
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
},
detailView: { objectPublisher in
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
}
)
}
)
Menu.ItemView(
title: "Pokedex API",
subtitle: "Importing JSON data from external source",
destination: {
Modern.PokedexDemo.MainView(
listView: Modern.PokedexDemo.UIKit.ListView.init
)
}
)
}
}
Section(header: Text("Classic (NSManagedObject subclasses)")) {
Menu.ItemView(
title: "Colors",
subtitle: "Observing list changes and single-object changes using ListMonitor",
destination: {
Classic.ColorsDemo.MainView()
}
)
.navigationTitle("CoreStore Demos")
.listStyle(.sidebar)
},
detail: {
if let selection = self.selection {
selection.destination
}
Section(header: Text("Advanced")) {
Menu.ItemView(
title: "Accounts",
subtitle: "Switching between multiple persistent stores",
destination: { EmptyView() }
)
.disabled(true)
Menu.ItemView(
title: "Evolution",
subtitle: "Migrating and reverse-migrating stores",
destination: {
Advanced.EvolutionDemo.MainView()
}
)
Menu.ItemView(
title: "Logger",
subtitle: "Implementing a custom logger",
destination: { EmptyView() }
)
.disabled(true)
else {
Menu.PlaceholderView()
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("CoreStore Demos")
Menu.PlaceholderView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
)
}
}
}
#if DEBUG
struct _Demo_Menu_MainView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
Menu.MainView()
}
}
#endif
@@ -2,43 +2,28 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Combine
import CoreStore
import SwiftUI
import UIKit
// MARK: - Menu
extension Menu {
// MARK: - Menu.PlaceholderView
struct PlaceholderView: UIViewControllerRepresentable {
// MARK: UIViewControllerRepresentable
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Self.Context) -> UIViewControllerType {
return UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Self.Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Void) {}
}
}
#if DEBUG
struct _Demo_Menu_PlaceholderView_Preview: PreviewProvider {
// MARK: PreviewProvider
static var previews: some View {
return Menu.PlaceholderView()
}
}
#endif
+195 -2
View File
@@ -2,9 +2,202 @@
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import Foundation
import SwiftUI
// MARK: - Menu
enum Menu {}
enum Menu {
// MARK: - Section
enum Section: String, CaseIterable, Hashable {
case modern = "Modern (CoreStoreObject subclasses)"
case classic = "Classic (NSManagedObject subclasses)"
case advanced = "Advanced"
var routes: [Route] {
switch self {
case .modern:
return [
.placemarks,
.timeZones,
.colorsUIKit,
.colorsSwiftUI,
.pokedex
]
case .classic:
return [
.classicColors
]
case .advanced:
return [
.accounts,
.evolution,
.logger
]
}
}
}
// MARK: - Route
enum Route: String, CaseIterable, Hashable, Identifiable {
case placemarks
case timeZones
case colorsUIKit
case colorsSwiftUI
case pokedex
case classicColors
case accounts
case evolution
case logger
var id: Self {
self
}
var title: String {
switch self {
case .placemarks:
"Placemarks"
case .timeZones:
"Time Zones"
case .colorsUIKit:
"Colors (UIKit)"
case .colorsSwiftUI:
"Colors (SwiftUI)"
case .pokedex:
"Pokedex API"
case .classicColors:
"Colors"
case .accounts:
"Accounts"
case .evolution:
"Evolution"
case .logger:
"Logger"
}
}
var subtitle: String {
switch self {
case .placemarks:
"Making changes using Transactions"
case .timeZones:
"Fetching objects and Querying raw values"
case .colorsUIKit:
"Observing list changes and single-object changes using DiffableDataSources"
case .colorsSwiftUI:
"Observing list changes and single-object changes using SwiftUI bindings"
case .pokedex:
"Importing JSON data from external source"
case .classicColors:
"Observing list changes and single-object changes using ListMonitor"
case .accounts:
"Switching between multiple persistent stores"
case .evolution:
"Migrating and reverse-migrating stores"
case .logger:
"Implementing a custom logger"
}
}
var isEnabled: Bool {
switch self {
case .placemarks,
.timeZones,
.colorsUIKit,
.colorsSwiftUI,
.pokedex,
.classicColors,
.evolution:
return true
case .accounts,
.logger:
return false
}
}
@MainActor
@ViewBuilder
var destination: some View {
switch self {
case .placemarks:
Modern.PlacemarksDemo.MainView()
case .timeZones:
Modern.TimeZonesDemo.MainView()
case .colorsUIKit:
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.UIKit.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
.ignoresSafeArea()
},
detailView: { objectPublisher in
Modern.ColorsDemo.UIKit.DetailView(objectPublisher)
}
)
case .colorsSwiftUI:
Modern.ColorsDemo.MainView(
listView: { listPublisher, onPaletteTapped in
Modern.ColorsDemo.SwiftUI.ListView(
listPublisher: listPublisher,
onPaletteTapped: onPaletteTapped
)
},
detailView: { objectPublisher in
Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher)
}
)
case .pokedex:
Modern.PokedexDemo.MainView(
listView: Modern.PokedexDemo.UIKit.ListView.init
)
case .classicColors:
Classic.ColorsDemo.MainView()
case .accounts, .logger:
EmptyView()
case .evolution:
Advanced.EvolutionDemo.MainView()
}
}
}
}
-36
View File
@@ -1,36 +0,0 @@
//
// Demo
// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved.
import SwiftUI
import UIKit
// MARK: - SceneDelegate
@objc final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: UIWindowSceneDelegate
@objc dynamic var window: UIWindow?
// MARK: UISceneDelegate
@objc dynamic func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard case let scene as UIWindowScene = scene else {
return
}
let window = UIWindow(windowScene: scene)
window.rootViewController = UIHostingController(
rootView: Menu.MainView()
)
self.window = window
window.makeKeyAndVisible()
}
}
+2 -2
View File
@@ -41,11 +41,11 @@ let dataStack = DataStack(
Entity<Animal>("Animal"),
Entity<Person>("Person"),
Entity<Dog>("Dog")
]/*,
],
versionLock: [
"Animal": [0x4a201cc685d53c0a, 0x16e6c3b561577875, 0xb032e2da61c792a0, 0xa133b801051acee4],
"Person": [0xca938eea1af4bd56, 0xbca30994506356ad, 0x7a7cc655898816ef, 0x1a4551ffedc9b214]
]*/
]
)
)
dataStack.addStorage(
+20 -2
View File
@@ -32,7 +32,8 @@ import CoreData
/**
The `AsynchronousDataTransaction` provides an interface for `DynamicObject` creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from `DataStack.perform(asynchronous:...)`.
*/
public final class AsynchronousDataTransaction: BaseDataTransaction {
@_nonSendable
public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction {
/**
Cancels a transaction by throwing `CoreStoreError.userCancelled`.
@@ -78,6 +79,23 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
return super.create(into)
}
/**
Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`.
- parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited
- returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`.
*/
public override func edit<O: DynamicObject>(
_ persistentID: DynamicObjectID<O>?
) -> O? {
Internals.assert(
!self.isCommitted,
"Attempted to update an entity for \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))."
)
return super.edit(persistentID)
}
/**
Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`.
@@ -188,7 +206,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
}
internal func autoCommit(
_ completion: @escaping @MainActor (
_ completion: @escaping @MainActor @Sendable (
_ hasChanges: Bool,
_ error: CoreStoreError?
) -> Void
@@ -110,6 +110,19 @@ extension BaseDataTransaction: FetchableSource, QueryableSource {
return self.context.fetchExisting(object)
}
/**
Fetches the `DynamicObject` instance in the transaction's context from an `NSManagedObjectID`.
- parameter persistentID: the `DynamicObjectID` for the object
- returns: the `DynamicObject` instance if the object exists in the transaction, or `nil` if not found.
*/
public func fetchExisting<O: DynamicObject>(
_ persistentID: DynamicObjectID<O>
) -> O? {
return self.context.fetchExisting(persistentID.managedObjectID)
}
/**
Fetches the `DynamicObject` instance in the transaction's context from an `NSManagedObjectID`.
@@ -136,6 +149,19 @@ extension BaseDataTransaction: FetchableSource, QueryableSource {
return self.context.fetchExisting(objects)
}
/**
Fetches the `DynamicObject` instances in the transaction's context from a list of `DynamicObjectID`.
- parameter objectIDs: the `DynamicObjectID` array for the objects
- returns: the `DynamicObject` array for objects that exists in the transaction
*/
public func fetchExisting<O: DynamicObject, S: Sequence>(
_ objectIDs: S
) -> [O] where S.Iterator.Element == DynamicObjectID<O> {
return self.context.fetchExisting(objectIDs)
}
/**
Fetches the `DynamicObject` instances in the transaction's context from a list of `NSManagedObjectID`.
+21
View File
@@ -115,6 +115,27 @@ public /*abstract*/ class BaseDataTransaction {
}
}
/**
Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`.
- parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited
- returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`.
*/
public func edit<O: DynamicObject>(
_ persistentID: DynamicObjectID<O>?
) -> O? {
Internals.assert(
self.isRunningInAllowedQueue(),
"Attempted to update an entity for \(Internals.typeName(persistentID)) outside its designated queue."
)
guard let persistentID = persistentID else {
return nil
}
return self.context.fetchExisting(persistentID.managedObjectID)
}
/**
Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`.
@@ -652,7 +652,7 @@ extension ObjectPublisher: CustomDebugStringConvertible, CoreStoreDebugStringCon
return createFormattedString(
"(", ")",
("objectID", self.objectID()),
("managedObjectID", self.cs_id()),
("object", self.object as Any)
)
}
@@ -677,7 +677,7 @@ extension ObjectSnapshot: CustomDebugStringConvertible, CoreStoreDebugStringConv
return createFormattedString(
"(", ")",
("objectID", self.objectID()),
("managedObjectID", self.cs_id()),
("dictionaryForValues", self.dictionaryForValues())
)
}
+2 -3
View File
@@ -24,7 +24,6 @@
//
import Foundation
import os
// MARK: - CoreStoreDefaults
@@ -94,6 +93,6 @@ public enum CoreStoreDefaults {
// MARK: Private
private static let defaultStackInstance: OSAllocatedUnfairLock<DataStack?> = .init(initialState: nil)
private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil)
private static let defaultStackInstance: Internals.Mutex<DataStack?> = .init(nil)
private static let loggerInstance: Internals.Mutex<(any CoreStoreLogger)?> = .init(nil)
}
+1 -1
View File
@@ -32,7 +32,7 @@ import Foundation
/**
All errors thrown from CoreStore are expressed in `CoreStoreError` enum values.
*/
public enum CoreStoreError: Error, CustomNSError, Hashable, @unchecked Sendable {
public enum CoreStoreError: Error, CustomNSError, Hashable, Sendable {
/**
A failure occured because of an unknown error.
+2
View File
@@ -65,6 +65,7 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable {
Do not call this directly. This is exposed as public only as a required initializer.
- Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations.
*/
@_spi(Internals)
public required init(rawObject: NSManagedObject) {
self.isMeta = false
@@ -84,6 +85,7 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable {
Do not call this directly. This is exposed as public only as a required initializer.
- Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations.
*/
@_spi(Internals)
public required init(asMeta: Void) {
self.isMeta = true
+7 -7
View File
@@ -257,20 +257,20 @@ public final class CoreStoreSchema: DynamicSchema {
// MARK: Internal
internal let entitiesByConfiguration: [String: Set<DynamicEntity>]
internal nonisolated(unsafe) let entitiesByConfiguration: [String: Set<DynamicEntity>]
// MARK: Private
private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.coreStoreDataModelBarrierQueue", qos: .userInteractive)
private let allEntities: Set<DynamicEntity>
private nonisolated(unsafe) let allEntities: Set<DynamicEntity>
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 nonisolated(unsafe) var entityDescriptionsByEntity: [DynamicEntity: NSEntityDescription] = [:]
private nonisolated(unsafe) var customGettersSettersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomGetterSetter]] = [:]
private nonisolated(unsafe) var customInitializersByEntity: [DynamicEntity: [KeyPathString: CoreStoreManagedObject.CustomInitializer]] = [:]
private nonisolated(unsafe) var fieldCodersByEntity: [DynamicEntity: [KeyPathString: Internals.AnyFieldCoder]] = [:]
private nonisolated(unsafe) weak var cachedRawModel: NSManagedObjectModel?
private func entityDescription(
for entity: DynamicEntity,
+7 -4
View File
@@ -32,7 +32,7 @@ import Foundation
/**
A `SchemaMappingProvider` that accepts custom mappings for some entities. Mappings of entities with no `CustomMapping` provided will be automatically calculated if possible.
*/
public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider {
public final class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider {
/**
The source model version for the mapping.
@@ -78,7 +78,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider {
/**
Provides the type of mapping for an entity. Mappings of entities with no `CustomMapping` provided will be automatically calculated if possible. Any conflicts or ambiguity will raise an assertion.
*/
public enum CustomMapping: Hashable {
public enum CustomMapping: Hashable, Sendable {
/**
The `sourceEntity` is meant to be removed from the source `DynamicSchema` and should not be migrated to the destination `DynamicSchema`.
@@ -105,7 +105,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider {
- parameter sourceObject: a proxy object representing the source entity. The properties can be accessed via keyPath.
- parameter createDestinationObject: the closure to create the object for the destination entity. The `CustomMapping.inferredTransformation` method can be used directly as the `transformer` if the changes can be inferred (i.e. lightweight). The object is created lazily and executing the closure multiple times will return the same instance. The destination object's properties can be accessed and updated via keyPath.
*/
public typealias Transformer = (
public typealias Transformer = @Sendable (
_ sourceObject: UnsafeSourceObject,
_ createDestinationObject: () -> UnsafeDestinationObject
) throws(any Swift.Error) -> Void
@@ -737,7 +737,10 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider {
.transformEntity(
sourceEntity: sourceEntityName,
destinationEntity: destinationEntityName,
transformer: CustomMapping.inferredTransformation
transformer: {
return try CustomMapping.inferredTransformation($0, $1)
}
)
)
}
+4 -3
View File
@@ -42,8 +42,9 @@ extension DataStack {
public func publishObject<O: DynamicObject>(
_ object: O
) -> ObjectPublisher<O> {
return self.publishObject(object.cs_id())
let context = self.unsafeContext()
return context.objectPublisher(managedObjectID: object.cs_id())
}
/**
@@ -57,7 +58,7 @@ extension DataStack {
) -> ObjectPublisher<O> {
let context = self.unsafeContext()
return context.objectPublisher(objectID: objectID)
return context.objectPublisher(managedObjectID: objectID.managedObjectID)
}
/**
+18 -16
View File
@@ -24,8 +24,7 @@
//
import Foundation
@preconcurrency import CoreData
import os
import CoreData
// MARK: - DataStack
@@ -50,7 +49,7 @@ extension DataStack {
*/
public func addStorage<T>(
_ storage: T,
completion: @escaping @MainActor (SetupResult<T>) -> Void
completion: @escaping @MainActor @Sendable (SetupResult<T>) -> Void
) {
self.coordinator.performAsynchronously {
@@ -111,7 +110,7 @@ extension DataStack {
*/
public func addStorage<T: LocalStorage>(
_ storage: T,
completion: @escaping @MainActor (SetupResult<T>) -> Void
completion: @escaping @MainActor @Sendable (SetupResult<T>) -> Void
) -> Progress? {
let fileURL = storage.fileURL
@@ -274,7 +273,7 @@ extension DataStack {
*/
public func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T,
completion: @escaping @MainActor (MigrationResult) -> Void
completion: @escaping @MainActor @Sendable (MigrationResult) -> Void
) throws(CoreStoreError) -> Progress? {
return try self.coordinator.performSynchronously {
@@ -386,7 +385,7 @@ extension DataStack {
private func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T,
metadata: [String: Any],
completion: @escaping @MainActor (MigrationResult) -> Void
completion: @escaping @MainActor @Sendable (MigrationResult) -> Void
) -> Progress? {
guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else {
@@ -433,8 +432,8 @@ extension DataStack {
}
let migrationTypes = migrationSteps.map { $0.migrationType }
let migrationState: OSAllocatedUnfairLock<(migrationResult: MigrationResult?, cancelled: Bool)> = .init(
initialState: (
let migrationState: Internals.Mutex<(migrationResult: MigrationResult?, cancelled: Bool)> = .init(
(
migrationResult: nil,
cancelled: false
)
@@ -451,6 +450,9 @@ extension DataStack {
let childProgress = Progress(parent: progress, userInfo: nil)
childProgress.totalUnitCount = 100
nonisolated(unsafe) let sourceModel = sourceModel
nonisolated(unsafe) let destinationModel = destinationModel
nonisolated(unsafe) let mappingModel = mappingModel
operations.append(
BlockOperation { [weak self] in
@@ -508,13 +510,11 @@ extension DataStack {
operations.forEach { migrationOperation.addDependency($0) }
migrationOperation.addExecutionBlock { () -> Void in
let migrationResult = migrationState.withLock { $0.migrationResult }
DispatchQueue.main.async {
progress.setProgressHandler(nil)
completion(
migrationState.withLock { $0.migrationResult }
?? .success(migrationTypes)
)
completion(migrationResult ?? .success(migrationTypes))
return
}
}
@@ -620,7 +620,7 @@ extension DataStack {
let estimatedTime: TimeInterval = 60 * 3 // 3 mins
let interval: TimeInterval = 1
let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount)
let fakeProgress: OSAllocatedUnfairLock<Float> = .init(initialState: 0)
let fakeProgress: Internals.Mutex<Float> = .init(0)
@Sendable
func recursiveCheck() {
@@ -656,9 +656,11 @@ extension DataStack {
)
)
}
timerQueue.sync {
fakeProgress.withLock({ $0 = 1.0 })
withoutActuallyEscaping(timerQueue.sync) { escapingClosure in
escapingClosure {
fakeProgress.withLock({ $0 = 1.0 })
}
}
_ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel)
progress.completedUnitCount = progress.totalUnitCount
+13
View File
@@ -37,6 +37,7 @@ extension DataStack {
- parameter object: the `DynamicObject` to observe changes from
- returns: an `ObjectMonitor` that monitors changes to `object`
*/
@MainActor
public func monitorObject<O: DynamicObject>(
_ object: O
) -> ObjectMonitor<O> {
@@ -55,6 +56,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
- returns: a `ListMonitor` instance that monitors changes to the list
*/
@MainActor
public func monitorList<O>(
_ from: From<O>,
_ fetchClauses: FetchClause...
@@ -70,6 +72,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
- returns: a `ListMonitor` instance that monitors changes to the list
*/
@MainActor
public func monitorList<O>(
_ from: From<O>,
_ fetchClauses: [FetchClause]
@@ -107,6 +110,7 @@ extension DataStack {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
- returns: a `ListMonitor` for a list of `DynamicObject`s that satisfy the specified `FetchChainableBuilderType`
*/
@MainActor
public func monitorList<B: FetchChainableBuilderType>(
_ clauseChain: B
) -> ListMonitor<B.ObjectType> {
@@ -124,6 +128,7 @@ extension DataStack {
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public func monitorList<O>(
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
@@ -144,6 +149,7 @@ extension DataStack {
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public func monitorList<O>(
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
@@ -187,6 +193,7 @@ extension DataStack {
- parameter createAsynchronously: the closure that receives the created `ListMonitor` instance
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/
@MainActor
public func monitorList<B: FetchChainableBuilderType>(
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B
@@ -207,6 +214,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
- returns: a `ListMonitor` instance that monitors changes to the list
*/
@MainActor
public func monitorSectionedList<O>(
_ from: From<O>,
_ sectionBy: SectionBy<O>,
@@ -228,6 +236,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
- returns: a `ListMonitor` instance that monitors changes to the list
*/
@MainActor
public func monitorSectionedList<O>(
_ from: From<O>,
_ sectionBy: SectionBy<O>,
@@ -268,6 +277,7 @@ extension DataStack {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
- returns: a `ListMonitor` for a list of `DynamicObject`s that satisfy the specified `SectionMonitorBuilderType`
*/
@MainActor
public func monitorSectionedList<B: SectionMonitorBuilderType>(
_ clauseChain: B
) -> ListMonitor<B.ObjectType> {
@@ -287,6 +297,7 @@ extension DataStack {
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections.
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public func monitorSectionedList<O>(
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
@@ -310,6 +321,7 @@ extension DataStack {
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections.
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public func monitorSectionedList<O>(
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
@@ -355,6 +367,7 @@ extension DataStack {
- parameter createAsynchronously: the closure that receives the created `ListMonitor` instance
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/
@MainActor
public func monitorSectionedList<B: SectionMonitorBuilderType>(
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B
+26
View File
@@ -46,6 +46,19 @@ extension DataStack: FetchableSource, QueryableSource {
return self.mainContext.fetchExisting(object)
}
/**
Fetches the `DynamicObject` instance in the `DataStack`'s context from an `NSManagedObjectID`.
- parameter persistentID: the `DynamicObjectID` for the object
- returns: the `DynamicObject` instance if the object exists in the `DataStack`, or `nil` if not found.
*/
public func fetchExisting<O: DynamicObject>(
_ persistentID: DynamicObjectID<O>
) -> O? {
return self.mainContext.fetchExisting(persistentID.managedObjectID)
}
/**
Fetches the `DynamicObject` instance in the `DataStack`'s context from an `NSManagedObjectID`.
@@ -72,6 +85,19 @@ extension DataStack: FetchableSource, QueryableSource {
return self.mainContext.fetchExisting(objects)
}
/**
Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `DynamicObjectID`.
- parameter objectIDs: the `DynamicObjectID` array for the objects
- returns: the `DynamicObject` array for objects that exists in the `DataStack`
*/
public func fetchExisting<O: DynamicObject, S: Sequence>(
_ objectIDs: S
) -> [O] where S.Iterator.Element == DynamicObjectID<O> {
return self.mainContext.fetchExisting(objectIDs)
}
/**
Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `NSManagedObjectID`.
+4 -3
View File
@@ -43,7 +43,7 @@ extension DataStack {
_ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T,
sourceIdentifier: (any Sendable)? = nil,
completion: @escaping @Sendable (AsynchronousDataTransaction.Result<T>) -> Void
completion: @escaping @MainActor @Sendable (AsynchronousDataTransaction.Result<T>) -> Void
) {
self.perform(
@@ -67,8 +67,8 @@ extension DataStack {
_ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T,
sourceIdentifier: (any Sendable)? = nil,
success: @escaping @Sendable (sending T) -> Void,
failure: @escaping @Sendable (CoreStoreError) -> Void
success: @escaping @MainActor @Sendable (sending T) -> Void,
failure: @escaping @MainActor @Sendable (CoreStoreError) -> Void
) {
nonisolated(unsafe) let transaction = AsynchronousDataTransaction(
@@ -186,6 +186,7 @@ extension DataStack {
/**
Refreshes all registered objects `NSManagedObject`s or `CoreStoreObject`s in the `DataStack`.
*/
@MainActor
public func refreshAndMergeAllObjects() {
Internals.assert(
+6 -5
View File
@@ -90,7 +90,7 @@ extension DataStack {
// MARK: - AddStorageSubscription
fileprivate final class AddStorageSubscription<S: Subscriber>: Subscription, @unchecked Sendable
fileprivate final class AddStorageSubscription<S: Subscriber>: Subscription
where S.Input == Output, S.Failure == CoreStoreError {
// MARK: FilePrivate
@@ -116,14 +116,15 @@ extension DataStack {
return
}
nonisolated(unsafe) var progress: Progress? = nil
nonisolated(unsafe) weak let weakSelf = self as Optional
progress = self.dataStack.addStorage(
self.storage,
completion: { [weak self] result in
completion: { result in
progress?.setProgressHandler(nil)
guard
let self = self,
let self = weakSelf,
let subscriber = self.subscriber
else {
@@ -151,12 +152,12 @@ extension DataStack {
)
if let progress = progress {
Internals.mainActorImmediate { @MainActor [weak self] in
Internals.mainActorImmediate { @MainActor in
progress.setProgressHandler { progress in
guard
let self = self,
let self = weakSelf,
let subscriber = self.subscriber
else {
+48 -49
View File
@@ -32,7 +32,7 @@ import CoreData
/**
The `DataStack` encapsulates the data model for the Core Data stack. Each `DataStack` can have multiple data stores, usually specified as a "Configuration" in the model editor. Behind the scenes, the DataStack manages its own `NSPersistentStoreCoordinator`, a root `NSManagedObjectContext` for disk saves, and a shared `NSManagedObjectContext` designed as a read-only model interface for `NSManagedObjects`.
*/
public final class DataStack: Equatable, @unchecked Sendable {
public final class DataStack: Equatable, Sendable {
/**
The resolved application name, used by the `DataStack` as the default Xcode model name (.xcdatamodel filename) if not explicitly provided.
@@ -398,7 +398,7 @@ public final class DataStack: Equatable, @unchecked Sendable {
- parameter completion: the closure to execute after all persistent stores are removed
*/
public func unsafeRemoveAllPersistentStores(
completion: @escaping @MainActor () -> Void = {}
completion: @escaping @MainActor @Sendable () -> Void = {}
) {
let coordinator = self.coordinator
@@ -466,7 +466,6 @@ public final class DataStack: Equatable, @unchecked Sendable {
internal let mainContext: NSManagedObjectContext
internal let schemaHistory: SchemaHistory
internal let childTransactionQueue = DispatchQueue.serial("com.coreStore.dataStack.childTransactionQueue", qos: .utility)
internal let storeMetadataLock: NSRecursiveLock = .init()
internal let migrationQueue: OperationQueue = Internals.with {
let migrationQueue = OperationQueue()
@@ -487,15 +486,14 @@ public final class DataStack: Equatable, @unchecked Sendable {
}
internal func persistentStores(
for entityIdentifier: Internals.EntityIdentifier
for entityIdentifier: sending Internals.EntityIdentifier
) -> [NSPersistentStore]? {
self.storeMetadataLock.lock()
defer {
self.storeMetadataLock.unlock()
return self.storeMetadataLock.withLockUnchecked { metadata in
return metadata.finalConfigurationsByEntityIdentifier[entityIdentifier]?
.map({ metadata.persistentStoresByFinalConfiguration[$0]! }) ?? []
}
return self.finalConfigurationsByEntityIdentifier[entityIdentifier]?
.map({ self.persistentStoresByFinalConfiguration[$0]! }) ?? []
}
internal func persistentStore(
@@ -504,34 +502,33 @@ public final class DataStack: Equatable, @unchecked Sendable {
inferStoreIfPossible: Bool
) -> (store: NSPersistentStore?, isAmbiguous: Bool) {
self.storeMetadataLock.lock()
defer {
self.storeMetadataLock.unlock()
}
let configurationsForEntity = self.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? []
if let configuration = configuration {
return self.storeMetadataLock.withLockUnchecked { metadata in
let configurationsForEntity = metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? []
if let configuration = configuration {
if configurationsForEntity.contains(configuration) {
if configurationsForEntity.contains(configuration) {
return (store: self.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false)
return (store: metadata.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false)
}
else if !inferStoreIfPossible {
return (store: nil, isAmbiguous: false)
}
}
else if !inferStoreIfPossible {
switch configurationsForEntity.count {
case 0:
return (store: nil, isAmbiguous: false)
case 1 where inferStoreIfPossible:
return (store: metadata.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false)
default:
return (store: nil, isAmbiguous: true)
}
}
switch configurationsForEntity.count {
case 0:
return (store: nil, isAmbiguous: false)
case 1 where inferStoreIfPossible:
return (store: self.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false)
default:
return (store: nil, isAmbiguous: true)
}
}
internal func createPersistentStoreFromStorage(
@@ -550,26 +547,24 @@ public final class DataStack: Equatable, @unchecked Sendable {
do {
self.storeMetadataLock.lock()
defer {
self.storeMetadataLock.unlock()
}
let configurationName = persistentStore.configurationName
self.persistentStoresByFinalConfiguration[configurationName] = persistentStore
for entityDescription in (self.coordinator.managedObjectModel.entities(forConfigurationName: configurationName) ?? []) {
self.storeMetadataLock.withLock { metadata in
let managedObjectClassName = entityDescription.managedObjectClassName!
Internals.assert(
NSClassFromString(managedObjectClassName) != nil,
"The class \(Internals.typeName(managedObjectClassName)) for the entity \(Internals.typeName(entityDescription.name)) does not exist. Check if the subclass type and module name are properly configured."
)
let entityIdentifier = Internals.EntityIdentifier(entityDescription)
if self.finalConfigurationsByEntityIdentifier[entityIdentifier] == nil {
let configurationName = persistentStore.configurationName
metadata.persistentStoresByFinalConfiguration[configurationName] = persistentStore
for entityDescription in (self.coordinator.managedObjectModel.entities(forConfigurationName: configurationName) ?? []) {
self.finalConfigurationsByEntityIdentifier[entityIdentifier] = []
let managedObjectClassName = entityDescription.managedObjectClassName!
Internals.assert(
NSClassFromString(managedObjectClassName) != nil,
"The class \(Internals.typeName(managedObjectClassName)) for the entity \(Internals.typeName(entityDescription.name)) does not exist. Check if the subclass type and module name are properly configured."
)
let entityIdentifier = Internals.EntityIdentifier(entityDescription)
if metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] == nil {
metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] = []
}
metadata.finalConfigurationsByEntityIdentifier[entityIdentifier]?.insert(configurationName)
}
self.finalConfigurationsByEntityIdentifier[entityIdentifier]?.insert(configurationName)
}
}
storage.cs_didAddToDataStack(self)
@@ -586,8 +581,12 @@ public final class DataStack: Equatable, @unchecked Sendable {
// MARK: Private
private var persistentStoresByFinalConfiguration = [String: NSPersistentStore]()
private var finalConfigurationsByEntityIdentifier = [Internals.EntityIdentifier: Set<String>]()
private let storeMetadataLock: Internals.Mutex<
(
persistentStoresByFinalConfiguration: [String: NSPersistentStore],
finalConfigurationsByEntityIdentifier: [Internals.EntityIdentifier: Set<String>]
)
> = .init(([:], [:]))
deinit {
+7 -1
View File
@@ -218,6 +218,7 @@ extension DiffableDataSource {
public func itemID(for indexPath: IndexPath) -> O.ObjectID? {
return self.dispatcher.itemIdentifier(for: indexPath)
.map(O.ObjectID.init(managedObjectID:))
}
/**
@@ -228,7 +229,7 @@ extension DiffableDataSource {
*/
public func indexPath(for itemID: O.ObjectID) -> IndexPath? {
return self.dispatcher.indexPath(for: itemID)
return self.dispatcher.indexPath(for: itemID.managedObjectID)
}
/**
@@ -254,6 +255,11 @@ extension DiffableDataSource {
// MARK: Internal
internal let dispatcher: Internals.DiffableDataUIDispatcher<O>
internal func itemID(for indexPath: IndexPath) -> NSManagedObjectID? {
return self.dispatcher.itemIdentifier(for: indexPath)
}
}
}
@@ -127,7 +127,7 @@ extension DiffableDataSource {
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
guard let objectID = self.itemID(for: indexPath) else {
guard let objectID: NSManagedObjectID = self.itemID(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
@@ -85,7 +85,7 @@ extension DiffableDataSource {
public init(
tableView: UITableView,
dataStack: DataStack,
cellProvider: @escaping @MainActor (UITableView, IndexPath, O) -> UITableViewCell?
cellProvider: @escaping @MainActor @Sendable (UITableView, IndexPath, O) -> UITableViewCell?
) {
self.cellProvider = cellProvider
@@ -150,7 +150,7 @@ extension DiffableDataSource {
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
guard let objectID = self.itemID(for: indexPath) else {
guard let objectID: NSManagedObjectID = self.itemID(for: indexPath) else {
Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list")
}
+4 -1
View File
@@ -220,7 +220,10 @@ extension DiffableDataSource.Target {
}
},
animated: animated,
completion: group.leave
completion: {
group.leave()
}
)
}
}
+41 -13
View File
@@ -32,16 +32,19 @@ import CoreData
/**
All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`.
*/
public protocol DynamicObject: AnyObject {
@_nonSendable
public nonisolated protocol DynamicObject: AnyObject, SendableMetatype {
/**
The object ID for this instance
*/
typealias ObjectID = NSManagedObjectID
typealias ObjectID = DynamicObjectID<Self>
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_forceCreate(
entityDescription: NSEntityDescription,
into context: NSManagedObjectContext,
@@ -51,14 +54,16 @@ public protocol DynamicObject: AnyObject {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_snapshotDictionary(
id: ObjectID,
managedObjectID: NSManagedObjectID,
context: NSManagedObjectContext
) -> [String: Any]?
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_fromRaw(
object: NSManagedObject
) -> Self
@@ -66,6 +71,7 @@ public protocol DynamicObject: AnyObject {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_matches(
object: NSManagedObject
) -> Bool
@@ -73,16 +79,26 @@ public protocol DynamicObject: AnyObject {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
func cs_toRaw() -> NSManagedObject
/**
Used internally by CoreStore. Do not call directly.
*/
func cs_id() -> ObjectID
@_spi(Internals)
func cs_id() -> NSManagedObjectID
}
extension DynamicObject {
// MARK: Public
public func persistentID() -> DynamicObjectID<Self> {
return .init(managedObjectID: self.cs_id())
}
// MARK: Internal
internal func runtimeType() -> Self.Type {
@@ -99,6 +115,7 @@ extension NSManagedObject: DynamicObject {
// MARK: DynamicObject
@_spi(Internals)
public class func cs_forceCreate(
entityDescription: NSEntityDescription,
into context: NSManagedObjectContext,
@@ -112,13 +129,14 @@ extension NSManagedObject: DynamicObject {
}
return object
}
@_spi(Internals)
public class func cs_snapshotDictionary(
id: ObjectID,
managedObjectID: NSManagedObjectID,
context: NSManagedObjectContext
) -> [String: Any]? {
guard let object = context.fetchExisting(id) as NSManagedObject? else {
guard let object = context.fetchExisting(managedObjectID) as NSManagedObject? else {
return nil
}
@@ -131,6 +149,7 @@ extension NSManagedObject: DynamicObject {
return dictionary
}
@_spi(Internals)
public class func cs_fromRaw(object: NSManagedObject) -> Self {
#if swift(>=5.9)
@@ -143,6 +162,7 @@ extension NSManagedObject: DynamicObject {
#endif
}
@_spi(Internals)
public static func cs_matches(
object: NSManagedObject
) -> Bool {
@@ -150,12 +170,14 @@ extension NSManagedObject: DynamicObject {
return object.isKind(of: self)
}
@_spi(Internals)
public func cs_toRaw() -> NSManagedObject {
return self
}
public func cs_id() -> ObjectID {
@_spi(Internals)
public func cs_id() -> NSManagedObjectID {
return self.objectID
}
@@ -168,6 +190,7 @@ extension CoreStoreObject {
// MARK: DynamicObject
@_spi(Internals)
public class func cs_forceCreate(
entityDescription: NSEntityDescription,
into context: NSManagedObjectContext,
@@ -182,9 +205,10 @@ extension CoreStoreObject {
}
return self.cs_fromRaw(object: object)
}
@_spi(Internals)
public class func cs_snapshotDictionary(
id: ObjectID,
managedObjectID: NSManagedObjectID,
context: NSManagedObjectContext
) -> [String: Any]? {
@@ -240,7 +264,7 @@ extension CoreStoreObject {
}
}
}
guard let object = context.fetchExisting(id) as CoreStoreObject? else {
guard let object = context.fetchExisting(managedObjectID) as CoreStoreObject? else {
return nil
}
@@ -253,7 +277,7 @@ extension CoreStoreObject {
else {
guard
let object = context.fetchExisting(id) as CoreStoreObject?,
let object = context.fetchExisting(managedObjectID) as CoreStoreObject?,
let rawObject = object.rawObject,
!rawObject.isDeleted
else {
@@ -292,6 +316,7 @@ extension CoreStoreObject {
return values
}
@_spi(Internals)
public class func cs_fromRaw(object: NSManagedObject) -> Self {
if let coreStoreObject = object.coreStoreObject {
@@ -310,6 +335,7 @@ extension CoreStoreObject {
return coreStoreObject
}
@_spi(Internals)
public static func cs_matches(
object: NSManagedObject
) -> Bool {
@@ -321,12 +347,14 @@ extension CoreStoreObject {
return (self as AnyClass).isSubclass(of: type as AnyClass)
}
@_spi(Internals)
public func cs_toRaw() -> NSManagedObject {
return self.rawObject!
}
public func cs_id() -> ObjectID {
@_spi(Internals)
public func cs_id() -> NSManagedObjectID {
return self.rawObject!.objectID
}
+137
View File
@@ -0,0 +1,137 @@
//
// DynamicObject.swift
// CoreStore
//
// Copyright © 2026 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
// MARK: - DynamicObjectID
public struct DynamicObjectID<O: DynamicObject>: Hashable, ObjectRepresentation, Sendable {
/**
The associated `NSManagedObject` or `CoreStoreObject` entity class
*/
public let entityClass: O.Type
public init(
managedObjectID: NSManagedObjectID
) {
self.init(
entityClass: O.self,
managedObjectID: managedObjectID
)
}
public init(
entityClass: O.Type,
managedObjectID: NSManagedObjectID
) {
Internals.assert(
Internals.EntityIdentifier(entityClass) == Internals.EntityIdentifier(managedObjectID.entity),
"The \(Internals.typeName(managedObjectID)) does not belong to the entity \(Internals.typeName(entityClass))."
)
self.entityClass = entityClass
self.managedObjectID = managedObjectID
}
// MARK: Equatable
public static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.managedObjectID == rhs.managedObjectID
}
// MARK: Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(self.managedObjectID)
}
// MARK: AnyObjectRepresentation
@_spi(Internals)
public func cs_dataStack() -> DataStack? {
return nil
}
@_spi(Internals)
public func cs_id() -> NSManagedObjectID {
return self.managedObjectID
}
// MARK: ObjectRepresentation
public typealias ObjectType = O
public func asPublisher(in dataStack: DataStack) -> ObjectPublisher<ObjectType> {
let context = dataStack.unsafeContext()
return context.objectPublisher(managedObjectID: self.managedObjectID)
}
public func asReadOnly(in dataStack: DataStack) -> ObjectType? {
let context = dataStack.unsafeContext()
return context.fetchExisting(self.managedObjectID)
}
public func asEditable(in transaction: BaseDataTransaction) -> ObjectType? {
let context = transaction.unsafeContext()
return context.fetchExisting(self.managedObjectID)
}
public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot<ObjectType>? {
let context = dataStack.unsafeContext()
return ObjectSnapshot<ObjectType>(managedObjectID: self.managedObjectID, context: context)
}
public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<ObjectType>? {
let context = transaction.unsafeContext()
return ObjectSnapshot<ObjectType>(managedObjectID: self.managedObjectID, context: context)
}
// MARK: Internal
internal let managedObjectID: NSManagedObjectID
internal var entityIdentifier: Internals.EntityIdentifier {
return Internals.EntityIdentifier(self.managedObjectID.entity)
}
}
+2 -1
View File
@@ -35,7 +35,7 @@ import Foundation
- `UnsafeDataModelSchema`: describes models loaded directly from an existing `NSManagedObjectModel`. It is not advisable to continue using this model as its metadata are not available to CoreStore.
- `CoreStoreSchema`: describes models written for `CoreStoreObject` Swift class declarations.
*/
public protocol DynamicSchema {
public protocol DynamicSchema: Sendable {
/**
The version string for this model schema.
@@ -45,5 +45,6 @@ public protocol DynamicSchema {
/**
Do not call this directly. The `NSManagedObjectModel` for this schema may be created lazily and using this method directly may affect the integrity of the model.
*/
@_spi(Internals)
func rawModel() -> NSManagedObjectModel
}
+6
View File
@@ -203,31 +203,37 @@ public /*abstract*/ class DynamicEntity: Hashable {
/**
Do not use directly.
*/
@_spi(Internals)
public let type: DynamicObject.Type
/**
Do not use directly.
*/
@_spi(Internals)
public let entityName: EntityName
/**
Do not use directly.
*/
@_spi(Internals)
public let isAbstract: Bool
/**
Do not use directly.
*/
@_spi(Internals)
public let versionHashModifier: String?
/**
Do not use directly.
*/
@_spi(Internals)
public let indexes: [[KeyPathString]]
/**
Do not use directly.
*/
@_spi(Internals)
public let uniqueConstraints: [[KeyPathString]]
+15 -10
View File
@@ -57,6 +57,7 @@ public protocol FieldRelationshipType {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_toReturnType(
from value: NativeValueType?
) -> Self
@@ -64,6 +65,7 @@ public protocol FieldRelationshipType {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_toPublishedType(
from value: SnapshotValueType,
in context: NSManagedObjectContext
@@ -72,6 +74,7 @@ public protocol FieldRelationshipType {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_toNativeType(
from value: Self
) -> NativeValueType?
@@ -79,6 +82,7 @@ public protocol FieldRelationshipType {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_toSnapshotType(
from value: PublishedType
) -> SnapshotValueType
@@ -86,8 +90,9 @@ public protocol FieldRelationshipType {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
static func cs_valueForSnapshot(
from objectIDs: [DestinationObjectType.ObjectID]
from objectIDs: [NSManagedObjectID]
) -> SnapshotValueType
}
@@ -138,7 +143,7 @@ extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrap
in context: NSManagedObjectContext
) -> PublishedType {
return value.map(context.objectPublisher(objectID:))
return value.map(context.objectPublisher(managedObjectID:))
}
public static func cs_toNativeType(
@@ -152,11 +157,11 @@ extension Optional: FieldRelationshipType, FieldRelationshipToOneType where Wrap
from value: PublishedType
) -> SnapshotValueType {
return value?.objectID()
return value?.cs_id()
}
public static func cs_valueForSnapshot(
from objectIDs: [DestinationObjectType.ObjectID]
from objectIDs: [NSManagedObjectID]
) -> SnapshotValueType {
return objectIDs.first
@@ -194,7 +199,7 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati
in context: NSManagedObjectContext
) -> PublishedType {
return value.map(context.objectPublisher(objectID:))
return value.map(context.objectPublisher(managedObjectID:))
}
public static func cs_toNativeType(
@@ -208,11 +213,11 @@ extension Array: FieldRelationshipType, FieldRelationshipToManyType, FieldRelati
from value: PublishedType
) -> SnapshotValueType {
return value.map({ $0.objectID() })
return value.map({ $0.cs_id() })
}
public static func cs_valueForSnapshot(
from objectIDs: [DestinationObjectType.ObjectID]
from objectIDs: [NSManagedObjectID]
) -> SnapshotValueType {
return objectIDs
@@ -250,7 +255,7 @@ extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelation
in context: NSManagedObjectContext
) -> PublishedType {
return PublishedType(value.map(context.objectPublisher(objectID:)))
return PublishedType(value.map(context.objectPublisher(managedObjectID:)))
}
public static func cs_toNativeType(
@@ -264,11 +269,11 @@ extension Set: FieldRelationshipType, FieldRelationshipToManyType, FieldRelation
from value: PublishedType
) -> SnapshotValueType {
return SnapshotValueType(value.map({ $0.objectID() }))
return SnapshotValueType(value.map({ $0.cs_id() }))
}
public static func cs_valueForSnapshot(
from objectIDs: [DestinationObjectType.ObjectID]
from objectIDs: [NSManagedObjectID]
) -> SnapshotValueType {
return .init(objectIDs)
+1
View File
@@ -41,6 +41,7 @@ public protocol FieldOptionalType: ExpressibleByNilLiteral {
/**
Used internally by CoreStore. Do not call directly.
*/
@_spi(Internals)
var cs_wrappedValue: Wrapped? { get }
}
+8 -8
View File
@@ -59,9 +59,9 @@ extension ForEach where Content: View {
public init<O: DynamicObject>(
_ objectSnapshots: Data,
@ViewBuilder content: @escaping (ObjectSnapshot<O>) -> Content
) where Data.Element == ObjectSnapshot<O>, ID == O.ObjectID {
) where Data.Element == ObjectSnapshot<O>, ID == NSManagedObjectID {
self.init(objectSnapshots, id: \.cs_objectID, content: content)
self.init(objectSnapshots, id: \.managedObjectID, content: content)
}
/**
@@ -89,9 +89,9 @@ extension ForEach where Content: View {
public init<O: DynamicObject>(
objectIn listSnapshot: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data == ListSnapshot<O>, ID == O.ObjectID {
) where Data == ListSnapshot<O>, ID == NSManagedObjectID {
self.init(listSnapshot, id: \.cs_objectID, content: content)
self.init(listSnapshot, id: \.managedObjectID, content: content)
}
/**
@@ -118,9 +118,9 @@ extension ForEach where Content: View {
public init<O: DynamicObject>(
objectIn objectPublishers: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data.Element == ObjectPublisher<O>, ID == O.ObjectID {
) where Data.Element == ObjectPublisher<O>, ID == NSManagedObjectID {
self.init(objectPublishers, id: \.cs_objectID, content: content)
self.init(objectPublishers, id: \.managedObjectID, content: content)
}
/**
@@ -191,9 +191,9 @@ extension ForEach where Content: View {
public init<O: DynamicObject>(
objectIn sectionInfo: Data,
@ViewBuilder content: @escaping (ObjectPublisher<O>) -> Content
) where Data == ListSnapshot<O>.SectionInfo, ID == O.ObjectID {
) where Data == ListSnapshot<O>.SectionInfo, ID == NSManagedObjectID {
self.init(sectionInfo, id: \.cs_objectID, content: content)
self.init(sectionInfo, id: \.managedObjectID, content: content)
}
}
+13 -13
View File
@@ -148,7 +148,7 @@ extension From {
- returns: a `FetchChainBuilder` with closure where the `NSFetchRequest` may be configured
*/
public func tweak(
_ fetchRequest: @escaping (NSFetchRequest<NSFetchRequestResult>) -> Void
_ fetchRequest: @escaping @Sendable (NSFetchRequest<NSFetchRequestResult>) -> Void
) -> FetchChainBuilder<O> {
return self.fetchChain(appending: Tweak(fetchRequest))
@@ -273,7 +273,7 @@ extension From {
*/
public func sectionBy(
_ sectionKeyPath: KeyPathString,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return .init(
@@ -348,7 +348,7 @@ extension From where O: NSManagedObject {
*/
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, T>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -434,7 +434,7 @@ extension From where O: CoreStoreObject {
*/
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, FieldContainer<O>.Stored<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -453,7 +453,7 @@ extension From where O: CoreStoreObject {
*/
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, FieldContainer<O>.Virtual<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -472,7 +472,7 @@ extension From where O: CoreStoreObject {
*/
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, FieldContainer<O>.Coded<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -604,7 +604,7 @@ extension FetchChainBuilder {
- returns: a new `FetchChainBuilder` containing the `Tweak` clause
*/
public func tweak(
_ fetchRequest: @escaping (NSFetchRequest<NSFetchRequestResult>) -> Void
_ fetchRequest: @escaping @Sendable (NSFetchRequest<NSFetchRequestResult>) -> Void
) -> FetchChainBuilder<O> {
return self.fetchChain(appending: Tweak(fetchRequest))
@@ -795,7 +795,7 @@ extension QueryChainBuilder {
- returns: a new `QueryChainBuilder` containing the `Tweak` clause
*/
public func tweak(
_ fetchRequest: @escaping (NSFetchRequest<NSFetchRequestResult>) -> Void
_ fetchRequest: @escaping @Sendable (NSFetchRequest<NSFetchRequestResult>) -> Void
) -> QueryChainBuilder<O, R> {
return self.queryChain(appending: Tweak(fetchRequest))
@@ -1093,7 +1093,7 @@ extension SectionMonitorChainBuilder {
- returns: a new `SectionMonitorChainBuilder` containing the `Tweak` clause
*/
public func tweak(
_ fetchRequest: @escaping (NSFetchRequest<NSFetchRequestResult>) -> Void
_ fetchRequest: @escaping @Sendable (NSFetchRequest<NSFetchRequestResult>) -> Void
) -> SectionMonitorChainBuilder<O> {
return self.sectionMonitorChain(appending: Tweak(fetchRequest))
@@ -1232,7 +1232,7 @@ extension From where O: CoreStoreObject {
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, ValueContainer<O>.Required<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -1243,7 +1243,7 @@ extension From where O: CoreStoreObject {
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, ValueContainer<O>.Optional<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -1254,7 +1254,7 @@ extension From where O: CoreStoreObject {
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, TransformableContainer<O>.Required<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
@@ -1265,7 +1265,7 @@ extension From where O: CoreStoreObject {
public func sectionBy<T>(
_ sectionKeyPath: KeyPath<O, TransformableContainer<O>.Optional<T>>,
sectionIndexTransformer: @escaping (_ sectionName: String?) -> String?
sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String?
) -> SectionMonitorChainBuilder<O> {
return self.sectionBy(
+3 -3
View File
@@ -39,7 +39,7 @@ import CoreData
let person = transaction.fetchOne(From<Person>("Configuration1"))
```
*/
public struct From<O: DynamicObject> {
public struct From<O: DynamicObject>: Sendable {
/**
The associated `NSManagedObject` or `CoreStoreObject` entity class
@@ -162,12 +162,12 @@ public struct From<O: DynamicObject> {
// MARK: Internal
internal let findPersistentStores: (_ context: NSManagedObjectContext) -> [NSPersistentStore]?
internal let findPersistentStores: @Sendable (_ context: NSManagedObjectContext) -> [NSPersistentStore]?
internal init(
entityClass: O.Type,
configurations: [ModelConfiguration]?,
findPersistentStores: @escaping (
findPersistentStores: @escaping @Sendable (
_ context: NSManagedObjectContext
) -> [NSPersistentStore]?
) {
+1 -1
View File
@@ -32,7 +32,7 @@ import CoreData
/**
The `GroupBy` clause specifies that the result of a query be grouped accoording to the specified key path.
*/
public struct GroupBy<O: DynamicObject>: GroupByClause, QueryClause, Hashable {
public struct GroupBy<O: DynamicObject>: GroupByClause, QueryClause, Hashable, Sendable {
/**
Initializes a `GroupBy` clause with an empty list of key path strings
+4 -2
View File
@@ -31,7 +31,7 @@ import CoreData
/**
A storage interface that is backed only in memory.
*/
public final class InMemoryStore: StorageInterface, @unchecked Sendable {
public final class InMemoryStore: StorageInterface {
/**
Initializes an `InMemoryStore` for the specified configuration
@@ -74,6 +74,7 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable {
/**
Do not call directly. Used by the `DataStack` internally.
*/
@_spi(Internals)
public func cs_didAddToDataStack(_ dataStack: DataStack) {
self.dataStack = dataStack
@@ -82,6 +83,7 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable {
/**
Do not call directly. Used by the `DataStack` internally.
*/
@_spi(Internals)
public func cs_didRemoveFromDataStack(_ dataStack: DataStack) {
self.dataStack = nil
@@ -90,5 +92,5 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable {
// MARK: Private
private weak var dataStack: DataStack?
private nonisolated(unsafe) weak var dataStack: DataStack?
}
+2 -2
View File
@@ -39,7 +39,7 @@ extension Internals {
internal typealias Arguments = T
internal typealias Result = U
internal init(_ closure: @escaping (T) -> U) {
internal init(_ closure: @escaping @Sendable (T) -> U) {
self.closure = closure
}
@@ -52,6 +52,6 @@ extension Internals {
// MARK: Private
private let closure: (T) -> U
private let closure: @Sendable (T) -> U
}
}
@@ -32,8 +32,6 @@ import QuartzCore
#endif
import os
// MARK: - Internals
@@ -99,7 +97,7 @@ extension Internals {
return
}
let performDiffingUpdates: @MainActor () -> Void = {
let performDiffingUpdates: @MainActor @Sendable () -> Void = {
let changeset = StagedChangeset(source: self.sections, target: newSections)
performUpdates(
@@ -148,7 +146,7 @@ extension Internals {
return self.sections[section].differenceIdentifier
}
func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? {
func itemIdentifier(for indexPath: IndexPath) -> NSManagedObjectID? {
guard self.sections.indices.contains(indexPath.section) else {
@@ -162,9 +160,9 @@ extension Internals {
return items[indexPath.item].differenceIdentifier
}
func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? {
func indexPath(for itemIdentifier: NSManagedObjectID) -> IndexPath? {
let indexPathMap: [O.ObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in
let indexPathMap: [NSManagedObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in
for (itemIndex, item) in section.element.elements.enumerated() {
@@ -243,7 +241,7 @@ extension Internals {
fileprivate init() {}
fileprivate func dispatch(_ action: @escaping @MainActor () -> Void) {
fileprivate func dispatch(_ action: @escaping @MainActor @Sendable () -> Void) {
let count = self.executingCount.incrementAndGet()
if Thread.isMainThread && count == 1 {
@@ -300,7 +298,7 @@ extension Internals {
// MARK: Private
private let value: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
private let value: Internals.Mutex<Int> = .init(0)
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ extension Internals {
// MARK: - EntityIdentifier
internal struct EntityIdentifier: Hashable {
internal struct EntityIdentifier: Hashable, Sendable {
// MARK: - Category
+115
View File
@@ -0,0 +1,115 @@
//
// Internals.Mutex.swift
// CoreStore
//
// Copyright © 2026 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 Synchronization
import os
// MARK: - Internals
extension Internals {
// MARK: - Mutex
internal struct Mutex<Value: ~Copyable>: ~Copyable, @unchecked Sendable {
// MARK: Internal
init(_ initialValue: consuming sending Value) {
self.storage = .init(initialValue)
}
borrowing func withLock<Result, E>(
_ body: (inout sending Value) throws(E) -> sending Result
) throws(E) -> sending Result
where E: Error, Result: ~Copyable {
let storage = self.storage
storage.lock()
defer {
storage.unlock()
}
return try body(&storage.value)
}
borrowing func withLockUnchecked<Result, E>(
_ body: (inout sending Value) throws(E) -> Result
) throws(E) -> sending Result
where E: Error {
let storage = self.storage
storage.lock()
defer {
storage.unlock()
}
return try body(&storage.value)
}
// MARK: Private
private let storage: Storage
// MARK: - Storage
fileprivate final class Storage {
// MARK: FilePrivate
var value: Value
init(_ initialValue: consuming sending Value) {
self.unfairLock = .allocate(capacity: 1)
self.unfairLock.initialize(to: os_unfair_lock())
self.value = initialValue
}
deinit {
self.unfairLock.deinitialize(count: 1)
self.unfairLock.deallocate()
}
func lock() {
os_unfair_lock_lock(self.unfairLock)
}
func unlock() {
os_unfair_lock_unlock(self.unfairLock)
}
// MARK: Private
private let unfairLock: os_unfair_lock_t
}
}
}
@@ -52,6 +52,27 @@ extension Internals {
using: closure
)
}
init(
notificationName: Notification.Name,
object: Any?,
closure: @escaping @MainActor (_ note: Notification) -> Void
) {
self.observer = NotificationCenter.default.addObserver(
forName: notificationName,
object: object,
queue: .main,
using: { note in
nonisolated(unsafe) let note = note
MainActor.assumeIsolated {
closure(note)
}
}
)
}
deinit {
@@ -66,7 +66,7 @@ extension Internals {
internal func addObserver<U: AnyObject>(
_ observer: U,
closure: @escaping (T) -> Void
closure: @escaping @Sendable (T) -> Void
) {
self.observers.setObject(
+14 -10
View File
@@ -139,15 +139,6 @@ internal enum Internals {
return try ObjectiveC.autoreleasepool(invoking: closure)
}
@inline(__always)
internal static func autoreleasepool<T, E>(
_ closure: () throws(E) -> T
) throws(E) -> T {
return try ObjectiveC.autoreleasepool(invoking: closure)
}
@inline(__always)
internal static func withCheckedThrowingContinuation<T>(
function: String = #function,
@@ -160,6 +151,17 @@ internal enum Internals {
)
}
#if compiler(>=27)
@inline(__always)
internal static func autoreleasepool<T, E>(
_ closure: () throws(E) -> T
) throws(E) -> T {
return try ObjectiveC.autoreleasepool(invoking: closure)
}
@inline(__always)
internal static func withCheckedThrowingContinuation<T, E>(
function: String = #function,
@@ -172,10 +174,12 @@ internal enum Internals {
)
}
#endif
@inline(__always)
internal static func mainActorImmediate(
_ body: @escaping @MainActor () -> Void
_ body: @escaping @MainActor @Sendable () -> Void
) {
if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) {
+1 -1
View File
@@ -39,7 +39,7 @@ import CoreData
let person = transaction.create(Into<MyPersonEntity>("Configuration1"))
```
*/
public struct Into<O: DynamicObject>: Hashable, @unchecked Sendable {
public struct Into<O: DynamicObject>: Hashable, Sendable {
/**
The associated `NSManagedObject` or `CoreStoreObject` entity class
+2 -2
View File
@@ -836,7 +836,7 @@ public func == <O, D: FieldRelationshipToOneType, R: ObjectRepresentation>(
return Where<O>(
O.meta[keyPath: keyPath].keyPath,
isEqualTo: object?.objectID()
isEqualTo: object?.cs_id()
)
}
@@ -870,7 +870,7 @@ public func != <O, D: FieldRelationshipToOneType, R: ObjectRepresentation>(
return !Where<O>(
O.meta[keyPath: keyPath].keyPath,
isEqualTo: object?.objectID()
isEqualTo: object?.cs_id()
)
}

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