From 890e150b9503a78f4dfe12f8da282ae13278ca43 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 15 Jul 2026 11:50:43 +0900 Subject: [PATCH] WIP --- CoreStore.xcodeproj/project.pbxproj | 24 +- CoreStoreTests/BaseTestCase.swift | 6 +- CoreStoreTests/DynamicModelTests.swift | 1 + CoreStoreTests/FetchTests.swift | 44 +- CoreStoreTests/FromTests.swift | 4 + CoreStoreTests/ImportTests.swift | 24 +- CoreStoreTests/ListObserverTests.swift | 414 +++++++------ CoreStoreTests/ObjectObserverTests.swift | 40 +- CoreStoreTests/ObjectPublisherTests.swift | 6 +- CoreStoreTests/QueryTests.swift | 9 + CoreStoreTests/SetupTests.swift | 1 + CoreStoreTests/TransactionTests.swift | 83 ++- CoreStoreTests/TweakTests.swift | 5 +- Demo/Demo.xcodeproj/project.pbxproj | 26 +- Demo/Info.plist | 21 - Demo/Sources/AppDelegate.swift | 33 - Demo/Sources/DemoApp.swift | 15 + .../Advanced.EvolutionDemo.CreatureType.swift | 1 + ...ed.EvolutionDemo.CreaturesDataSource.swift | 135 ++-- .../Advanced.EvolutionDemo.ItemView.swift | 51 +- .../Advanced.EvolutionDemo.ListView.swift | 105 +--- .../Advanced.EvolutionDemo.MainView.swift | 93 ++- .../Advanced.EvolutionDemo.Migrator.swift | 154 +++-- .../Advanced.EvolutionDemo.ProgressView.swift | 178 +++--- .../Advanced.EvolutionDemo.V1.Creature.swift | 15 +- .../Advanced.EvolutionDemo.V2.Creature.swift | 15 +- .../Advanced.EvolutionDemo.V3.Creature.swift | 31 +- .../Advanced.EvolutionDemo.V4.Creature.swift | 33 +- .../Classic.ColorsDemo.DetailView.swift | 38 +- .../Classic.ColorsDemo.LIstView.swift | 50 +- .../Classic.ColorsDemo.MainView.swift | 264 ++++---- .../ColorsDemo/Classic.ColorsDemo.swift | 2 + .../Modern.ColorsDemo.MainView.swift | 150 ++--- ...Modern.ColorsDemo.SwiftUI.DetailView.swift | 41 +- .../Modern.ColorsDemo.SwiftUI.ItemView.swift | 42 +- .../Modern.ColorsDemo.SwiftUI.ListView.swift | 59 +- .../Modern.ColorsDemo.UIKit.DetailView.swift | 36 +- ...olorsDemo.UIKit.DetailViewController.swift | 14 +- .../Modern.ColorsDemo.UIKit.ListView.swift | 60 +- ....ColorsDemo.UIKit.ListViewController.swift | 28 +- .../Modern/ColorsDemo/Modern.ColorsDemo.swift | 3 + .../Modern.PlacemarksDemo.Geocoder.swift | 75 +-- .../Modern.PlacemarksDemo.MainView.swift | 87 +-- .../Modern.PlacemarksDemo.swift | 8 +- .../Modern.PokedexDemo.MainView.swift | 47 +- .../Modern.PokedexDemo.Service.swift | 577 +++++++++--------- .../Modern.PokedexDemo.UIKit.ListView.swift | 35 +- .../PokedexDemo/Modern.PokedexDemo.swift | 6 +- .../Modern.TimeZonesDemo.ItemView.swift | 22 +- .../Modern.TimeZonesDemo.ListView.swift | 28 +- .../Modern.TimeZonesDemo.MainView.swift | 61 +- .../TimeZonesDemo/Modern.TimeZonesDemo.swift | 4 +- Demo/Sources/Helpers/ImageDownloader.swift | 83 +-- Demo/Sources/Helpers/LazyView.swift | 29 - Demo/Sources/Helpers/Menu/Menu.ItemView.swift | 51 +- Demo/Sources/Helpers/Menu/Menu.MainView.swift | 146 ++--- .../Helpers/Menu/Menu.PlaceholderView.swift | 33 +- Demo/Sources/Helpers/Menu/Menu.swift | 197 +++++- Demo/Sources/SceneDelegate.swift | 36 -- Playground_macOS.playground/Contents.swift | 4 +- Sources/AsynchronousDataTransaction.swift | 22 +- Sources/BaseDataTransaction+Querying.swift | 26 + Sources/BaseDataTransaction.swift | 21 + ...reStore+CustomDebugStringConvertible.swift | 4 +- Sources/CoreStoreDefaults.swift | 5 +- Sources/CoreStoreError.swift | 2 +- Sources/CoreStoreObject.swift | 2 + Sources/CoreStoreSchema.swift | 14 +- Sources/CustomSchemaMappingProvider.swift | 11 +- Sources/DataStack+DataSources.swift | 7 +- Sources/DataStack+Migration.swift | 34 +- Sources/DataStack+Observing.swift | 13 + Sources/DataStack+Querying.swift | 26 + Sources/DataStack+Transaction.swift | 7 +- Sources/DataStack.AddStoragePublisher.swift | 11 +- Sources/DataStack.swift | 97 ++- Sources/DiffableDataSource.BaseAdapter.swift | 8 +- ...taSource.CollectionViewAdapter-UIKit.swift | 2 +- ...bleDataSource.TableViewAdapter-UIKit.swift | 4 +- Sources/DiffableDataSource.Target.swift | 5 +- Sources/DynamicObject.swift | 54 +- Sources/DynamicObjectID.swift | 137 +++++ Sources/DynamicSchema.swift | 3 +- Sources/Entity.swift | 6 + Sources/FIeldRelationshipType.swift | 25 +- Sources/FieldOptionalType.swift | 1 + Sources/ForEach+SwiftUI.swift | 16 +- Sources/From+Querying.swift | 26 +- Sources/From.swift | 6 +- Sources/GroupBy.swift | 2 +- Sources/InMemoryStore.swift | 6 +- Sources/Internals.Closure.swift | 4 +- .../Internals.DiffableDataUIDispatcher.swift | 14 +- Sources/Internals.EntityIdentifier.swift | 2 +- Sources/Internals.Mutext.swift | 115 ++++ Sources/Internals.NotificationObserver.swift | 21 + ...Internals.SharedNotificationObserver.swift | 2 +- Sources/Internals.swift | 24 +- Sources/Into.swift | 2 +- Sources/KeyPath+Querying.swift | 4 +- Sources/ListMonitor.swift | 132 ++-- Sources/ListPublisher.SnapshotPublisher.swift | 21 +- Sources/ListPublisher.swift | 5 +- Sources/ListSnapshot.SectionInfo.swift | 7 +- Sources/ListSnapshot.swift | 52 +- Sources/ListState.swift | 45 +- Sources/MigrationChain.swift | 2 +- .../NSManagedObjectContext+CoreStore.swift | 8 +- Sources/NSManagedObjectContext+Querying.swift | 8 + .../NSManagedObjectContext+Transaction.swift | 2 +- Sources/ObjectMonitor.swift | 56 +- Sources/ObjectObserver.swift | 4 +- .../ObjectPublisher.SnapshotPublisher.swift | 21 +- Sources/ObjectPublisher.swift | 97 +-- Sources/ObjectRepresentation.swift | 26 +- Sources/ObjectSnapshot.swift | 59 +- Sources/ObjectState.swift | 19 +- Sources/OrderBy.swift | 4 +- Sources/Progress+Convenience.swift | 13 +- Sources/Relationship.ToManyOrdered.swift | 2 +- Sources/Relationship.ToManyUnordered.swift | 2 +- Sources/Relationship.ToOne.swift | 2 +- Sources/SQLiteStore.swift | 10 +- Sources/SchemaHistory.swift | 28 +- Sources/SchemaMappingProvider.swift | 3 +- Sources/SectionBy.swift | 22 +- Sources/Select.swift | 3 +- Sources/StorageInterface.swift | 4 + Sources/SynchronousDataTransaction.swift | 20 +- Sources/Tweak.swift | 14 +- Sources/UnsafeDataModelSchema.swift | 2 +- Sources/UserInfo.swift | 37 +- Sources/Where.Expression.swift | 4 +- Sources/Where.swift | 4 +- Sources/XcodeDataModelSchema.swift | 9 +- 135 files changed, 2895 insertions(+), 2526 deletions(-) delete mode 100644 Demo/Sources/AppDelegate.swift create mode 100644 Demo/Sources/DemoApp.swift delete mode 100644 Demo/Sources/Helpers/LazyView.swift delete mode 100644 Demo/Sources/SceneDelegate.swift create mode 100644 Sources/DynamicObjectID.swift create mode 100644 Sources/Internals.Mutext.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 697b110..76e70ec 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicObjectID.swift; sourceTree = ""; }; + 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.Mutext.swift; sourceTree = ""; }; 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; diff --git a/CoreStoreTests/BaseTestCase.swift b/CoreStoreTests/BaseTestCase.swift index 13c3968..cb8a7d8 100644 --- a/CoreStoreTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTestCase.swift @@ -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) { diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 0bc9e6a..6271674 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -201,6 +201,7 @@ class Person: CoreStoreObject { class DynamicModelTests: BaseTestDataTestCase { + @MainActor @objc dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() { diff --git a/CoreStoreTests/FetchTests.swift b/CoreStoreTests/FetchTests.swift index aa1a195..26c28e5 100644 --- a/CoreStoreTests/FetchTests.swift +++ b/CoreStoreTests/FetchTests.swift @@ -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() { diff --git a/CoreStoreTests/FromTests.swift b/CoreStoreTests/FromTests.swift index 21dded8..31a08ad 100644 --- a/CoreStoreTests/FromTests.swift +++ b/CoreStoreTests/FromTests.swift @@ -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 diff --git a/CoreStoreTests/ImportTests.swift b/CoreStoreTests/ImportTests.swift index 3111c67..4c98ce2 100644 --- a/CoreStoreTests/ImportTests.swift +++ b/CoreStoreTests/ImportTests.swift @@ -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 { diff --git a/CoreStoreTests/ListObserverTests.swift b/CoreStoreTests/ListObserverTests.swift index c5eb384..0be23e1 100644 --- a/CoreStoreTests/ListObserverTests.swift +++ b/CoreStoreTests/ListObserverTests.swift @@ -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 = .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()) 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 = .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(), @@ -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 = .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 = .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 diff --git a/CoreStoreTests/ObjectObserverTests.swift b/CoreStoreTests/ObjectObserverTests.swift index 2e09cd1..c238341 100644 --- a/CoreStoreTests/ObjectObserverTests.swift +++ b/CoreStoreTests/ObjectObserverTests.swift @@ -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 = .init(initialState: 0) + let events: Internals.Mutex = .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, + 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 = .init(initialState: 0) + let events: Internals.Mutex = .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() diff --git a/CoreStoreTests/ObjectPublisherTests.swift b/CoreStoreTests/ObjectPublisherTests.swift index 1bf8a0b..98191d3 100644 --- a/CoreStoreTests/ObjectPublisherTests.swift +++ b/CoreStoreTests/ObjectPublisherTests.swift @@ -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() diff --git a/CoreStoreTests/QueryTests.swift b/CoreStoreTests/QueryTests.swift index 167692d..3a0cee9 100644 --- a/CoreStoreTests/QueryTests.swift +++ b/CoreStoreTests/QueryTests.swift @@ -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] diff --git a/CoreStoreTests/SetupTests.swift b/CoreStoreTests/SetupTests.swift index 7b56d12..0530d8a 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -27,6 +27,7 @@ import CoreData import XCTest @testable +@_spi(Internals) import CoreStore diff --git a/CoreStoreTests/TransactionTests.swift b/CoreStoreTests/TransactionTests.swift index c63f7d3..289728b 100644 --- a/CoreStoreTests/TransactionTests.swift +++ b/CoreStoreTests/TransactionTests.swift @@ -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 = .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 diff --git a/CoreStoreTests/TweakTests.swift b/CoreStoreTests/TweakTests.swift index d82c40f..b494392 100644 --- a/CoreStoreTests/TweakTests.swift +++ b/CoreStoreTests/TweakTests.swift @@ -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() 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")) } } diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 1368aae..1f0df5e 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -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 = ""; }; B5931B4B251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.EvolutionDemo.V2.FromV3.swift; sourceTree = ""; }; 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 = ""; }; - B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + B5A3911C24E5429200E7E8BD /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; B5A3912024E5429200E7E8BD /* Menu.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.MainView.swift; sourceTree = ""; }; B5A3912224E5429300E7E8BD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -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 = ""; }; B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.Geocoder.swift; sourceTree = ""; }; B5A3917F24E787D900E7E8BD /* InstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsView.swift; sourceTree = ""; }; B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.swift; sourceTree = ""; }; @@ -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; }; diff --git a/Demo/Info.plist b/Demo/Info.plist index 49e8f4a..5b5ff9d 100644 --- a/Demo/Info.plist +++ b/Demo/Info.plist @@ -20,29 +20,8 @@ 1 LSRequiresIPhoneOS - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - UILaunchStoryboardName LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - UIStatusBarTintParameters UINavigationBar diff --git a/Demo/Sources/AppDelegate.swift b/Demo/Sources/AppDelegate.swift deleted file mode 100644 index 907769e..0000000 --- a/Demo/Sources/AppDelegate.swift +++ /dev/null @@ -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 - ) - } -} diff --git a/Demo/Sources/DemoApp.swift b/Demo/Sources/DemoApp.swift new file mode 100644 index 0000000..03b18ef --- /dev/null +++ b/Demo/Sources/DemoApp.swift @@ -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() + } + } +} diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreatureType.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreatureType.swift index a8561e0..1d62b21 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreatureType.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreatureType.swift @@ -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 diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift index ead93d3..2efc9e2 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift @@ -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( + + init( listPublisher: ListPublisher, 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()) for offset in 0 ..< count { - + let object = transaction.create(Into()) 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()) }, 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( - listPublisher: ListPublisher, - 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()) - for offset in 0 ..< count { - - let object = transaction.create(Into()) - 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()) - }, - 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 } } diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift index 402eb06..2d37842 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift @@ -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 diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ListView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ListView.swift index ce3700b..3332d79 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ListView.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ListView.swift @@ -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("Creature") - ] - ) - ) - try! dataStack.addStorageAndWait( - SQLiteStore(fileName: "Advanced.EvolutionDemo.ListView.Preview.sqlite") - ) - try! dataStack.perform( - synchronous: { transaction in - - for dnaCode in 0 ..< 10 as Range { - - let object = transaction.create(Into()) - 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 diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.MainView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.MainView.swift index 072995a..2237762 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.MainView.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.MainView.swift @@ -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 diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift index 2ae71ca..6c906c1 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift @@ -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() + } ) } diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift index 3568cea..3ff66f7 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift @@ -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 diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V1.Creature.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V1.Creature.swift index 36e940f..6b43154 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V1.Creature.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V1.Creature.swift @@ -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() @@ -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() ) } - + static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V1.Creature { - + return transaction.create( Into() ) diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V2.Creature.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V2.Creature.swift index 41e2d00..7fbe3f8 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V2.Creature.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V2.Creature.swift @@ -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() @@ -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() ) } - + static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V2.Creature { - + return transaction.create( Into() ) diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V3.Creature.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V3.Creature.swift index aab4fa4..992ac17 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V3.Creature.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V3.Creature.swift @@ -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() @@ -75,16 +76,16 @@ extension Advanced.EvolutionDemo.V3 { dataStack: dataStack ) } - + static func count(in transaction: BaseDataTransaction) throws -> Int { - + return try transaction.fetchCount( From() ) } - + static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V3.Creature { - + return transaction.create( Into() ) diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V4.Creature.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V4.Creature.swift index da7989d..04d652e 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V4.Creature.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.V4.Creature.swift @@ -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() @@ -71,16 +72,16 @@ extension Advanced.EvolutionDemo.V4 { dataStack: dataStack ) } - + static func count(in transaction: BaseDataTransaction) throws -> Int { - + return try transaction.fetchCount( From() ) } - + static func create(in transaction: BaseDataTransaction) -> Advanced.EvolutionDemo.V4.Creature { - + return transaction.create( Into() ) diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailView.swift index 729e4ad..e1c77df 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailView.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailView.swift @@ -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 { @@ -48,32 +47,3 @@ extension Classic.ColorsDemo { private let palette: ObjectMonitor } } - -#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())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Classic.ColorsDemo.DetailView( - Classic.ColorsDemo.dataStack.monitorObject( - Classic.ColorsDemo.palettesMonitor[0, 0] - ) - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift index b958685..84a4f28 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift @@ -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())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Classic.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return } - ) - return Classic.ColorsDemo.ListView( - listMonitor: Classic.ColorsDemo.palettesMonitor, - onPaletteTapped: { _ in } - ) - } -} + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Classic.ColorsDemo.ListView( + listMonitor: Classic.ColorsDemo.palettesMonitor, + onPaletteTapped: { _ in } + ) +} diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift index a40a803..99cf696 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift @@ -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 - - @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) { - + listMonitor.addObserver(self) self.count = listMonitor.numberOfObjects() } - + fileprivate func selectedObject() -> ObjectMonitor? { - - 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) { - - self.objectWillChange.send() - self.count = monitor.numberOfObjects() - } - - func listMonitorDidRefetch(_ monitor: ListMonitor) { - - self.objectWillChange.send() - self.count = monitor.numberOfObjects() - } - - // MARK: ListObjectObserver - - func listMonitor(_ monitor: ListMonitor, 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) { + let count = monitor.numberOfObjects() + + Task { @MainActor in + self.count = count } } - - + + nonisolated func listMonitorDidRefetch(_ monitor: ListMonitor) { + let count = monitor.numberOfObjects() + + Task { @MainActor in + self.count = count + } + } + + // MARK: ListObjectObserver + + nonisolated func listMonitor( + _ monitor: ListMonitor, + 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())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } - } - ) - return Classic.ColorsDemo.MainView() - } -} - -#endif diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.swift index a0122a0..6006c00 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.swift @@ -38,6 +38,7 @@ extension Classic { return dataStack }() + @MainActor static let palettesMonitor: ListMonitor = Classic.ColorsDemo.dataStack.monitorSectionedList( From() .sectionBy(\.colorGroup) @@ -45,6 +46,7 @@ extension Classic { .orderBy(.ascending(\.hue)) ) + @MainActor static var filter: Classic.ColorsDemo.Filter = .all { didSet { diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift index 47a4278..eb81d40 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -10,7 +10,7 @@ import SwiftUI extension Modern.ColorsDemo { // MARK: - Modern.ColorsDemo.MainView - + struct MainView: View { // MARK: Internal @@ -20,7 +20,8 @@ extension Modern.ColorsDemo { _ listPublisher: ListPublisher, _ onPaletteTapped: @escaping (ObjectPublisher) -> Void ) -> ListView, - detailView: @escaping (ObjectPublisher) -> DetailView) { + detailView: @escaping (ObjectPublisher) -> 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()) }, sourceIdentifier: TransactionSource.clear, @@ -108,10 +118,10 @@ extension Modern.ColorsDemo { } private func addColor() { - + Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + _ = transaction.create(Into()) }, 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()) { - + 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())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + 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..()) + 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 diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift index b83e1b8..5e74d66 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift @@ -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) { - + 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())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Modern.ColorsDemo.SwiftUI.DetailView( - Modern.ColorsDemo.palettesPublisher.snapshot.first! - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift index 850813f..ed2e044 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift @@ -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())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Modern.ColorsDemo.SwiftUI.ItemView( - Modern.ColorsDemo.palettesPublisher.snapshot.first! - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift index e11a4cf..52149d0 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift @@ -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())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return } - ) - return Modern.ColorsDemo.SwiftUI.ListView( - listPublisher: Modern.ColorsDemo.palettesPublisher, - onPaletteTapped: { _ in } - ) - } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: Modern.ColorsDemo.palettesPublisher, + onPaletteTapped: { _ in } + ) } - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift index bedbe52..b12e6f1 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift @@ -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 } } - -#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())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Modern.ColorsDemo.UIKit.DetailView( - Modern.ColorsDemo.palettesPublisher.snapshot.first! - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift index b2a5077..afb1ed1 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift @@ -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, - didUpdateObject object: Modern.ColorsDemo.Palette, + didUpdateObject object: sending Modern.ColorsDemo.Palette, changedPersistentKeys: Set, 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) { diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift index 1965bc8..15b4e15 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift @@ -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())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return } - ) - return Modern.ColorsDemo.UIKit.ListView( - listPublisher: Modern.ColorsDemo.palettesPublisher, - onPaletteTapped: { _ in } - ) - } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.UIKit.ListView( + listPublisher: Modern.ColorsDemo.palettesPublisher, + onPaletteTapped: { _ in } + ) } - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift index 46e5e6f..e82b46f 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift @@ -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 { // 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() } diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift index 3becb11..f31172c 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift @@ -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.dataStack.publishList( From() .sectionBy( @@ -54,6 +56,7 @@ extension Modern { .orderBy(.ascending(\.$hue)) ) + @MainActor static var filter: Modern.ColorsDemo.Filter = .all { didSet { diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift index a8c3934..ef4094a 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift @@ -13,51 +13,58 @@ extension Modern.PlacemarksDemo { // MARK: Geocoder + @MainActor final class Geocoder { // MARK: Internal func geocode( - place: ObjectSnapshot, - completion: @escaping (_ title: String?, _ subtitle: String?) -> Void - ) { + place: ObjectSnapshot + ) 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 diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift index dcc896f..7d5dfff 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift @@ -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? - 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 diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift index 86764c1..7fc7c21 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift @@ -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 = { let dataStack = Modern.PlacemarksDemo.dataStack diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift index c84f5d3..a25b7d2 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -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: 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 diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index a365b18..37cd65d 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -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 { + 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 = try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) - ) - let results: [Dictionary] = try self.parseJSON( - json["results"] - ) - _ = try transaction.importUniqueObjects( - Into(), - 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 = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) + ) + let results: [Dictionary] = try self.parseJSON( + json["results"] + ) + _ = try transaction.importUniqueObjects( + Into(), + 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, - from output: URLSession.DataTaskPublisher.Output - ) -> Future, Modern.PokedexDemo.Service.Error> { + for detailsObjectID: NSManagedObjectID, + from data: Data + ) async throws -> ObjectSnapshot { - 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 = try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) - ) - guard - let species = try transaction.importUniqueObject( - Into(), - 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 = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) ) + guard + let species = try transaction.importUniqueObject( + Into(), + source: json + ) + else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + transaction + .edit(Into(), 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, - from outputs: [URLSession.DataTaskPublisher.Output] - ) -> Future { + 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(), - 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(), + 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(), 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( + + static nonisolated func parseJSON( _ 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( + + static nonisolated func parseJSON( _ 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) { - self.fetchSpeciesIfNeeded(for: pokedexEntry) - } - - - // MARK: Private - - private var cancellable: Dictionary = [:] - - private lazy var pokedexEntries: AnyPublisher = 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) { - 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) { - - 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? + + @ObservationIgnored + private var detailTasks: [String: Task] = [:] + + 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 + ) { + + 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) diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift index e4c2cd7..eebaa6d 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift @@ -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() @@ -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 } } -#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 diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift index a472b33..c86a455 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift @@ -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.dataStack.publishList( From() .orderBy(.ascending(\.$index)) diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift index 6361938..7b336aa 100644 --- a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ItemView.swift @@ -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 diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift index 3037da0..fc86b8b 100644 --- a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.ListView.swift @@ -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() - .orderBy(.ascending(\.$name)) - ) - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift index 8f06df4..298f97d 100644 --- a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.MainView.swift @@ -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() .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 diff --git a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift index a58e93f..7aaf6a2 100644 --- a/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift +++ b/Demo/Sources/Demos/Modern/TimeZonesDemo/Modern.TimeZonesDemo.swift @@ -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 diff --git a/Demo/Sources/Helpers/ImageDownloader.swift b/Demo/Sources/Helpers/ImageDownloader.swift index bdd3ae6..aeb301a 100644 --- a/Demo/Sources/Helpers/ImageDownloader.swift +++ b/Demo/Sources/Helpers/ImageDownloader.swift @@ -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? } diff --git a/Demo/Sources/Helpers/LazyView.swift b/Demo/Sources/Helpers/LazyView.swift deleted file mode 100644 index fd74aa8..0000000 --- a/Demo/Sources/Helpers/LazyView.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Demo -// Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. - -import SwiftUI - -// MARK: - LazyView - -struct LazyView: View { - - // MARK: Internal - - init(_ load: @escaping () -> Content) { - - self.load = load - } - - - // MARK: View - - var body: Content { - - self.load() - } - - // MARK: Private - - private let load: () -> Content -} diff --git a/Demo/Sources/Helpers/Menu/Menu.ItemView.swift b/Demo/Sources/Helpers/Menu/Menu.ItemView.swift index 4f208fc..a070ff2 100644 --- a/Demo/Sources/Helpers/Menu/Menu.ItemView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.ItemView.swift @@ -10,34 +10,36 @@ extension Menu { // MARK: - Menu.ItemView - struct ItemView: 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 diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift index 36a3129..4373f77 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -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 diff --git a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift index 9618327..03f7bc9 100644 --- a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift @@ -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 diff --git a/Demo/Sources/Helpers/Menu/Menu.swift b/Demo/Sources/Helpers/Menu/Menu.swift index 34de26d..32fd476 100644 --- a/Demo/Sources/Helpers/Menu/Menu.swift +++ b/Demo/Sources/Helpers/Menu/Menu.swift @@ -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() + } + } + } +} diff --git a/Demo/Sources/SceneDelegate.swift b/Demo/Sources/SceneDelegate.swift deleted file mode 100644 index afb6e32..0000000 --- a/Demo/Sources/SceneDelegate.swift +++ /dev/null @@ -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() - } -} diff --git a/Playground_macOS.playground/Contents.swift b/Playground_macOS.playground/Contents.swift index 05f6fa7..12a5d1a 100644 --- a/Playground_macOS.playground/Contents.swift +++ b/Playground_macOS.playground/Contents.swift @@ -41,11 +41,11 @@ let dataStack = DataStack( Entity("Animal"), Entity("Person"), Entity("Dog") - ]/*, + ], versionLock: [ "Animal": [0x4a201cc685d53c0a, 0x16e6c3b561577875, 0xb032e2da61c792a0, 0xa133b801051acee4], "Person": [0xca938eea1af4bd56, 0xbca30994506356ad, 0x7a7cc655898816ef, 0x1a4551ffedc9b214] - ]*/ + ] ) ) dataStack.addStorage( diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index ce01227..4e5546c 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -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( + _ persistentID: DynamicObjectID? + ) -> 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 diff --git a/Sources/BaseDataTransaction+Querying.swift b/Sources/BaseDataTransaction+Querying.swift index c74eaa0..78a217a 100644 --- a/Sources/BaseDataTransaction+Querying.swift +++ b/Sources/BaseDataTransaction+Querying.swift @@ -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( + _ persistentID: DynamicObjectID + ) -> 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( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return self.context.fetchExisting(objectIDs) + } + /** Fetches the `DynamicObject` instances in the transaction's context from a list of `NSManagedObjectID`. diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index df88ee7..ee65c71 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -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( + _ persistentID: DynamicObjectID? + ) -> 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`. diff --git a/Sources/CoreStore+CustomDebugStringConvertible.swift b/Sources/CoreStore+CustomDebugStringConvertible.swift index e362e26..5d1e034 100644 --- a/Sources/CoreStore+CustomDebugStringConvertible.swift +++ b/Sources/CoreStore+CustomDebugStringConvertible.swift @@ -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()) ) } diff --git a/Sources/CoreStoreDefaults.swift b/Sources/CoreStoreDefaults.swift index f66dec4..df9d7f9 100644 --- a/Sources/CoreStoreDefaults.swift +++ b/Sources/CoreStoreDefaults.swift @@ -24,7 +24,6 @@ // import Foundation -import os // MARK: - CoreStoreDefaults @@ -94,6 +93,6 @@ public enum CoreStoreDefaults { // MARK: Private - private static let defaultStackInstance: OSAllocatedUnfairLock = .init(initialState: nil) - private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil) + private static let defaultStackInstance: Internals.Mutex = .init(nil) + private static let loggerInstance: Internals.Mutex<(any CoreStoreLogger)?> = .init(nil) } diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index 4f463e0..136b920 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -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. diff --git a/Sources/CoreStoreObject.swift b/Sources/CoreStoreObject.swift index e848c85..2307beb 100644 --- a/Sources/CoreStoreObject.swift +++ b/Sources/CoreStoreObject.swift @@ -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 diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index 7f5afb9..6196113 100644 --- a/Sources/CoreStoreSchema.swift +++ b/Sources/CoreStoreSchema.swift @@ -257,20 +257,20 @@ public final class CoreStoreSchema: DynamicSchema { // MARK: Internal - internal let entitiesByConfiguration: [String: Set] + internal nonisolated(unsafe) let entitiesByConfiguration: [String: Set] // MARK: Private private static let barrierQueue = DispatchQueue.concurrent("com.coreStore.coreStoreDataModelBarrierQueue", qos: .userInteractive) - private let allEntities: Set + private nonisolated(unsafe) let allEntities: Set - 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, diff --git a/Sources/CustomSchemaMappingProvider.swift b/Sources/CustomSchemaMappingProvider.swift index d75f8db..3fdd599 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -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) + } ) ) } diff --git a/Sources/DataStack+DataSources.swift b/Sources/DataStack+DataSources.swift index 617ef19..b3dedfe 100644 --- a/Sources/DataStack+DataSources.swift +++ b/Sources/DataStack+DataSources.swift @@ -42,8 +42,9 @@ extension DataStack { public func publishObject( _ object: O ) -> ObjectPublisher { - - return self.publishObject(object.cs_id()) + + let context = self.unsafeContext() + return context.objectPublisher(managedObjectID: object.cs_id()) } /** @@ -57,7 +58,7 @@ extension DataStack { ) -> ObjectPublisher { let context = self.unsafeContext() - return context.objectPublisher(objectID: objectID) + return context.objectPublisher(managedObjectID: objectID.managedObjectID) } /** diff --git a/Sources/DataStack+Migration.swift b/Sources/DataStack+Migration.swift index 14e5005..c7251f0 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -24,8 +24,7 @@ // import Foundation -@preconcurrency import CoreData -import os +import CoreData // MARK: - DataStack @@ -50,7 +49,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping @MainActor (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) { self.coordinator.performAsynchronously { @@ -111,7 +110,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping @MainActor (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) -> Progress? { let fileURL = storage.fileURL @@ -274,7 +273,7 @@ extension DataStack { */ public func upgradeStorageIfNeeded( _ 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( _ 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 = .init(initialState: 0) + let fakeProgress: Internals.Mutex = .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 diff --git a/Sources/DataStack+Observing.swift b/Sources/DataStack+Observing.swift index 2ec7f30..8dc9572 100644 --- a/Sources/DataStack+Observing.swift +++ b/Sources/DataStack+Observing.swift @@ -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( _ object: O ) -> ObjectMonitor { @@ -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( _ from: From, _ 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( _ from: From, _ 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( _ clauseChain: B ) -> ListMonitor { @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> 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( _ from: From, _ sectionBy: SectionBy, @@ -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( _ from: From, _ sectionBy: SectionBy, @@ -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( _ clauseChain: B ) -> ListMonitor { @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -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( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B diff --git a/Sources/DataStack+Querying.swift b/Sources/DataStack+Querying.swift index 6c48c15..f1cbb59 100644 --- a/Sources/DataStack+Querying.swift +++ b/Sources/DataStack+Querying.swift @@ -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( + _ persistentID: DynamicObjectID + ) -> 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( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return self.mainContext.fetchExisting(objectIDs) + } + /** Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `NSManagedObjectID`. diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index 2994b44..68946fd 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -43,7 +43,7 @@ extension DataStack { _ transaction: AsynchronousDataTransaction ) throws(any Swift.Error) -> T, sourceIdentifier: (any Sendable)? = nil, - completion: @escaping @Sendable (AsynchronousDataTransaction.Result) -> Void + completion: @escaping @MainActor @Sendable (AsynchronousDataTransaction.Result) -> 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( diff --git a/Sources/DataStack.AddStoragePublisher.swift b/Sources/DataStack.AddStoragePublisher.swift index 62caaac..b12d006 100644 --- a/Sources/DataStack.AddStoragePublisher.swift +++ b/Sources/DataStack.AddStoragePublisher.swift @@ -90,7 +90,7 @@ extension DataStack { // MARK: - AddStorageSubscription - fileprivate final class AddStorageSubscription: Subscription, @unchecked Sendable + fileprivate final class AddStorageSubscription: 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 { diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index be5a3f7..5c5c237 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -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]() + private let storeMetadataLock: Internals.Mutex< + ( + persistentStoresByFinalConfiguration: [String: NSPersistentStore], + finalConfigurationsByEntityIdentifier: [Internals.EntityIdentifier: Set] + ) + > = .init(([:], [:])) deinit { diff --git a/Sources/DiffableDataSource.BaseAdapter.swift b/Sources/DiffableDataSource.BaseAdapter.swift index c4ced63..f87b68a 100644 --- a/Sources/DiffableDataSource.BaseAdapter.swift +++ b/Sources/DiffableDataSource.BaseAdapter.swift @@ -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 + + internal func itemID(for indexPath: IndexPath) -> NSManagedObjectID? { + + return self.dispatcher.itemIdentifier(for: indexPath) + } } } diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift index 73014d1..95e8375 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift @@ -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") } diff --git a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift index a3f3936..5abf3f2 100644 --- a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift +++ b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift @@ -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") } diff --git a/Sources/DiffableDataSource.Target.swift b/Sources/DiffableDataSource.Target.swift index 961d5ca..a4717ab 100644 --- a/Sources/DiffableDataSource.Target.swift +++ b/Sources/DiffableDataSource.Target.swift @@ -220,7 +220,10 @@ extension DiffableDataSource.Target { } }, animated: animated, - completion: group.leave + completion: { + + group.leave() + } ) } } diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index d0e42a2..fb95c46 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -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 + /** 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 { + + 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 } diff --git a/Sources/DynamicObjectID.swift b/Sources/DynamicObjectID.swift new file mode 100644 index 0000000..a2fb43f --- /dev/null +++ b/Sources/DynamicObjectID.swift @@ -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: 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 { + + 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? { + + let context = dataStack.unsafeContext() + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) + } + + public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { + + let context = transaction.unsafeContext() + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) + } + + + // MARK: Internal + + internal let managedObjectID: NSManagedObjectID + + internal var entityIdentifier: Internals.EntityIdentifier { + + return Internals.EntityIdentifier(self.managedObjectID.entity) + } +} diff --git a/Sources/DynamicSchema.swift b/Sources/DynamicSchema.swift index ab7cfb1..60c7dd5 100644 --- a/Sources/DynamicSchema.swift +++ b/Sources/DynamicSchema.swift @@ -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 } diff --git a/Sources/Entity.swift b/Sources/Entity.swift index 0982efd..05dc5b6 100644 --- a/Sources/Entity.swift +++ b/Sources/Entity.swift @@ -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]] diff --git a/Sources/FIeldRelationshipType.swift b/Sources/FIeldRelationshipType.swift index 6825db7..013e205 100644 --- a/Sources/FIeldRelationshipType.swift +++ b/Sources/FIeldRelationshipType.swift @@ -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) diff --git a/Sources/FieldOptionalType.swift b/Sources/FieldOptionalType.swift index f9f07b6..adfed81 100644 --- a/Sources/FieldOptionalType.swift +++ b/Sources/FieldOptionalType.swift @@ -41,6 +41,7 @@ public protocol FieldOptionalType: ExpressibleByNilLiteral { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) var cs_wrappedValue: Wrapped? { get } } diff --git a/Sources/ForEach+SwiftUI.swift b/Sources/ForEach+SwiftUI.swift index 4a027ec..06f5977 100644 --- a/Sources/ForEach+SwiftUI.swift +++ b/Sources/ForEach+SwiftUI.swift @@ -59,9 +59,9 @@ extension ForEach where Content: View { public init( _ objectSnapshots: Data, @ViewBuilder content: @escaping (ObjectSnapshot) -> Content - ) where Data.Element == ObjectSnapshot, ID == O.ObjectID { + ) where Data.Element == ObjectSnapshot, 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( objectIn listSnapshot: Data, @ViewBuilder content: @escaping (ObjectPublisher) -> Content - ) where Data == ListSnapshot, ID == O.ObjectID { + ) where Data == ListSnapshot, 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( objectIn objectPublishers: Data, @ViewBuilder content: @escaping (ObjectPublisher) -> Content - ) where Data.Element == ObjectPublisher, ID == O.ObjectID { + ) where Data.Element == ObjectPublisher, 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( objectIn sectionInfo: Data, @ViewBuilder content: @escaping (ObjectPublisher) -> Content - ) where Data == ListSnapshot.SectionInfo, ID == O.ObjectID { + ) where Data == ListSnapshot.SectionInfo, ID == NSManagedObjectID { - self.init(sectionInfo, id: \.cs_objectID, content: content) + self.init(sectionInfo, id: \.managedObjectID, content: content) } } diff --git a/Sources/From+Querying.swift b/Sources/From+Querying.swift index cc8e38f..57b101c 100644 --- a/Sources/From+Querying.swift +++ b/Sources/From+Querying.swift @@ -148,7 +148,7 @@ extension From { - returns: a `FetchChainBuilder` with closure where the `NSFetchRequest` may be configured */ public func tweak( - _ fetchRequest: @escaping (NSFetchRequest) -> Void + _ fetchRequest: @escaping @Sendable (NSFetchRequest) -> Void ) -> FetchChainBuilder { 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 { return .init( @@ -348,7 +348,7 @@ extension From where O: NSManagedObject { */ public func sectionBy( _ sectionKeyPath: KeyPath, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -434,7 +434,7 @@ extension From where O: CoreStoreObject { */ public func sectionBy( _ sectionKeyPath: KeyPath.Stored>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -453,7 +453,7 @@ extension From where O: CoreStoreObject { */ public func sectionBy( _ sectionKeyPath: KeyPath.Virtual>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -472,7 +472,7 @@ extension From where O: CoreStoreObject { */ public func sectionBy( _ sectionKeyPath: KeyPath.Coded>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -604,7 +604,7 @@ extension FetchChainBuilder { - returns: a new `FetchChainBuilder` containing the `Tweak` clause */ public func tweak( - _ fetchRequest: @escaping (NSFetchRequest) -> Void + _ fetchRequest: @escaping @Sendable (NSFetchRequest) -> Void ) -> FetchChainBuilder { 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) -> Void + _ fetchRequest: @escaping @Sendable (NSFetchRequest) -> Void ) -> QueryChainBuilder { 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) -> Void + _ fetchRequest: @escaping @Sendable (NSFetchRequest) -> Void ) -> SectionMonitorChainBuilder { return self.sectionMonitorChain(appending: Tweak(fetchRequest)) @@ -1232,7 +1232,7 @@ extension From where O: CoreStoreObject { public func sectionBy( _ sectionKeyPath: KeyPath.Required>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -1243,7 +1243,7 @@ extension From where O: CoreStoreObject { public func sectionBy( _ sectionKeyPath: KeyPath.Optional>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -1254,7 +1254,7 @@ extension From where O: CoreStoreObject { public func sectionBy( _ sectionKeyPath: KeyPath.Required>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( @@ -1265,7 +1265,7 @@ extension From where O: CoreStoreObject { public func sectionBy( _ sectionKeyPath: KeyPath.Optional>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) -> SectionMonitorChainBuilder { return self.sectionBy( diff --git a/Sources/From.swift b/Sources/From.swift index f00fb2e..43fcfa5 100644 --- a/Sources/From.swift +++ b/Sources/From.swift @@ -39,7 +39,7 @@ import CoreData let person = transaction.fetchOne(From("Configuration1")) ``` */ -public struct From { +public struct From: Sendable { /** The associated `NSManagedObject` or `CoreStoreObject` entity class @@ -162,12 +162,12 @@ public struct From { // 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]? ) { diff --git a/Sources/GroupBy.swift b/Sources/GroupBy.swift index 6aa347f..bef6f23 100644 --- a/Sources/GroupBy.swift +++ b/Sources/GroupBy.swift @@ -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: GroupByClause, QueryClause, Hashable { +public struct GroupBy: GroupByClause, QueryClause, Hashable, Sendable { /** Initializes a `GroupBy` clause with an empty list of key path strings diff --git a/Sources/InMemoryStore.swift b/Sources/InMemoryStore.swift index 8794d72..7297d1d 100644 --- a/Sources/InMemoryStore.swift +++ b/Sources/InMemoryStore.swift @@ -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? } diff --git a/Sources/Internals.Closure.swift b/Sources/Internals.Closure.swift index d904852..628801f 100644 --- a/Sources/Internals.Closure.swift +++ b/Sources/Internals.Closure.swift @@ -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 } } diff --git a/Sources/Internals.DiffableDataUIDispatcher.swift b/Sources/Internals.DiffableDataUIDispatcher.swift index f9777a5..b6d783c 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.swift @@ -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 = .init(initialState: 0) + private let value: Internals.Mutex = .init(0) } } diff --git a/Sources/Internals.EntityIdentifier.swift b/Sources/Internals.EntityIdentifier.swift index 8129efe..0730433 100644 --- a/Sources/Internals.EntityIdentifier.swift +++ b/Sources/Internals.EntityIdentifier.swift @@ -33,7 +33,7 @@ extension Internals { // MARK: - EntityIdentifier - internal struct EntityIdentifier: Hashable { + internal struct EntityIdentifier: Hashable, Sendable { // MARK: - Category diff --git a/Sources/Internals.Mutext.swift b/Sources/Internals.Mutext.swift new file mode 100644 index 0000000..d322129 --- /dev/null +++ b/Sources/Internals.Mutext.swift @@ -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: ~Copyable, @unchecked Sendable { + + // MARK: Internal + + init(_ initialValue: consuming sending Value) { + + self.storage = .init(initialValue) + } + + borrowing func withLock( + _ 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( + _ 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 + } + } +} diff --git a/Sources/Internals.NotificationObserver.swift b/Sources/Internals.NotificationObserver.swift index e6f3670..72cac2f 100644 --- a/Sources/Internals.NotificationObserver.swift +++ b/Sources/Internals.NotificationObserver.swift @@ -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 { diff --git a/Sources/Internals.SharedNotificationObserver.swift b/Sources/Internals.SharedNotificationObserver.swift index 2a56c0a..b70e714 100644 --- a/Sources/Internals.SharedNotificationObserver.swift +++ b/Sources/Internals.SharedNotificationObserver.swift @@ -66,7 +66,7 @@ extension Internals { internal func addObserver( _ observer: U, - closure: @escaping (T) -> Void + closure: @escaping @Sendable (T) -> Void ) { self.observers.setObject( diff --git a/Sources/Internals.swift b/Sources/Internals.swift index a281d70..eeab985 100644 --- a/Sources/Internals.swift +++ b/Sources/Internals.swift @@ -139,15 +139,6 @@ internal enum Internals { return try ObjectiveC.autoreleasepool(invoking: closure) } - @inline(__always) - internal static func autoreleasepool( - _ closure: () throws(E) -> T - ) throws(E) -> T { - - return try ObjectiveC.autoreleasepool(invoking: closure) - } - - @inline(__always) internal static func withCheckedThrowingContinuation( function: String = #function, @@ -160,6 +151,17 @@ internal enum Internals { ) } + +#if compiler(>=27) + + @inline(__always) + internal static func autoreleasepool( + _ closure: () throws(E) -> T + ) throws(E) -> T { + + return try ObjectiveC.autoreleasepool(invoking: closure) + } + @inline(__always) internal static func withCheckedThrowingContinuation( 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, *) { diff --git a/Sources/Into.swift b/Sources/Into.swift index eba923d..341c6d7 100644 --- a/Sources/Into.swift +++ b/Sources/Into.swift @@ -39,7 +39,7 @@ import CoreData let person = transaction.create(Into("Configuration1")) ``` */ -public struct Into: Hashable, @unchecked Sendable { +public struct Into: Hashable, Sendable { /** The associated `NSManagedObject` or `CoreStoreObject` entity class diff --git a/Sources/KeyPath+Querying.swift b/Sources/KeyPath+Querying.swift index 6f67009..16708a1 100644 --- a/Sources/KeyPath+Querying.swift +++ b/Sources/KeyPath+Querying.swift @@ -836,7 +836,7 @@ public func == ( return Where( O.meta[keyPath: keyPath].keyPath, - isEqualTo: object?.objectID() + isEqualTo: object?.cs_id() ) } @@ -870,7 +870,7 @@ public func != ( return !Where( O.meta[keyPath: keyPath].keyPath, - isEqualTo: object?.objectID() + isEqualTo: object?.cs_id() ) } diff --git a/Sources/ListMonitor.swift b/Sources/ListMonitor.swift index e5053be..828b5f6 100644 --- a/Sources/ListMonitor.swift +++ b/Sources/ListMonitor.swift @@ -66,7 +66,7 @@ import CoreData ``` In the example above, both `person1` and `person2` will contain the object at section=2, index=3. */ -public final class ListMonitor: Hashable, @unchecked Sendable { +public final class ListMonitor: Hashable, Sendable { // MARK: Public (Accessors) @@ -386,8 +386,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -395,28 +397,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -433,8 +435,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObjectObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -442,28 +446,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -475,7 +479,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertObject: object, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteObject: { (observer, monitor, object, fromIndexPath) in @@ -484,7 +488,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteObject: object, fromIndexPath: fromIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didUpdateObject: { (observer, monitor, object, atIndexPath) in @@ -493,7 +497,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didUpdateObject: object, atIndexPath: atIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didMoveObject: { (observer, monitor, object, fromIndexPath, toIndexPath) in @@ -503,7 +507,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable didMoveObject: object, fromIndexPath: fromIndexPath, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -520,8 +524,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListSectionObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -529,28 +535,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -562,7 +568,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertObject: object, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteObject: { (observer, monitor, object, fromIndexPath) in @@ -571,7 +577,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteObject: object, fromIndexPath: fromIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didUpdateObject: { (observer, monitor, object, atIndexPath) in @@ -580,7 +586,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didUpdateObject: object, atIndexPath: atIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didMoveObject: { (observer, monitor, object, fromIndexPath, toIndexPath) in @@ -590,7 +596,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable didMoveObject: object, fromIndexPath: fromIndexPath, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -602,7 +608,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertSection: sectionInfo, toSectionIndex: toIndex, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteSection: { (observer, monitor, sectionInfo, fromIndex) in @@ -611,7 +617,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteSection: sectionInfo, fromSectionIndex: fromIndex, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -624,6 +630,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObserver` to unregister notifications to */ + @MainActor public func removeObserver(_ observer: U) where U.ListEntityType == O { self.unregisterObserver(observer) @@ -635,7 +642,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable /** Returns `true` if a call to `refetch(...)` was made to the `ListMonitor` and is currently waiting for the fetching to complete. Returns `false` otherwise. */ - public private(set) var isPendingRefetch = false + public nonisolated(unsafe) private(set) var isPendingRefetch = false /** Asks the `ListMonitor` to refetch its objects using the specified series of `FetchClause`s. Note that this method does not execute the fetch immediately; the actual fetching will happen after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. @@ -646,6 +653,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - Important: Starting CoreStore 4.0, all `FetchClause`s required by the `ListMonitor` should be provided in the arguments list of `refetch(...)`. */ + @MainActor public func refetch( _ fetchClauses: FetchClause..., sourceIdentifier: (any Sendable)? = nil @@ -666,6 +674,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - Important: Starting CoreStore 4.0, all `FetchClause`s required by the `ListMonitor` should be provided in the arguments list of `refetch(...)`. */ + @MainActor public func refetch( _ fetchClauses: [FetchClause], sourceIdentifier: (any Sendable)? = nil @@ -892,6 +901,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, willChange: @escaping @Sendable ( @@ -969,6 +979,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, didInsertObject: @escaping @Sendable ( @@ -1056,6 +1067,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, didInsertSection: @escaping @Sendable ( @@ -1104,6 +1116,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func unregisterObserver(_ observer: AnyObject) { Internals.assert( @@ -1125,6 +1138,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable Internals.setAssociatedRetainedObject(nilValue, forKey: &self.didDeleteSectionKey, inObject: observer) } + @MainActor internal func refetch( _ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, sourceIdentifier: (any Sendable)? @@ -1154,7 +1168,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable } let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( - context: self.fetchedResultsController.managedObjectContext, + context: self.managedObjectContext, from: self.from, sectionBy: self.sectionBy, applyFetchClauses: self.applyFetchClauses @@ -1216,32 +1230,34 @@ public final class ListMonitor: Hashable, @unchecked Sendable // MARK: Private - fileprivate var fetchedResultsController: Internals.CoreStoreFetchedResultsController - fileprivate let taskGroup = DispatchGroup() - internal let sectionByIndexTransformer: (_ sectionName: KeyPathString?) -> String? - + private let managedObjectContext: NSManagedObjectContext + private let taskGroup = DispatchGroup() + private let sectionByIndexTransformer: @Sendable (_ sectionName: KeyPathString?) -> String? private let isSectioned: Bool - - private var willChangeListKey: Void? - private var didChangeListKey: Void? - private var willRefetchListKey: Void? - private var didRefetchListKey: Void? - - private var didInsertObjectKey: Void? - private var didDeleteObjectKey: Void? - private var didUpdateObjectKey: Void? - private var didMoveObjectKey: Void? - - private var didInsertSectionKey: Void? - private var didDeleteSectionKey: Void? - - private var fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate - private var observerForWillChangePersistentStore: Internals.NotificationObserver! - private var observerForDidChangePersistentStore: Internals.NotificationObserver! private let transactionQueue: DispatchQueue - private var applyFetchClauses: (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void - private var isPersistentStoreChanging: Bool = false { + private nonisolated(unsafe) var applyFetchClauses: (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void + + private nonisolated(unsafe) var willChangeListKey: Void? + private nonisolated(unsafe) var didChangeListKey: Void? + private nonisolated(unsafe) var willRefetchListKey: Void? + private nonisolated(unsafe) var didRefetchListKey: Void? + + private nonisolated(unsafe) var didInsertObjectKey: Void? + private nonisolated(unsafe) var didDeleteObjectKey: Void? + private nonisolated(unsafe) var didUpdateObjectKey: Void? + private nonisolated(unsafe) var didMoveObjectKey: Void? + + private nonisolated(unsafe) var didInsertSectionKey: Void? + private nonisolated(unsafe) var didDeleteSectionKey: Void? + + private nonisolated(unsafe) var observerForWillChangePersistentStore: Internals.NotificationObserver! + private nonisolated(unsafe) var observerForDidChangePersistentStore: Internals.NotificationObserver! + + private nonisolated(unsafe) var fetchedResultsController: Internals.CoreStoreFetchedResultsController + private nonisolated(unsafe) var fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate + + private nonisolated(unsafe) var isPersistentStoreChanging: Bool = false { didSet { @@ -1302,18 +1318,21 @@ public final class ListMonitor: Hashable, @unchecked Sendable from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: (@Sendable (ListMonitor) -> Void)? + createAsynchronously: (@MainActor (ListMonitor) -> Void)? ) { + self.managedObjectContext = context self.isSectioned = (sectionBy != nil) self.from = from self.sectionBy = sectionBy - (self.fetchedResultsController, self.fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( + let (fetchedResultsController, fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( context: context, from: from, sectionBy: sectionBy, applyFetchClauses: applyFetchClauses ) + self.fetchedResultsController = fetchedResultsController + self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer { @@ -1325,7 +1344,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable } self.transactionQueue = transactionQueue self.applyFetchClauses = applyFetchClauses - self.fetchedResultsControllerDelegate.handler = self + fetchedResultsControllerDelegate.handler = self guard let coordinator = context.parentStack?.coordinator else { @@ -1335,8 +1354,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable self.observerForWillChangePersistentStore = Internals.NotificationObserver( notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresWillChange, object: coordinator, - queue: OperationQueue.main, - closure: { [weak self] (note) -> Void in + closure: { @MainActor [weak self] (note) -> Void in guard let self = self else { @@ -1357,8 +1375,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable self.observerForDidChangePersistentStore = Internals.NotificationObserver( notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresDidChange, object: coordinator, - queue: OperationQueue.main, - closure: { [weak self] (note) -> Void in + closure: { @MainActor [weak self] (note) -> Void in guard let self = self else { @@ -1389,7 +1406,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable try! self.fetchedResultsController.performFetchFromSpecifiedStores() self.taskGroup.notify(queue: .main) { - createAsynchronously(self) + MainActor.assumeIsolated { + + createAsynchronously(self) + } } } } diff --git a/Sources/ListPublisher.SnapshotPublisher.swift b/Sources/ListPublisher.SnapshotPublisher.swift index 36285d1..08f71a1 100644 --- a/Sources/ListPublisher.SnapshotPublisher.swift +++ b/Sources/ListPublisher.SnapshotPublisher.swift @@ -52,7 +52,7 @@ extension ListPublisher { public typealias Output = ListSnapshot public typealias Failure = Never - public func receive( + public func receive( subscriber: S ) where S.Input == Output, S.Failure == Failure { @@ -90,7 +90,7 @@ extension ListPublisher { // MARK: - ListSnapshotSubscription - fileprivate final class ListSnapshotSubscription: Subscription, @unchecked Sendable + fileprivate final class ListSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -115,15 +115,17 @@ extension ListPublisher { return } - Internals.mainActorImmediate { [self] in + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { @@ -142,9 +144,10 @@ extension ListPublisher { self.subscriber = nil + nonisolated(unsafe) let strongSelf = self Internals.mainActorImmediate { - self.publisher.removeObserver(self) + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index 5657f34..1520847 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -98,7 +98,7 @@ public final class ListPublisher: Hashable { public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ListPublisher) -> Void + _ callback: @escaping @Sendable (ListPublisher) -> Void ) { Internals.assert( @@ -134,7 +134,7 @@ public final class ListPublisher: Hashable { _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ listPublisher: ListPublisher, _ sourceIdentifier: Any? ) -> Void @@ -234,6 +234,7 @@ public final class ListPublisher: Hashable { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack diff --git a/Sources/ListSnapshot.SectionInfo.swift b/Sources/ListSnapshot.SectionInfo.swift index fa12f9a..a508616 100644 --- a/Sources/ListSnapshot.SectionInfo.swift +++ b/Sources/ListSnapshot.SectionInfo.swift @@ -38,7 +38,7 @@ extension ListSnapshot { public let sectionID: SectionID - public let itemIDs: [ItemID] + public let itemIDs: [NSManagedObjectID] // MARK: RandomAccessCollection @@ -79,7 +79,7 @@ extension ListSnapshot { public subscript(position: Int) -> ObjectPublisher { let itemID = self.itemIDs[position] - return self.context.objectPublisher(objectID: itemID) + return self.context.objectPublisher(managedObjectID: itemID) } public func index(_ i: Index, offsetBy distance: Int) -> Index { @@ -100,7 +100,7 @@ extension ListSnapshot { public subscript(bounds: Range) -> ArraySlice { let itemIDs = self.itemIDs[bounds] - return ArraySlice(itemIDs.map(self.context.objectPublisher(objectID:))) + return ArraySlice(itemIDs.map(self.context.objectPublisher(managedObjectID:))) } @@ -126,6 +126,7 @@ extension ListSnapshot { } self.sectionID = sectionID self.itemIDs = listSnapshot.itemIDs(inSectionWithID: sectionID) + .map({ $0.managedObjectID }) self.context = context } } diff --git a/Sources/ListSnapshot.swift b/Sources/ListSnapshot.swift index 865d2ea..455365b 100644 --- a/Sources/ListSnapshot.swift +++ b/Sources/ListSnapshot.swift @@ -43,7 +43,7 @@ import AppKit Since `ListSnapshot` is a value type, you can freely modify its items. */ -public struct ListSnapshot: RandomAccessCollection, Hashable { +public struct ListSnapshot: RandomAccessCollection, Hashable, Sendable { // MARK: Public (Accessors) @@ -72,7 +72,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { let context = self.context! let itemID = self.diffableSnapshot.itemIdentifier(atAllItemsIndex: index)! - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } /** @@ -90,7 +90,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return nil } - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } /** @@ -109,7 +109,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { let snapshot = self.diffableSnapshot let sectionID = snapshot.sectionIdentifiers[sectionIndex] let itemID = snapshot.itemIdentifiers(inSection: sectionID)[itemIndex] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } /** @@ -141,7 +141,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return nil } let itemID = itemIDs[itemIndex] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } /** @@ -287,7 +287,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { */ public func sectionID(containingItemWithID itemID: ItemID) -> SectionID? { - return self.diffableSnapshot.sectionIdentifier(containingItem: itemID) + return self.diffableSnapshot.sectionIdentifier(containingItem: itemID.managedObjectID) } /** @@ -319,6 +319,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { public var itemIDs: [ItemID] { return self.diffableSnapshot.itemIdentifiers + .map(ItemID.init(managedObjectID:)) } /** @@ -330,6 +331,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { public func itemIDs(inSectionWithID sectionID: SectionID) -> [ItemID] { return self.diffableSnapshot.itemIdentifiers(inSection: sectionID) + .map(ItemID.init(managedObjectID:)) } /** @@ -345,7 +347,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { ) -> [ItemID] where S.Element == Int { let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID) - return indices.map({ itemIDs[$0] }) + return indices.map({ .init(managedObjectID: itemIDs[$0]) }) } /** @@ -356,7 +358,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { */ public func indexOfItem(withID itemID: ItemID) -> Index? { - return self.diffableSnapshot.indexOfItem(itemID) + return self.diffableSnapshot.indexOfItem(itemID.managedObjectID) } /** @@ -383,7 +385,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return indices.map { position in let itemID = itemIDs[position] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } } @@ -397,7 +399,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { let context = self.context! let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID) - return itemIDs.map(context.objectPublisher(objectID:)) + return itemIDs.map(context.objectPublisher(managedObjectID:)) } /** @@ -417,7 +419,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return itemIndices.map { position in let itemID = itemIDs[position] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } } @@ -434,7 +436,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return indices.lazy.map { position in let itemID = itemIDs[position] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } } @@ -448,7 +450,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { let context = self.context! let itemIDs = self.diffableSnapshot.itemIdentifiers(inSection: sectionID) - return itemIDs.lazy.map(context.objectPublisher(objectID:)) + return itemIDs.lazy.map(context.objectPublisher(managedObjectID:)) } /** @@ -468,7 +470,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return itemIndices.lazy.map { position in let itemID = itemIDs[position] - return context.objectPublisher(objectID: itemID) + return context.objectPublisher(managedObjectID: itemID) } } @@ -487,8 +489,8 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { ) where C.Element == ItemID { self.mutate { - - $0.appendItems(itemIDs, toSection: sectionID) + + $0.appendItems(itemIDs.map({ $0.managedObjectID }), toSection: sectionID) } } @@ -505,7 +507,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.unsafeAppendItems(itemIDs, toSectionAt: sectionIndex) + $0.unsafeAppendItems(itemIDs.map({ $0.managedObjectID }), toSectionAt: sectionIndex) } } @@ -522,7 +524,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.insertItems(itemIDs, beforeItem: beforeItemID) + $0.insertItems(itemIDs.map({ $0.managedObjectID }), beforeItem: beforeItemID.managedObjectID) } } @@ -539,7 +541,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.insertItems(itemIDs, afterItem: afterItemID) + $0.insertItems(itemIDs.map({ $0.managedObjectID }), afterItem: afterItemID.managedObjectID) } } @@ -556,7 +558,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.unsafeInsertItems(itemIDs, at: indexPath) + $0.unsafeInsertItems(itemIDs.map({ $0.managedObjectID }), at: indexPath) } } @@ -569,7 +571,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.deleteItems(itemIDs) + $0.deleteItems(itemIDs.map({ $0.managedObjectID })) } } @@ -610,7 +612,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.moveItem(itemID, beforeItem: beforeItemID) + $0.moveItem(itemID.managedObjectID, beforeItem: beforeItemID.managedObjectID) } } @@ -627,7 +629,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.moveItem(itemID, afterItem: afterItemID) + $0.moveItem(itemID.managedObjectID, afterItem: afterItemID.managedObjectID) } } @@ -657,7 +659,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { self.mutate { - $0.reloadItems(itemIDs) + $0.reloadItems(itemIDs.map({ $0.managedObjectID })) } } @@ -906,7 +908,7 @@ public struct ListSnapshot: RandomAccessCollection, Hashable { return .init() } let itemIDs = self.diffableSnapshot.itemIdentifiers(atAllItemsBounds: bounds) - return ArraySlice(itemIDs.map(context.objectPublisher(objectID:))) + return ArraySlice(itemIDs.map(context.objectPublisher(managedObjectID:))) } diff --git a/Sources/ListState.swift b/Sources/ListState.swift index 3c9c986..9cc3ba2 100644 --- a/Sources/ListState.swift +++ b/Sources/ListState.swift @@ -35,7 +35,7 @@ import SwiftUI A property wrapper type that can read `ListPublisher` changes. */ @propertyWrapper -public struct ListState: DynamicProperty { +public struct ListState: DynamicProperty { // MARK: Public @@ -67,7 +67,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ listPublisher: ListPublisher + _ listPublisher: ListPublisher ) { self._observer = .init(wrappedValue: .init(listPublisher: listPublisher)) @@ -103,7 +103,7 @@ public struct ListState: DynamicProperty { public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -145,7 +145,7 @@ public struct ListState: DynamicProperty { public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -179,7 +179,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, + _ from: From, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -218,7 +218,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, + _ from: From, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -263,8 +263,8 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -311,8 +311,8 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -324,13 +324,13 @@ public struct ListState: DynamicProperty { // MARK: @propertyWrapper @MainActor - public var wrappedValue: ListSnapshot { + public var wrappedValue: ListSnapshot { return self.observer.items } @MainActor - public var projectedValue: ListPublisher { + public var projectedValue: ListPublisher { return self.observer.listPublisher } @@ -355,31 +355,28 @@ public struct ListState: DynamicProperty { @MainActor private final class Observer: Observation.Observable { - private let registrar = ObservationRegistrar() - private var _items: ListSnapshot + let listPublisher: ListPublisher - let listPublisher: ListPublisher - - var items: ListSnapshot { + nonisolated var items: ListSnapshot { get { self.registrar.access(self, keyPath: \.items) - return self._items + return self.current.withLock({ $0 }) } set { self.registrar.withMutation(of: self, keyPath: \.items) { - self._items = newValue + self.current.withLock({ $0 = newValue }) } } } - init(listPublisher: ListPublisher) { + init(listPublisher: ListPublisher) { self.listPublisher = listPublisher - self._items = listPublisher.snapshot + self.current = .init(listPublisher.snapshot) listPublisher.addObserver(self) { [weak self] (listPublisher) in @@ -395,6 +392,12 @@ public struct ListState: DynamicProperty { self.listPublisher.removeObserver(self) } + + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex> } } diff --git a/Sources/MigrationChain.swift b/Sources/MigrationChain.swift index 5301b70..66f223d 100644 --- a/Sources/MigrationChain.swift +++ b/Sources/MigrationChain.swift @@ -60,7 +60,7 @@ import CoreData - a version appears twice as a key in a dictionary literal - a loop is found in any of the paths */ -public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLiteral, ExpressibleByDictionaryLiteral, ExpressibleByArrayLiteral, Equatable { +public struct MigrationChain: ExpressibleByNilLiteral, ExpressibleByStringLiteral, ExpressibleByDictionaryLiteral, ExpressibleByArrayLiteral, Equatable, Sendable { /** Initializes the `MigrationChain` with empty values, which instructs the `DataStack` to use the .xcdatamodel's current version as the final version, and to disable progressive migrations. diff --git a/Sources/NSManagedObjectContext+CoreStore.swift b/Sources/NSManagedObjectContext+CoreStore.swift index b2d2517..2ee0fd2 100644 --- a/Sources/NSManagedObjectContext+CoreStore.swift +++ b/Sources/NSManagedObjectContext+CoreStore.swift @@ -87,7 +87,7 @@ extension NSManagedObjectContext { } @nonobjc - internal func objectPublisher(objectID: NSManagedObjectID) -> ObjectPublisher { + internal func objectPublisher(managedObjectID: NSManagedObjectID) -> ObjectPublisher { let cache: NSMapTable> = self.userInfo(for: .objectPublishersCache(O.self)) { @@ -95,12 +95,12 @@ extension NSManagedObjectContext { } return Internals.with { - if let objectPublisher = cache.object(forKey: objectID) { + if let objectPublisher = cache.object(forKey: managedObjectID) { return objectPublisher } - let objectPublisher = ObjectPublisher.createUncached(objectID: objectID, context: self) - cache.setObject(objectPublisher, forKey: objectID) + let objectPublisher = ObjectPublisher.createUncached(managedObjectID: managedObjectID, context: self) + cache.setObject(objectPublisher, forKey: managedObjectID) return objectPublisher } } diff --git a/Sources/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift index 601ccb7..eac7a16 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -100,6 +100,14 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { return objects.compactMap({ self.fetchExisting($0.cs_id()) }) } + @nonobjc + public func fetchExisting( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return objectIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) + } + @nonobjc public func fetchExisting( _ objectIDs: S diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index ab24259..ed9f77a 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -187,7 +187,7 @@ extension NSManagedObjectContext { @nonobjc internal func saveAsynchronously( sourceIdentifier: (any Sendable)?, - completion: @escaping @MainActor (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } + completion: @escaping @MainActor @Sendable (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } ) { self.perform { diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index 83a7e73..cb7c319 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -39,7 +39,7 @@ import CoreData Observers registered via `addObserver(_:)` are not retained. `ObjectMonitor` only keeps a `weak` reference to all observers, thus keeping itself free from retain-cycles. */ -public final class ObjectMonitor: Hashable, ObjectRepresentation, @unchecked Sendable { +public final class ObjectMonitor: Hashable, ObjectRepresentation, Sendable { /** Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. @@ -71,6 +71,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) @@ -111,6 +112,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to unregister notifications to */ + @MainActor public func removeObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) @@ -165,11 +167,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.id } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -182,7 +186,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { - return dataStack.unsafeContext().objectPublisher(objectID: self.id) + return dataStack.unsafeContext().objectPublisher(managedObjectID: self.id) } public func asReadOnly(in dataStack: DataStack) -> O? { @@ -198,20 +202,20 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.id, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.id, context: context) } // MARK: Internal internal init( - objectID: O.ObjectID, + objectID: NSManagedObjectID, context: NSManagedObjectContext ) { @@ -240,9 +244,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController try! fetchedResultsController.performFetchFromSpecifiedStores() - self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] + self.lastCommittedAttributes.withLock { + + $0 = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] + } } + @MainActor internal func registerObserver( _ observer: U, willChangeObject: @escaping @Sendable ( @@ -303,25 +311,29 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati return } - - let previousCommitedAttributes = self.lastCommittedAttributes - let currentCommitedAttributes = object.cs_toRaw().committedValues(forKeys: nil) as! [String: NSObject] - - var changedKeys = Set() - for key in currentCommitedAttributes.keys { + let changedKeys = self.lastCommittedAttributes.withLock { - if previousCommitedAttributes[key] != currentCommitedAttributes[key] { + let previousCommitedAttributes = $0 + let currentCommitedAttributes = object.cs_toRaw().committedValues(forKeys: nil) as! [String: NSObject] + + var changedKeys = Set() + for key in currentCommitedAttributes.keys { - changedKeys.insert(key) + if previousCommitedAttributes[key] != currentCommitedAttributes[key] { + + changedKeys.insert(key) + } } + + $0 = currentCommitedAttributes + return changedKeys } - - self.lastCommittedAttributes = currentCommitedAttributes didUpdateObject(observer, monitor, object, changedKeys) } ) } + @MainActor internal func unregisterObserver(_ observer: AnyObject) { Internals.assert( @@ -343,14 +355,14 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: Private - private let id: O.ObjectID + private let id: NSManagedObjectID private let fetchedResultsController: Internals.CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate - private var lastCommittedAttributes = [String: NSObject]() + private let lastCommittedAttributes: Internals.Mutex<[String: NSObject]> = .init([:]) - private var willChangeObjectKey: Void? - private var didDeleteObjectKey: Void? - private var didUpdateObjectKey: Void? + private nonisolated(unsafe) var willChangeObjectKey: Void? + private nonisolated(unsafe) var didDeleteObjectKey: Void? + private nonisolated(unsafe) var didUpdateObjectKey: Void? private var context: NSManagedObjectContext { diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index 3149e29..cfe5aab 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -36,12 +36,12 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ObjectObserver: AnyObject & Sendable { +public protocol ObjectObserver: AnyObject, Sendable { /** The `DynamicObject` type for the observed object */ - associatedtype ObjectEntityType: DynamicObject & Sendable + associatedtype ObjectEntityType: DynamicObject /** Handles processing just before a change to the observed `object` occurs. (Optional) diff --git a/Sources/ObjectPublisher.SnapshotPublisher.swift b/Sources/ObjectPublisher.SnapshotPublisher.swift index a610caa..80e6348 100644 --- a/Sources/ObjectPublisher.SnapshotPublisher.swift +++ b/Sources/ObjectPublisher.SnapshotPublisher.swift @@ -52,7 +52,7 @@ extension ObjectPublisher { public typealias Output = ObjectSnapshot? public typealias Failure = Never - public func receive( + public func receive( subscriber: S ) where S.Input == Output, S.Failure == Failure { @@ -90,7 +90,7 @@ extension ObjectPublisher { // MARK: - ObjectSnapshotSubscription - fileprivate final class ObjectSnapshotSubscription: Subscription, @unchecked Sendable + fileprivate final class ObjectSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -115,15 +115,17 @@ extension ObjectPublisher { return } - Internals.mainActorImmediate { [self] in + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { @@ -142,9 +144,10 @@ extension ObjectPublisher { self.subscriber = nil + nonisolated(unsafe) let strongSelf = self Internals.mainActorImmediate { - self.publisher.removeObserver(self) + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 6a6388e..12d2746 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -57,7 +57,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash /** The actual `DynamicObject` instance. Becomes `nil` if the object has been deleted. */ - public private(set) lazy var object: O? = self.context.fetchExisting(self.id) + public private(set) lazy var object: O? = self.context.fetchExisting(self.managedObjectID) @@ -80,7 +80,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ObjectPublisher) -> Void + _ callback: @escaping @Sendable (ObjectPublisher) -> Void ) { Internals.assert( @@ -118,7 +118,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ objectPublisher: ObjectPublisher, _ sourceIdentifier: Any? ) -> Void @@ -160,11 +160,13 @@ public final class ObjectPublisher: ObjectRepresentation, Hash // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { - return self.id + return self.managedObjectID } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -182,17 +184,17 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self } - return context.objectPublisher(objectID: self.id) + return context.objectPublisher(managedObjectID: self.managedObjectID) } public func asReadOnly(in dataStack: DataStack) -> O? { - return dataStack.unsafeContext().fetchExisting(self.id) + return dataStack.unsafeContext().fetchExisting(self.managedObjectID) } public func asEditable(in transaction: BaseDataTransaction) -> O? { - return transaction.unsafeContext().fetchExisting(self.id) + return transaction.unsafeContext().fetchExisting(self.managedObjectID) } public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { @@ -202,7 +204,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self.lazySnapshot } - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { @@ -212,7 +214,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self.lazySnapshot } - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } @@ -220,7 +222,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public static func == (_ lhs: ObjectPublisher, _ rhs: ObjectPublisher) -> Bool { - return lhs.id == rhs.id + return lhs.managedObjectID == rhs.managedObjectID && lhs.context == rhs.context } @@ -229,24 +231,24 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(self.managedObjectID) hasher.combine(self.context) } // MARK: Internal - internal var cs_objectID: O.ObjectID { - - return self.objectID() - } + internal let managedObjectID: NSManagedObjectID - internal static func createUncached(objectID: O.ObjectID, context: NSManagedObjectContext) -> ObjectPublisher { + internal static func createUncached( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext + ) -> ObjectPublisher { return self.init( - objectID: objectID, + managedObjectID: managedObjectID, context: context, - initializer: ObjectSnapshot.init(objectID:context:) + initializer: ObjectSnapshot.init(managedObjectID:context:) ) } @@ -261,43 +263,52 @@ public final class ObjectPublisher: ObjectRepresentation, Hash fileprivate typealias ObserverClosureType = Internals.Closure<(objectPublisher: ObjectPublisher, sourceIdentifier: Any?), Void> - fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot?) { + fileprivate init( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext, + initializer: @escaping @Sendable (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot? + ) { - self.id = objectID + self.managedObjectID = managedObjectID self.context = context self.$lazySnapshot.initialize { [weak self] in guard let self = self else { - return initializer(objectID, context) + return initializer(managedObjectID, context) } - context.objectsDidChangeObserver(for: self).addObserver(self) { [weak self] (updatedIDs, deletedIDs) in - - guard let self = self else { - - return - } - if deletedIDs.contains(objectID) { - - self.object = nil - - self.$lazySnapshot.reset({ nil }) - self.notifyObservers(sourceIdentifier: self.context.saveMetadata) - } - else if updatedIDs.contains(objectID) { - - self.$lazySnapshot.reset({ initializer(objectID, context) }) - self.notifyObservers(sourceIdentifier: self.context.saveMetadata) - } - } - return initializer(objectID, context) + nonisolated(unsafe) weak let weakSelf = self as Optional + context + .objectsDidChangeObserver(for: self) + .addObserver( + self, + closure: { (updatedIDs, deletedIDs) in + + guard let self = weakSelf else { + + return + } + if deletedIDs.contains(managedObjectID) { + + self.object = nil + + self.$lazySnapshot.reset({ nil }) + self.notifyObservers(sourceIdentifier: self.context.saveMetadata) + } + else if updatedIDs.contains(managedObjectID) { + + self.$lazySnapshot.reset({ initializer(managedObjectID, context) }) + self.notifyObservers(sourceIdentifier: self.context.saveMetadata) + } + } + ) + return initializer(managedObjectID, context) } } // MARK: Private - private let id: O.ObjectID private let context: NSManagedObjectContext @Internals.LazyNonmutating(uninitialized: ()) diff --git a/Sources/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index 0ad3787..c30114f 100644 --- a/Sources/ObjectRepresentation.swift +++ b/Sources/ObjectRepresentation.swift @@ -33,15 +33,17 @@ import CoreData */ public protocol AnyObjectRepresentation { - /** - The internal ID for the object. - */ - func objectID() -> NSManagedObjectID - /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) func cs_dataStack() -> DataStack? + + /** + The internal `NSManagedObjectID` for the object. Do not call directly. + */ + @_spi(Internals) + func cs_id() -> NSManagedObjectID } @@ -98,7 +100,7 @@ extension DynamicObject where Self: ObjectRepresentation { return self.cs_toRaw() .managedObjectContext - .map({ $0.objectPublisher(objectID: self.cs_id()) }) + .map({ $0.objectPublisher(managedObjectID: self.cs_id()) }) } /** @@ -108,17 +110,19 @@ extension DynamicObject where Self: ObjectRepresentation { return self.cs_toRaw() .managedObjectContext - .flatMap({ ObjectSnapshot(objectID: self.cs_id(), context: $0) }) + .flatMap({ ObjectSnapshot(managedObjectID: self.cs_id(), context: $0) }) } // MARK: AnyObjectRepresentation - public func objectID() -> Self.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.cs_id() } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.cs_toRaw().managedObjectContext?.parentStack @@ -130,7 +134,7 @@ extension DynamicObject where Self: ObjectRepresentation { public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() - return context.objectPublisher(objectID: self.cs_id()) + return context.objectPublisher(managedObjectID: self.cs_id()) } public func asReadOnly(in dataStack: DataStack) -> Self? { @@ -156,12 +160,12 @@ extension DynamicObject where Self: ObjectRepresentation { public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.cs_id(), context: context) + return ObjectSnapshot(managedObjectID: self.cs_id(), context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.cs_id(), context: context) + return ObjectSnapshot(managedObjectID: self.cs_id(), context: context) } } diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index b4b93ad..f3c7a96 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -40,7 +40,7 @@ import AppKit The `ObjectSnapshot` is a full copy of a `DynamicObject`'s properties at a given point in time. This is useful especially when keeping thread-safe state values, in ViewModels for example. Since this is a value type, any changes in this `struct` does not affect the actual object. */ @dynamicMemberLookup -public struct ObjectSnapshot: ObjectRepresentation, Hashable, @unchecked Sendable { +public struct ObjectSnapshot: ObjectRepresentation, Hashable, Sendable { // MARK: Public @@ -52,11 +52,13 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { - return self.id + return self.managedObjectID } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -70,29 +72,29 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() - return context.objectPublisher(objectID: self.id) + return context.objectPublisher(managedObjectID: self.managedObjectID) } public func asReadOnly(in dataStack: DataStack) -> O? { - return dataStack.unsafeContext().fetchExisting(self.id) + return dataStack.unsafeContext().fetchExisting(self.managedObjectID) } public func asEditable(in transaction: BaseDataTransaction) -> O? { - return transaction.unsafeContext().fetchExisting(self.id) + return transaction.unsafeContext().fetchExisting(self.managedObjectID) } public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } @@ -100,7 +102,7 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public static func == (_ lhs: Self, _ rhs: Self) -> Bool { - return lhs.id == rhs.id + return lhs.managedObjectID == rhs.managedObjectID && (lhs.generation == rhs.generation || lhs.valuesRef == rhs.valuesRef) } @@ -109,34 +111,34 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(self.managedObjectID) hasher.combine(self.valuesRef) } // MARK: Internal + + internal let managedObjectID: NSManagedObjectID - internal init?(objectID: O.ObjectID, context: NSManagedObjectContext) { + internal init?( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext + ) { - guard let values = O.cs_snapshotDictionary(id: objectID, context: context) else { + guard let values = O.cs_snapshotDictionary(managedObjectID: managedObjectID, context: context) else { return nil } - self.id = objectID + self.managedObjectID = managedObjectID self.context = context self.values = values self.generation = .init() } - - internal var cs_objectID: O.ObjectID { - - return self.objectID() - } // MARK: FilePrivate - fileprivate var values: [String: Any] { + fileprivate nonisolated(unsafe) var values: [String: Any] { didSet { @@ -147,7 +149,6 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, // MARK: Private - private let id: O.ObjectID private let context: NSManagedObjectContext private var generation: UUID @@ -344,16 +345,16 @@ extension ObjectSnapshot where O: CoreStoreObject { get { let key = String(keyPath: member) - guard let id = self.values[key] as? D.ObjectID else { + guard let id = self.values[key] as? NSManagedObjectID else { return nil } - return self.context.objectPublisher(objectID: id) + return self.context.objectPublisher(managedObjectID: id) } set { let key = String(keyPath: member) - self.values[key] = newValue?.objectID() + self.values[key] = newValue?.cs_id() } } @@ -365,13 +366,13 @@ extension ObjectSnapshot where O: CoreStoreObject { let key = String(keyPath: member) let context = self.context - let ids = self.values[key] as! [D.ObjectID] - return ids.map(context.objectPublisher(objectID:)) + let ids = self.values[key] as! [NSManagedObjectID] + return ids.map(context.objectPublisher(managedObjectID:)) } set { let key = String(keyPath: member) - self.values[key] = newValue.map({ $0.objectID() }) + self.values[key] = newValue.map({ $0.cs_id() }) } } @@ -383,13 +384,13 @@ extension ObjectSnapshot where O: CoreStoreObject { let key = String(keyPath: member) let context = self.context - let ids = self.values[key] as! Set - return Set(ids.map(context.objectPublisher(objectID:))) + let ids = self.values[key] as! Set + return Set(ids.map(context.objectPublisher(managedObjectID:))) } set { let key = String(keyPath: member) - self.values[key] = Set(newValue.map({ $0.objectID() })) + self.values[key] = Set(newValue.map({ $0.cs_id() })) } } } diff --git a/Sources/ObjectState.swift b/Sources/ObjectState.swift index efa336f..bf6156d 100644 --- a/Sources/ObjectState.swift +++ b/Sources/ObjectState.swift @@ -103,23 +103,20 @@ public struct ObjectState: DynamicProperty { @MainActor private final class Observer: Observation.Observable { - private let registrar = ObservationRegistrar() - private var _item: ObjectSnapshot? - let objectPublisher: ObjectPublisher? - var item: ObjectSnapshot? { + nonisolated var item: ObjectSnapshot? { get { self.registrar.access(self, keyPath: \.item) - return self._item + return self.current.withLock({ $0 }) } set { self.registrar.withMutation(of: self, keyPath: \.item) { - self._item = newValue + self.current.withLock({ $0 = newValue }) } } } @@ -132,12 +129,12 @@ public struct ObjectState: DynamicProperty { else { self.objectPublisher = nil - self._item = nil + self.current = .init(nil) return } self.objectPublisher = objectPublisher - self._item = objectPublisher.snapshot + self.current = .init(objectPublisher.snapshot) objectPublisher.addObserver(self) { [weak self] (objectPublisher) in @@ -153,6 +150,12 @@ public struct ObjectState: DynamicProperty { self.objectPublisher?.removeObserver(self) } + + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex?> } } diff --git a/Sources/OrderBy.swift b/Sources/OrderBy.swift index d1225f6..8a9ef29 100644 --- a/Sources/OrderBy.swift +++ b/Sources/OrderBy.swift @@ -32,7 +32,7 @@ import CoreData /** The `OrderBy` clause specifies the sort order for results for a fetch or a query. */ -public struct OrderBy: OrderByClause, FetchClause, QueryClause, DeleteClause, Hashable { +public struct OrderBy: OrderByClause, FetchClause, QueryClause, DeleteClause, Hashable, Sendable { /** Combines two `OrderBy` sort descriptors together @@ -104,7 +104,7 @@ public struct OrderBy: OrderByClause, FetchClause, QueryClause public typealias ObjectType = O - public let sortDescriptors: [NSSortDescriptor] + public nonisolated(unsafe) let sortDescriptors: [NSSortDescriptor] // MARK: FetchClause, QueryClause, DeleteClause diff --git a/Sources/Progress+Convenience.swift b/Sources/Progress+Convenience.swift index 5599251..2a46d64 100644 --- a/Sources/Progress+Convenience.swift +++ b/Sources/Progress+Convenience.swift @@ -37,7 +37,7 @@ extension Progress { */ @nonobjc @MainActor - public func setProgressHandler(_ closure: (@MainActor (_ progress: Progress) -> Void)?) { + public func setProgressHandler(_ closure: (@MainActor @Sendable (_ progress: Progress) -> Void)?) { self.progressObserver.progressHandler = closure } @@ -81,12 +81,12 @@ extension Progress { // MARK: - ProgressObserver @objc -private final class ProgressObserver: NSObject, @unchecked Sendable { +private final class ProgressObserver: NSObject, Sendable { private unowned let progress: Progress @MainActor - fileprivate var progressHandler: (@MainActor (_ progress: Progress) -> Void)? { + fileprivate var progressHandler: (@MainActor @Sendable (_ progress: Progress) -> Void)? { didSet { @@ -127,7 +127,12 @@ private final class ProgressObserver: NSObject, @unchecked Sendable { } } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) { guard let progress = object as? Progress, progress == self.progress, diff --git a/Sources/Relationship.ToManyOrdered.swift b/Sources/Relationship.ToManyOrdered.swift index 26bb880..10bfeee 100644 --- a/Sources/Relationship.ToManyOrdered.swift +++ b/Sources/Relationship.ToManyOrdered.swift @@ -196,7 +196,7 @@ extension RelationshipContainer { internal var valueForSnapshot: Any? { - return self.value.map({ $0.objectID() }) + return self.value.map({ $0.cs_id() }) } private init(keyPath: String, minCount: Int, maxCount: Int, inverseKeyPath: @escaping () -> String?, deleteRule: DeleteRule, versionHashModifier: @autoclosure @escaping () -> String?, renamingIdentifier: @autoclosure @escaping () -> String?, affectedByKeyPaths: @autoclosure @escaping () -> Set) { diff --git a/Sources/Relationship.ToManyUnordered.swift b/Sources/Relationship.ToManyUnordered.swift index f07ca91..fe3818f 100644 --- a/Sources/Relationship.ToManyUnordered.swift +++ b/Sources/Relationship.ToManyUnordered.swift @@ -196,7 +196,7 @@ extension RelationshipContainer { internal var valueForSnapshot: Any? { - return Set(self.value.map({ $0.objectID() })) + return Set(self.value.map({ $0.cs_id() })) } private init(keyPath: KeyPathString, inverseKeyPath: @escaping () -> KeyPathString?, deleteRule: DeleteRule, minCount: Int, maxCount: Int, versionHashModifier: @autoclosure @escaping () -> String?, renamingIdentifier: @autoclosure @escaping () -> String?, affectedByKeyPaths: @autoclosure @escaping () -> Set) { diff --git a/Sources/Relationship.ToOne.swift b/Sources/Relationship.ToOne.swift index fb5d47d..61e36d1 100644 --- a/Sources/Relationship.ToOne.swift +++ b/Sources/Relationship.ToOne.swift @@ -180,7 +180,7 @@ extension RelationshipContainer { internal var valueForSnapshot: Any? { - return self.value?.objectID() + return self.value?.cs_id() } diff --git a/Sources/SQLiteStore.swift b/Sources/SQLiteStore.swift index 3f762b8..ccb339f 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -33,7 +33,7 @@ import CoreData - Warning: The default SQLite file location for the `LegacySQLiteStore` and `SQLiteStore` are different. If the app was depending on CoreStore's default directories prior to 2.0.0, make sure to use the `SQLiteStore.legacy(...)` factory methods to create the `SQLiteStore` instead of using initializers directly. */ -public final class SQLiteStore: LocalStorage, @unchecked Sendable { +public final class SQLiteStore: LocalStorage { /** Initializes an SQLite store interface from the given SQLite file URL. When this instance is passed to the `DataStack`'s `addStorage()` methods, a new SQLite file will be created if it does not exist. @@ -176,6 +176,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didAddToDataStack(_ dataStack: DataStack) { self.dataStack = dataStack @@ -184,6 +185,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { self.dataStack = nil @@ -205,7 +207,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Options that tell the `DataStack` how to setup the persistent store */ - public var localStorageOptions: LocalStorageOptions + public let localStorageOptions: LocalStorageOptions /** The options dictionary for the specified `LocalStorageOptions` @@ -231,6 +233,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Called by the `DataStack` to perform checkpoint operations on the storage. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE to force a checkpoint. */ + @_spi(Internals) public func cs_finalizeStorageAndWait( soureModelHint: NSManagedObjectModel ) throws(any Swift.Error) { @@ -252,6 +255,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ + @_spi(Internals) public func cs_eraseStorageAndWait( metadata: [String: Any], soureModelHint: NSManagedObjectModel? @@ -375,5 +379,5 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { // MARK: Private - private weak var dataStack: DataStack? + private nonisolated(unsafe) weak var dataStack: DataStack? } diff --git a/Sources/SchemaHistory.swift b/Sources/SchemaHistory.swift index 8432abc..61d14e7 100644 --- a/Sources/SchemaHistory.swift +++ b/Sources/SchemaHistory.swift @@ -23,7 +23,7 @@ // SOFTWARE. // -import CoreData +@preconcurrency import CoreData import Foundation @@ -32,7 +32,7 @@ import Foundation /** The `SchemaHistory` encapsulates multiple `DynamicSchema` across multiple model versions. It contains all model history and is used by the `DataStack` to */ -public final class SchemaHistory: ExpressibleByArrayLiteral { +public final class SchemaHistory: ExpressibleByArrayLiteral, Sendable { /** The version string for the current model version. The `DataStack` will try to migrate all `StorageInterface`s added to itself to this version, following the version steps provided by the `migrationChain`. @@ -161,6 +161,14 @@ public final class SchemaHistory: ExpressibleByArrayLiteral { self.migrationChain = migrationChain self.currentModelVersion = currentModelVersion self.rawModel = schemaByVersion[currentModelVersion]!.rawModel() + self.entityDescriptionsByEntityIdentifier = self.rawModel.entities.reduce( + into: [:], + { mapping, entityDescription in + + let entityIdentifier = Internals.EntityIdentifier(entityDescription) + mapping[entityIdentifier] = entityDescription + } + ) } @@ -168,7 +176,7 @@ public final class SchemaHistory: ExpressibleByArrayLiteral { public typealias Element = DynamicSchema - public convenience init(arrayLiteral elements: DynamicSchema...) { + public convenience init(arrayLiteral elements: any DynamicSchema...) { self.init( allSchema: elements, @@ -180,19 +188,9 @@ public final class SchemaHistory: ExpressibleByArrayLiteral { // MARK: Internal - internal let schemaByVersion: [ModelVersion: DynamicSchema] + internal let schemaByVersion: [ModelVersion: any DynamicSchema] internal let rawModel: NSManagedObjectModel - - internal private(set) lazy var entityDescriptionsByEntityIdentifier: [Internals.EntityIdentifier: NSEntityDescription] = Internals.with { [unowned self] in - - var mapping: [Internals.EntityIdentifier: NSEntityDescription] = [:] - self.rawModel.entities.forEach { (entityDescription) in - - let entityIdentifier = Internals.EntityIdentifier(entityDescription) - mapping[entityIdentifier] = entityDescription - } - return mapping - } + internal let entityDescriptionsByEntityIdentifier: [Internals.EntityIdentifier: NSEntityDescription] internal func rawModel(for modelVersion: ModelVersion) -> NSManagedObjectModel? { diff --git a/Sources/SchemaMappingProvider.swift b/Sources/SchemaMappingProvider.swift index 420ef1c..9d16ad4 100644 --- a/Sources/SchemaMappingProvider.swift +++ b/Sources/SchemaMappingProvider.swift @@ -32,11 +32,12 @@ import Foundation /** The `SchemaMappingProvider` provides migration mapping information between two `DynamicSchema` versions. */ -public protocol SchemaMappingProvider { +public protocol SchemaMappingProvider: Sendable { /** Do not call directly. */ + @_spi(Internals) func cs_createMappingModel( from sourceSchema: DynamicSchema, to destinationSchema: DynamicSchema, diff --git a/Sources/SectionBy.swift b/Sources/SectionBy.swift index 7b00e35..8a05e04 100644 --- a/Sources/SectionBy.swift +++ b/Sources/SectionBy.swift @@ -39,7 +39,7 @@ import CoreData ) ``` */ -public struct SectionBy { +public struct SectionBy: Sendable { /** Initializes a `SectionBy` clause with the key path to use to group `ListMonitor` objects into sections @@ -63,7 +63,7 @@ public struct SectionBy { */ public init( _ sectionKeyPath: KeyPathString, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.sectionKeyPath = sectionKeyPath @@ -74,7 +74,7 @@ public struct SectionBy { // MARK: Internal internal let sectionKeyPath: KeyPathString - internal let sectionIndexTransformer: (_ sectionName: String?) -> String? + internal let sectionIndexTransformer: @Sendable (_ sectionName: String?) -> String? } @@ -104,7 +104,7 @@ extension SectionBy where O: NSManagedObject { */ public init( _ sectionKeyPath: KeyPath, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -167,7 +167,7 @@ extension SectionBy where O: CoreStoreObject { */ public init( _ sectionKeyPath: KeyPath.Stored>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -185,7 +185,7 @@ extension SectionBy where O: CoreStoreObject { */ public init( _ sectionKeyPath: KeyPath.Virtual>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -203,7 +203,7 @@ extension SectionBy where O: CoreStoreObject { */ public init( _ sectionKeyPath: KeyPath.Coded>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -255,7 +255,7 @@ extension SectionBy { public init( _ sectionKeyPath: KeyPath.Required>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -266,7 +266,7 @@ extension SectionBy { public init( _ sectionKeyPath: KeyPath.Optional>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -277,7 +277,7 @@ extension SectionBy { public init( _ sectionKeyPath: KeyPath.Required>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( @@ -288,7 +288,7 @@ extension SectionBy { public init( _ sectionKeyPath: KeyPath.Optional>, - sectionIndexTransformer: @escaping (_ sectionName: String?) -> String? + sectionIndexTransformer: @escaping @Sendable (_ sectionName: String?) -> String? ) { self.init( diff --git a/Sources/Select.swift b/Sources/Select.swift index ef4a229..177ca7b 100644 --- a/Sources/Select.swift +++ b/Sources/Select.swift @@ -33,7 +33,7 @@ import CoreData /** The `SelectResultType` protocol is implemented by return types supported by the `Select` clause. */ -public protocol SelectResultType {} +public protocol SelectResultType: Sendable {} // MARK: - SelectAttributesResultType @@ -43,6 +43,7 @@ public protocol SelectResultType {} */ public protocol SelectAttributesResultType: SelectResultType { + @_spi(Internals) static func cs_fromQueryResultsNativeType( _ result: [Any] ) -> [[String: Any]] diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 51bc608..de929a9 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -54,11 +54,13 @@ public protocol StorageInterface: AnyObject, Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) func cs_didAddToDataStack(_ dataStack: DataStack) /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) func cs_didRemoveFromDataStack(_ dataStack: DataStack) } @@ -146,6 +148,7 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform checkpoint operations on the storage. (SQLite stores for example, can convert the database's WAL journaling mode to DELETE to force a checkpoint) */ + @_spi(Internals) func cs_finalizeStorageAndWait( soureModelHint: NSManagedObjectModel ) throws(any Swift.Error) @@ -153,6 +156,7 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ + @_spi(Internals) func cs_eraseStorageAndWait( metadata: [String: Any], soureModelHint: NSManagedObjectModel? diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index c1dbad5..a219a51 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -32,7 +32,8 @@ import CoreData /** The `SynchronousDataTransaction` 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.beginSynchronous(_:)`. */ -public final class SynchronousDataTransaction: BaseDataTransaction { +@_nonSendable +public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { /** Cancels a transaction by throwing `CoreStoreError.userCancelled`. @@ -67,6 +68,23 @@ public final class SynchronousDataTransaction: 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( + _ persistentID: DynamicObjectID? + ) -> 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`. diff --git a/Sources/Tweak.swift b/Sources/Tweak.swift index 1a63bd1..2b6fb21 100644 --- a/Sources/Tweak.swift +++ b/Sources/Tweak.swift @@ -42,12 +42,7 @@ import CoreData ) ``` */ -public struct Tweak: FetchClause, QueryClause, DeleteClause { - - /** - The block to customize the `NSFetchRequest` - */ - public let closure: (_ fetchRequest: NSFetchRequest) -> Void +public struct Tweak: FetchClause, QueryClause, DeleteClause, Sendable { /** Initializes a `Tweak` clause with a closure where the `NSFetchRequest` may be configured. @@ -55,7 +50,7 @@ public struct Tweak: FetchClause, QueryClause, DeleteClause { - Important: `Tweak`'s closure is executed only just before the fetch occurs, so make sure that any values captured by the closure is not prone to race conditions. Also, some utilities (such as `ListMonitor`s) may keep `FetchClause`s in memory and may thus introduce retain cycles if reference captures are not handled properly. - parameter closure: the block to customize the `NSFetchRequest` */ - public init(_ closure: @escaping (_ fetchRequest: NSFetchRequest) -> Void) { + public init(_ closure: @escaping @Sendable (_ fetchRequest: NSFetchRequest) -> Void) { self.closure = closure } @@ -67,4 +62,9 @@ public struct Tweak: FetchClause, QueryClause, DeleteClause { self.closure(fetchRequest as! NSFetchRequest) } + + + // MARK: Private + + public let closure: @Sendable (_ fetchRequest: NSFetchRequest) -> Void } diff --git a/Sources/UnsafeDataModelSchema.swift b/Sources/UnsafeDataModelSchema.swift index a313b32..108c627 100644 --- a/Sources/UnsafeDataModelSchema.swift +++ b/Sources/UnsafeDataModelSchema.swift @@ -23,7 +23,7 @@ // SOFTWARE. // -import CoreData +@preconcurrency import CoreData import Foundation diff --git a/Sources/UserInfo.swift b/Sources/UserInfo.swift index 133eee3..05dfc57 100644 --- a/Sources/UserInfo.swift +++ b/Sources/UserInfo.swift @@ -38,7 +38,7 @@ import Foundation ``` - Important: Do not use this class to store thread-sensitive data. */ -public final class UserInfo { +public final class UserInfo: Sendable { /** Allows external libraries to store custom data. App code should rarely have a need for this. @@ -51,25 +51,21 @@ public final class UserInfo { - Important: Do not use this method to store thread-sensitive data. - parameter key: the key for custom data. Make sure this is a static pointer that will never be changed. */ - public subscript(key: UnsafeRawPointer) -> Any? { + public subscript(key: UnsafeRawPointer) -> (any Sendable)? { get { - self.lock.lock() - defer { + return self.data.withLock { (info: inout _) in - self.lock.unlock() + return info[key] } - return self.data[key] } set { - self.lock.lock() - defer { + return self.data.withLock { (info: inout _) in - self.lock.unlock() + info[key] = newValue } - self.data[key] = newValue } } @@ -86,20 +82,18 @@ public final class UserInfo { - parameter lazyInit: a closure to use to lazily-initialize the data - returns: A custom data identified by `key` */ - public subscript(key: UnsafeRawPointer, lazyInit closure: () -> Any) -> Any { + public subscript(key: UnsafeRawPointer, lazyInit closure: () -> any Sendable) -> any Sendable { - self.lock.lock() - defer { - - self.lock.unlock() - } - if let value = self.data[key] { + return self.data.withLock { (info: inout _) in + if let value = info[key] { + + return value + } + let value = closure() + info[key] = value return value } - let value = closure() - self.data[key] = value - return value } @@ -110,6 +104,5 @@ public final class UserInfo { // MARK: Private - private var data: [UnsafeRawPointer: Any] = [:] - private let lock = NSRecursiveLock() + private let data: Internals.Mutex<[UnsafeRawPointer: any Sendable]> = .init([:]) } diff --git a/Sources/Where.Expression.swift b/Sources/Where.Expression.swift index 53b0065..5413f42 100644 --- a/Sources/Where.Expression.swift +++ b/Sources/Where.Expression.swift @@ -32,7 +32,7 @@ import CoreData /** Used only for `Where.Expression` type constraints. Currently supports `SingleTarget` and `CollectionTarget`. */ -public protocol WhereExpressionTrait {} +public protocol WhereExpressionTrait: SendableMetatype {} // MARK: - Where @@ -50,7 +50,7 @@ extension Where { ) ``` */ - public struct Expression: CustomStringConvertible, KeyPathStringConvertible { + public struct Expression: CustomStringConvertible, KeyPathStringConvertible, Sendable { /** Currently supports `SingleTarget` and `CollectionTarget`. diff --git a/Sources/Where.swift b/Sources/Where.swift index 496e773..e3aaf1a 100644 --- a/Sources/Where.swift +++ b/Sources/Where.swift @@ -32,7 +32,7 @@ import CoreData /** The `Where` clause specifies the conditions for a fetch or a query. */ -public struct Where: WhereClauseType, FetchClause, QueryClause, DeleteClause, Hashable { +public struct Where: WhereClauseType, FetchClause, QueryClause, DeleteClause, Hashable, Sendable { /** Combines two `Where` predicates together using `AND` operator @@ -432,7 +432,7 @@ public struct Where: WhereClauseType, FetchClause, QueryClause // MARK: AnyWhereClause - public let predicate: NSPredicate + public nonisolated(unsafe) let predicate: NSPredicate public init(_ predicate: NSPredicate) { diff --git a/Sources/XcodeDataModelSchema.swift b/Sources/XcodeDataModelSchema.swift index af69321..6ed7f06 100644 --- a/Sources/XcodeDataModelSchema.swift +++ b/Sources/XcodeDataModelSchema.swift @@ -164,6 +164,7 @@ public final class XcodeDataModelSchema: DynamicSchema { self.modelVersion = modelName self.modelVersionFileURL = modelVersionFileURL + self.rootModelFileURL = modelVersionFileURL.deletingLastPathComponent() } @@ -190,13 +191,9 @@ public final class XcodeDataModelSchema: DynamicSchema { internal let modelVersionFileURL: URL - private lazy var rootModelFileURL: URL = Internals.with { [unowned self] in - - return self.modelVersionFileURL.deletingLastPathComponent() - } - // MARK: Private - private weak var cachedRawModel: NSManagedObjectModel? + private let rootModelFileURL: URL + private nonisolated(unsafe) weak var cachedRawModel: NSManagedObjectModel? }