diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index b4326833..c8122564 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 /* PersistentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* PersistentID.swift */; }; + 5F13955F2FFCDBEF001EE0A4 /* PersistentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* PersistentID.swift */; }; + 5F1395602FFCDBEF001EE0A4 /* PersistentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* PersistentID.swift */; }; + 5F1395612FFCDBEF001EE0A4 /* PersistentID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* PersistentID.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 */; }; @@ -864,7 +872,7 @@ containerPortal = 2F03A52719C5C6DA005002A5 /* Project object */; proxyType = 1; remoteGlobalIDString = B52DD1731BE1F8CC00949AFE; - remoteInfo = "CoreStore OSX"; + remoteInfo = "CoreStore macOS"; }; B5D372871A39CF4D00F583D9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -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 /* PersistentID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentID.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 /* PersistentID.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 */, @@ -1891,9 +1903,9 @@ productReference = B5114DC928CEEE5400EEAE78 /* CoreStoreTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - B52DD1731BE1F8CC00949AFE /* CoreStore OSX */ = { + B52DD1731BE1F8CC00949AFE /* CoreStore macOS */ = { isa = PBXNativeTarget; - buildConfigurationList = B52DD18C1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStore OSX" */; + buildConfigurationList = B52DD18C1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStore macOS" */; buildPhases = ( B52DD1711BE1F8CC00949AFE /* Headers */, B52DD16F1BE1F8CC00949AFE /* Sources */, @@ -1904,14 +1916,14 @@ ); dependencies = ( ); - name = "CoreStore OSX"; - productName = "CoreStore OSX"; + name = "CoreStore macOS"; + productName = "CoreStore macOS"; productReference = B52DD1741BE1F8CC00949AFE /* CoreStore.framework */; productType = "com.apple.product-type.framework"; }; - B52DD17C1BE1F8CC00949AFE /* CoreStoreTests OSX */ = { + B52DD17C1BE1F8CC00949AFE /* CoreStoreTests macOS */ = { isa = PBXNativeTarget; - buildConfigurationList = B52DD18D1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStoreTests OSX" */; + buildConfigurationList = B52DD18D1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStoreTests macOS" */; buildPhases = ( B52DD1791BE1F8CC00949AFE /* Sources */, B52DD17A1BE1F8CC00949AFE /* Frameworks */, @@ -1922,8 +1934,8 @@ dependencies = ( B52DD1801BE1F8CD00949AFE /* PBXTargetDependency */, ); - name = "CoreStoreTests OSX"; - productName = "CoreStore OSXTests"; + name = "CoreStoreTests macOS"; + productName = "CoreStore macOSTests"; productReference = B52DD17D1BE1F8CC00949AFE /* CoreStoreTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; @@ -2004,8 +2016,8 @@ 82BA18911C4BBCBA00A0916E /* CoreStoreTests tvOS */, B563216E1BD65082006C9394 /* CoreStore watchOS */, B5114DA328CEEE5400EEAE78 /* CoreStoreTests watchOS */, - B52DD1731BE1F8CC00949AFE /* CoreStore OSX */, - B52DD17C1BE1F8CC00949AFE /* CoreStoreTests OSX */, + B52DD1731BE1F8CC00949AFE /* CoreStore macOS */, + B52DD17C1BE1F8CC00949AFE /* CoreStoreTests macOS */, ); }; /* End PBXProject section */ @@ -2131,6 +2143,7 @@ B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */, + 5F13955F2FFCDBEF001EE0A4 /* PersistentID.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 /* PersistentID.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 /* PersistentID.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 /* PersistentID.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 */, @@ -2953,7 +2973,7 @@ }; B52DD1801BE1F8CD00949AFE /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = B52DD1731BE1F8CC00949AFE /* CoreStore OSX */; + target = B52DD1731BE1F8CC00949AFE /* CoreStore macOS */; targetProxy = B52DD17F1BE1F8CD00949AFE /* PBXContainerItemProxy */; }; B5D372881A39CF4D00F583D9 /* PBXTargetDependency */ = { @@ -3016,8 +3036,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - MACOSX_DEPLOYMENT_TARGET = 13.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "-D DEBUG"; @@ -3027,12 +3047,12 @@ SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 16.6; + TVOS_DEPLOYMENT_TARGET = 17.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 9.6; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Debug; }; @@ -3083,8 +3103,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; - MACOSX_DEPLOYMENT_TARGET = 13.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStore; PRODUCT_NAME = CoreStore; @@ -3092,13 +3112,13 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 16.6; + TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 9.6; + WATCHOS_DEPLOYMENT_TARGET = 10.0; }; name = Release; }; @@ -3366,7 +3386,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 13.5; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 9.3.0; OTHER_LDFLAGS = ( "-weak_framework", @@ -3401,7 +3421,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 13.5; + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 9.3.0; OTHER_LDFLAGS = ( "-weak_framework", @@ -3582,7 +3602,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - B52DD18C1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStore OSX" */ = { + B52DD18C1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStore macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( B52DD1851BE1F8CD00949AFE /* Debug */, @@ -3591,7 +3611,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - B52DD18D1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStoreTests OSX" */ = { + B52DD18D1BE1F8CD00949AFE /* Build configuration list for PBXNativeTarget "CoreStoreTests macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( B52DD1871BE1F8CD00949AFE /* Debug */, diff --git a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore OSX.xcscheme b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore macOS.xcscheme similarity index 93% rename from CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore OSX.xcscheme rename to CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore macOS.xcscheme index 7a7d9b2f..0250c731 100644 --- a/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore OSX.xcscheme +++ b/CoreStore.xcodeproj/xcshareddata/xcschemes/CoreStore macOS.xcscheme @@ -16,7 +16,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "B52DD1731BE1F8CC00949AFE" BuildableName = "CoreStore.framework" - BlueprintName = "CoreStore OSX" + BlueprintName = "CoreStore macOS" ReferencedContainer = "container:CoreStore.xcodeproj"> @@ -32,7 +32,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "B52DD1731BE1F8CC00949AFE" BuildableName = "CoreStore.framework" - BlueprintName = "CoreStore OSX" + BlueprintName = "CoreStore macOS" ReferencedContainer = "container:CoreStore.xcodeproj"> @@ -43,7 +43,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "B52DD17C1BE1F8CC00949AFE" BuildableName = "CoreStoreTests.xctest" - BlueprintName = "CoreStoreTests OSX" + BlueprintName = "CoreStoreTests macOS" ReferencedContainer = "container:CoreStore.xcodeproj"> @@ -64,7 +64,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "B52DD1731BE1F8CC00949AFE" BuildableName = "CoreStore.framework" - BlueprintName = "CoreStore OSX" + BlueprintName = "CoreStore macOS" ReferencedContainer = "container:CoreStore.xcodeproj"> @@ -80,7 +80,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "B52DD1731BE1F8CC00949AFE" BuildableName = "CoreStore.framework" - BlueprintName = "CoreStore OSX" + BlueprintName = "CoreStore macOS" ReferencedContainer = "container:CoreStore.xcodeproj"> diff --git a/CoreStoreTests/BaseTestCase.swift b/CoreStoreTests/BaseTestCase.swift index f12acde4..cb8a7d80 100644 --- a/CoreStoreTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTestCase.swift @@ -28,6 +28,7 @@ import XCTest @testable import CoreStore + #if !SWIFT_PACKAGE extension Bundle { @@ -47,7 +48,11 @@ class BaseTestCase: XCTestCase { // MARK: Internal @nonobjc - func prepareStack(configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) throws -> Void) { + @MainActor + func prepareStack( + configurations: [ModelConfiguration] = [nil], + _ closure: (_ dataStack: DataStack) throws -> Void + ) { let stack = DataStack( xcodeModelName: "Model", @@ -79,6 +84,7 @@ class BaseTestCase: XCTestCase { } @nonobjc + @MainActor func expectLogger(_ expectations: [TestLogger.Expectation], closure: () throws -> T) rethrows -> T { CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations(expectations)) @@ -97,6 +103,7 @@ class BaseTestCase: XCTestCase { } @nonobjc + @MainActor func expectError(code: CoreStoreErrorCode, closure: () throws -> T) { CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations([.logError])) @@ -135,12 +142,14 @@ class BaseTestCase: XCTestCase { } @nonobjc + @MainActor func checkExpectationsImmediately() { self.waitForExpectations(timeout: 0, handler: { _ in }) } @nonobjc + @MainActor func waitAndCheckExpectations() { self.waitForExpectations(timeout: 10, handler: {_ in }) @@ -174,7 +183,7 @@ class BaseTestCase: XCTestCase { // MARK: - TestLogger -class TestLogger: CoreStoreLogger { +final class TestLogger: CoreStoreLogger { enum Expectation { @@ -187,13 +196,13 @@ class TestLogger: CoreStoreLogger { init(_ expectations: [Expectation: XCTestExpectation]) { - self.expectations = expectations + self.expectations = .init(expectations) } // MARK: CoreStoreLogger - var enableObjectConcurrencyDebugging: Bool = true + let enableObjectConcurrencyDebugging: Bool = true func log(level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { @@ -227,18 +236,21 @@ class TestLogger: CoreStoreLogger { // MARK: Private - private var expectations: [Expectation: XCTestExpectation] + private let expectations: Internals.Mutex<[Expectation: XCTestExpectation]> private func fulfill(_ expectation: Expectation) { - if let instance = self.expectations[expectation] { - - instance.fulfill() - self.expectations[expectation] = nil - } - else { + self.expectations.withLock { - XCTFail("Unexpected Logger Action: \(expectation)") + if let instance = $0[expectation] { + + instance.fulfill() + $0[expectation] = nil + } + else { + + XCTFail("Unexpected Logger Action: \(expectation)") + } } } } diff --git a/CoreStoreTests/ConvenienceTests.swift b/CoreStoreTests/ConvenienceTests.swift index dfb1a379..49590d39 100644 --- a/CoreStoreTests/ConvenienceTests.swift +++ b/CoreStoreTests/ConvenienceTests.swift @@ -35,6 +35,7 @@ import CoreStore class ConvenienceTests: BaseTestCase { @objc + @MainActor dynamic func test_ThatDataStacks_CanCreateFetchedResultsControllers() { self.prepareStack { (stack) in @@ -62,6 +63,7 @@ class ConvenienceTests: BaseTestCase { } @objc + @MainActor dynamic func test_ThatUnsafeDataTransactions_CanCreateFetchedResultsControllers() { self.prepareStack { (stack) in diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index d187b9ee..62716749 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -162,6 +162,18 @@ class Person: CoreStoreObject { @Field.Relationship("_spouseInverse", inverse: \.$spouse) private var spouseInverse: Person? + @Field.Coded("ownerInfosJson", coder: FieldCoders.Json.self) + var ownerInfosJson: [OwnerInfo] = [] + + @Field.Coded("ownerInfosPlist", coder: FieldCoders.Plist.self) + var ownerInfosPlist: [OwnerInfo] = [] + + struct OwnerInfo: Codable, Equatable { + var id: UUID + var displayName: String? + var otherIDs: Set + } + private static func getDisplayName(_ object: ObjectProxy, _ field: ObjectProxy.FieldProxy) -> String? { if let value = field.primitiveValue { @@ -189,6 +201,7 @@ class Person: CoreStoreObject { class DynamicModelTests: BaseTestDataTestCase { + @MainActor @objc dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() { @@ -203,7 +216,7 @@ class DynamicModelTests: BaseTestDataTestCase { versionLock: [ "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a], "Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e], - "Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992] + "Person": [0xbaec6549a025b59f, 0x3415c71e9f46fcf3, 0xb86b33433cb335eb, 0xfe441fde120dab67] ] ) ) @@ -223,6 +236,14 @@ class DynamicModelTests: BaseTestDataTestCase { let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done") let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done") let didSetObserverDone = self.expectation(description: "didSet-observe-done") + + let ownerInfos: [Person.OwnerInfo] = (1...3).map { + .init( + id: .init(), + displayName: "ownerInfo_\($0)", + otherIDs: Set((1...3).map({ _ in .init() })) + ) + } stack.perform( asynchronous: { (transaction) in @@ -373,11 +394,19 @@ class DynamicModelTests: BaseTestDataTestCase { person.job = .engineer XCTAssertEqual(person.job, .engineer) + person.ownerInfosJson = ownerInfos + XCTAssertEqual(person.ownerInfosJson, ownerInfos) + + person.ownerInfosPlist = ownerInfos + XCTAssertEqual(person.ownerInfosPlist, ownerInfos) + let personSnapshot2 = person.asSnapshot(in: transaction)! XCTAssertEqual(person.name, personSnapshot2.$name) XCTAssertEqual(person.title, personSnapshot2.$title) XCTAssertEqual(person.displayName, personSnapshot2.$displayName) XCTAssertEqual(person.job, personSnapshot2.$job) + XCTAssertEqual(person.ownerInfosJson, personSnapshot2.$ownerInfosJson) + XCTAssertEqual(person.ownerInfosPlist, personSnapshot2.$ownerInfosPlist) var personSnapshot3 = personSnapshot2 personSnapshot3.$name = "James" @@ -390,6 +419,8 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(personSnapshot3.$name, "James") XCTAssertEqual(personSnapshot3.$displayName, "Sir John") XCTAssertEqual(personSnapshot3.$job, .engineer) + XCTAssertEqual(personSnapshot3.$ownerInfosJson, ownerInfos) + XCTAssertEqual(personSnapshot3.$ownerInfosPlist, ownerInfos) @@ -409,6 +440,8 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(personPublisher.$name, "John") XCTAssertEqual(personPublisher.$displayName, "Sir John") XCTAssertEqual(personPublisher.$job, .engineer) + XCTAssertEqual(personPublisher.$ownerInfosJson, ownerInfos) + XCTAssertEqual(personPublisher.$ownerInfosPlist, ownerInfos) updateDone.fulfill() }, @@ -444,6 +477,8 @@ class DynamicModelTests: BaseTestDataTestCase { XCTAssertEqual(person!.customField.string, "customString") XCTAssertEqual(person!.job, .engineer) XCTAssertEqual(person!.pets.first, dog) + XCTAssertEqual(person!.ownerInfosJson, ownerInfos) + XCTAssertEqual(person!.ownerInfosPlist, ownerInfos) let p3 = Where({ $0.$age == 10 }) XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10)) diff --git a/CoreStoreTests/FetchTests.swift b/CoreStoreTests/FetchTests.swift index aa1a1950..26c28e56 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 21dded87..31a08add 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/GroupByTests.swift b/CoreStoreTests/GroupByTests.swift index 0e6e8504..369505fd 100644 --- a/CoreStoreTests/GroupByTests.swift +++ b/CoreStoreTests/GroupByTests.swift @@ -63,6 +63,7 @@ final class GroupByTests: BaseTestCase { } @objc + @MainActor dynamic func test_ThatGroupByClauses_ApplyToFetchRequestsCorrectly() { self.prepareStack { (dataStack) in diff --git a/CoreStoreTests/ImportTests.swift b/CoreStoreTests/ImportTests.swift index 3111c67c..71c0e520 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() { @@ -1046,17 +1060,17 @@ class ImportTests: BaseTestDataTestCase { // MARK: - TestInsertError -private struct TestInsertError: Error {} +private struct TestInsertError: Swift::Error {} // MARK: - TestUpdateError -private struct TestUpdateError: Error {} +private struct TestUpdateError: Swift::Error {} // MARK: - TestIDError -private struct TestIDError: Error {} +private struct TestIDError: Swift::Error {} // MARK: - TestEntity1 @@ -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 c5eb384f..0be23e12 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 { - - events += 1 + return events.withLock { events 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, 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 { - - 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, self.dateFormatter.date(from: "2000-01-11T00:00:00Z")!) + return events.withLock { events in - case NSNumber(value: 102)?: - XCTAssertEqual(indexPath?.section, 0) - XCTAssertEqual(indexPath?.item, 0) + XCTAssert(events == 1 || events == 2) - 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")!) + let userInfo = note.userInfo + XCTAssertNotNil(userInfo) + XCTAssertEqual( + Set(userInfo?.keys.map({ $0 as! String }) ?? []), + ["indexPath", "object"] + ) - default: - XCTFail() - } - defer { + let indexPath = userInfo?["indexPath"] as? IndexPath + let object = userInfo?["object"] as? TestEntity1 - events += 1 + 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 } - 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 + + XCTAssertEqual(events, 1) - 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 + + 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 - events += 1 + 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 + + 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)) - events += 1 + 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/ListPublisherTests.swift b/CoreStoreTests/ListPublisherTests.swift index 18169b21..f5d6923f 100644 --- a/CoreStoreTests/ListPublisherTests.swift +++ b/CoreStoreTests/ListPublisherTests.swift @@ -36,6 +36,7 @@ import CoreStore class ListPublisherTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatListPublishers_CanReceiveInsertNotifications() { self.prepareStack { (stack) in @@ -62,7 +63,7 @@ class ListPublisherTests: BaseTestDataTestCase { 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) @@ -70,7 +71,7 @@ class ListPublisherTests: 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 }, @@ -92,6 +93,7 @@ class ListPublisherTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatListPublishers_CanReceiveUpdateNotifications() { self.prepareStack { (stack) in @@ -126,7 +128,7 @@ class ListPublisherTests: BaseTestDataTestCase { let saveExpectation = self.expectation(description: "save") stack.perform( - asynchronous: { (transaction) -> Bool in + asynchronous: { [dateFormatter] (transaction) -> Bool in if let object = try transaction.fetchOne( From(), @@ -136,7 +138,7 @@ class ListPublisherTests: 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 { @@ -150,7 +152,7 @@ class ListPublisherTests: 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 { @@ -176,6 +178,7 @@ class ListPublisherTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatListPublishers_CanReceiveMoveNotifications() { self.prepareStack { (stack) in @@ -242,6 +245,7 @@ class ListPublisherTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatListPublishers_CanReceiveDeleteNotifications() { self.prepareStack { (stack) in diff --git a/CoreStoreTests/ObjectObserverTests.swift b/CoreStoreTests/ObjectObserverTests.swift index 8ca76d32..c2383410 100644 --- a/CoreStoreTests/ObjectObserverTests.swift +++ b/CoreStoreTests/ObjectObserverTests.swift @@ -34,6 +34,7 @@ import CoreStore class ObjectObserverTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatObjectObservers_CanReceiveUpdateNotifications() { self.prepareStack { (stack) in @@ -54,23 +55,28 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - var events = 0 + let events: Internals.Mutex = .init(0) + let persistentID = object.persistentID() _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"), object: observer, handler: { (note) -> Bool in - XCTAssertEqual(events, 0) - XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - ["object": object] as NSDictionary - ) - defer { + nonisolated(unsafe) let note = note + return events.withLock { events in - events += 1 + XCTAssertEqual(events, 0) + XCTAssertEqual( + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID + ) + defer { + + events += 1 + } + return events == 0 } - return events == 0 } ) _ = self.expectation( @@ -78,35 +84,40 @@ class ObjectObserverTests: BaseTestDataTestCase { object: observer, handler: { (note) -> Bool in - XCTAssertEqual(events, 1) - XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - [ - "object": object, - "changedPersistentKeys": Set( + nonisolated(unsafe) let note = note + return events.withLock { events in + + XCTAssertEqual(events, 1) + XCTAssertEqual( + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID + ) + XCTAssertEqual( + note.userInfo?["changedPersistentKeys"] as? Set, + Set( [ #keyPath(TestEntity1.testNumber), #keyPath(TestEntity1.testString) ] ) - ] as NSDictionary - ) - let object = note.userInfo?["object"] as? TestEntity1 - XCTAssertEqual(object?.testNumber, NSNumber(value: 10)) - XCTAssertEqual(object?.testString, "nil:TestEntity1:10") - - defer { + ) + let object = note.userInfo?["object"] as? TestEntity1 + XCTAssertEqual(object?.testNumber, NSNumber(value: 10)) + XCTAssertEqual(object?.testString, "nil:TestEntity1:10") - events += 1 + defer { + + events += 1 + } + return events == 1 } - return events == 1 } ) let saveExpectation = self.expectation(description: "save") stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() @@ -131,6 +142,7 @@ class ObjectObserverTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatObjectObservers_CanReceiveDeleteNotifications() { self.prepareStack { (stack) in @@ -151,30 +163,35 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - var events = 0 + let events: Internals.Mutex = .init(0) + let persistentID = object.persistentID() _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"), object: observer, handler: { (note) -> Bool in - XCTAssertEqual(events, 0) - XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - ["object": object] as NSDictionary - ) - defer { + nonisolated(unsafe) let note = note + return events.withLock { events in - events += 1 + XCTAssertEqual(events, 0) + XCTAssertEqual( + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID + ) + defer { + + events += 1 + } + return events == 0 } - return events == 0 } ) let saveExpectation = self.expectation(description: "save") stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() @@ -202,7 +219,7 @@ class ObjectObserverTests: BaseTestDataTestCase { // MARK: TestObjectObserver -class TestObjectObserver: ObjectObserver { +final class TestObjectObserver: ObjectObserver { typealias ObjectEntityType = TestEntity1 diff --git a/CoreStoreTests/ObjectPublisherTests.swift b/CoreStoreTests/ObjectPublisherTests.swift index b6ff73a4..98191d38 100644 --- a/CoreStoreTests/ObjectPublisherTests.swift +++ b/CoreStoreTests/ObjectPublisherTests.swift @@ -34,6 +34,7 @@ import CoreStore class ObjectPublisherTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatObjectPublishers_CanReceiveUpdateNotifications() { self.prepareStack { (stack) in @@ -62,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() @@ -93,6 +95,7 @@ class ObjectPublisherTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatObjectPublishers_CanReceiveDeleteNotifications() { self.prepareStack { (stack) in @@ -121,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 167692d2..3a0cee9e 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 e9841dcc..0530d8a0 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -27,6 +27,7 @@ import CoreData import XCTest @testable +@_spi(Internals) import CoreStore @@ -35,6 +36,7 @@ import CoreStore class SetupTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatDataStacks_ConfigureCorrectly() { do { diff --git a/CoreStoreTests/SwiftUIReaderStateTests.swift b/CoreStoreTests/SwiftUIReaderStateTests.swift new file mode 100644 index 00000000..daea48bc --- /dev/null +++ b/CoreStoreTests/SwiftUIReaderStateTests.swift @@ -0,0 +1,545 @@ +#if canImport(SwiftUI) && canImport(AppKit) + +import AppKit +import Combine +import SwiftUI +import XCTest + +@testable +import CoreStore + + +@MainActor +private final class ObjectPublisherModel: ObservableObject { + + @Published var objectPublisher: ObjectPublisher? + + init(_ objectPublisher: ObjectPublisher?) { + + self.objectPublisher = objectPublisher + } +} + + +@MainActor +private final class ListPublisherModel: ObservableObject { + + @Published var listPublisher: ListPublisher + + init(_ listPublisher: ListPublisher) { + + self.listPublisher = listPublisher + } +} + + +private struct SignalView: View { + + let value: Value + let report: (Value) -> Void + + + var body: some View { + + SwiftUI.Color.clear + .frame(width: 1, height: 1) + .onChange(of: self.value, initial: true) { _, value in + + self.report(value) + } + } +} + + +@MainActor +private struct ObjectStateHarness: View { + + @ObservedObject var model: ObjectPublisherModel + + let report: (String) -> Void + + @ObjectState + private var object: ObjectSnapshot? + + init( + model: ObjectPublisherModel, + report: @escaping (String) -> Void + ) { + + self.model = model + self.report = report + self._object = .init(model.objectPublisher) + } + + + var body: some View { + + SignalView( + value: self.object?.testString ?? "", + report: self.report + ) + } +} + + +@MainActor +private struct ListStateHarness: View { + + @ObservedObject var model: ListPublisherModel + + let report: (String) -> Void + + @ListState + private var list: ListSnapshot + + init( + model: ListPublisherModel, + report: @escaping (String) -> Void + ) { + + self.model = model + self.report = report + self._list = .init(model.listPublisher) + } + + + var body: some View { + + SignalView( + value: listSignature(self.list), + report: self.report + ) + } +} + + +@MainActor +private struct ObjectReaderHarness: View { + + @ObservedObject var model: ObjectPublisherModel + + let report: (String) -> Void + + + var body: some View { + + ObjectReader( + self.model.objectPublisher, + keyPath: \.testString, + content: { value in + + SignalView( + value: value ?? "", + report: self.report + ) + }, + placeholder: { + + SignalView( + value: "", + report: self.report + ) + } + ) + } +} + + +@MainActor +private struct ListReaderHarness: View { + + @ObservedObject var model: ListPublisherModel + + let report: (String) -> Void + + + var body: some View { + + ListReader(self.model.listPublisher) { list in + + SignalView( + value: listSignature(list), + report: self.report + ) + } + } +} + + +@MainActor +private func listSignature(_ list: ListSnapshot) -> String { + + let ids = list.map { + + String($0.testEntityID?.intValue ?? -1) + } + .joined(separator: ",") + return ids.isEmpty ? "" : ids +} + + +// MARK: - SwiftUIReaderStateTests + +@MainActor +final class SwiftUIReaderStateTests: BaseTestDataTestCase { + + @objc + dynamic func test_ThatObjectState_RebindsWhenPublisherChanges() { + + self.prepareStack { stack in + + self.prepareTestDataForStack(stack) + + let firstPublisher = self.objectPublisher(withID: 101, in: stack) + let secondPublisher = self.objectPublisher(withID: 102, in: stack) + let model = ObjectPublisherModel(firstPublisher) + + let initialExpectation = self.expectation(description: "initial") + let swappedExpectation = self.expectation(description: "swapped") + let finalExpectation = self.expectation(description: "final") + let staleExpectation = self.expectation(description: "stale") + staleExpectation.isInverted = true + + var didSeeInitial = false + var didSeeSwapped = false + var didSeeFinal = false + var values: [String] = [] + + let window = self.host( + ObjectStateHarness(model: model) { value in + + values.append(value) + switch value { + + case "nil:TestEntity1:1" where !didSeeInitial: + didSeeInitial = true + initialExpectation.fulfill() + + case "nil:TestEntity1:2" where !didSeeSwapped: + didSeeSwapped = true + swappedExpectation.fulfill() + + case "new-bound" where !didSeeFinal: + didSeeFinal = true + finalExpectation.fulfill() + + case "old-bound": + staleExpectation.fulfill() + + default: + break + } + } + ) + + self.wait(for: [initialExpectation], timeout: 10) + + model.objectPublisher = secondPublisher + self.wait(for: [swappedExpectation], timeout: 10) + + self.updateObjectString(withID: 101, to: "old-bound", in: stack) + self.updateObjectString(withID: 102, to: "new-bound", in: stack) + self.wait(for: [finalExpectation, staleExpectation], timeout: 10) + + XCTAssertFalse(values.contains("old-bound")) + withExtendedLifetime(window, {}) + } + } + + @objc + dynamic func test_ThatListState_RebindsWhenPublisherChanges() { + + self.prepareStack { stack in + + self.prepareTestDataForStack(stack) + + let firstPublisher = self.listPublisher(matching: true, in: stack) + let secondPublisher = self.listPublisher(matching: false, in: stack) + let model = ListPublisherModel(firstPublisher) + + let initialExpectation = self.expectation(description: "initial") + let swappedExpectation = self.expectation(description: "swapped") + let finalExpectation = self.expectation(description: "final") + let staleExpectation = self.expectation(description: "stale") + staleExpectation.isInverted = true + + var didSeeInitial = false + var didSeeSwapped = false + var didSeeFinal = false + var values: [String] = [] + + let window = self.host( + ListStateHarness(model: model) { value in + + values.append(value) + switch value { + + case "101,103,105" where !didSeeInitial: + didSeeInitial = true + initialExpectation.fulfill() + + case "102,104" where !didSeeSwapped: + didSeeSwapped = true + swappedExpectation.fulfill() + + case "102,104,108" where !didSeeFinal: + didSeeFinal = true + finalExpectation.fulfill() + + case "101,103,105,107": + staleExpectation.fulfill() + + default: + break + } + } + ) + + self.wait(for: [initialExpectation], timeout: 10) + + model.listPublisher = secondPublisher + self.wait(for: [swappedExpectation], timeout: 10) + + self.insertObject(withID: 107, string: "old-list", boolean: true, in: stack) + self.insertObject(withID: 108, string: "new-list", boolean: false, in: stack) + self.wait(for: [finalExpectation, staleExpectation], timeout: 10) + + XCTAssertFalse(values.contains("101,103,105,107")) + withExtendedLifetime(window, {}) + } + } + + @objc + dynamic func test_ThatObjectReaders_RebindAndShowCustomPlaceholders() { + + self.prepareStack { stack in + + self.prepareTestDataForStack(stack) + + let firstPublisher = self.objectPublisher(withID: 101, in: stack) + let secondPublisher = self.objectPublisher(withID: 102, in: stack) + let model = ObjectPublisherModel(firstPublisher) + + let initialExpectation = self.expectation(description: "initial") + let swappedExpectation = self.expectation(description: "swapped") + let placeholderExpectation = self.expectation(description: "placeholder") + let staleExpectation = self.expectation(description: "stale") + staleExpectation.isInverted = true + + var didSeeInitial = false + var didSeeSwapped = false + var didSeePlaceholder = false + var values: [String] = [] + + let window = self.host( + ObjectReaderHarness(model: model) { value in + + values.append(value) + switch value { + + case "nil:TestEntity1:1" where !didSeeInitial: + didSeeInitial = true + initialExpectation.fulfill() + + case "nil:TestEntity1:2" where !didSeeSwapped: + didSeeSwapped = true + swappedExpectation.fulfill() + + case "" where !didSeePlaceholder: + didSeePlaceholder = true + placeholderExpectation.fulfill() + + case "old-reader": + staleExpectation.fulfill() + + default: + break + } + } + ) + + self.wait(for: [initialExpectation], timeout: 10) + + model.objectPublisher = secondPublisher + self.wait(for: [swappedExpectation], timeout: 10) + + self.deleteObject(withID: 102, in: stack) + self.updateObjectString(withID: 101, to: "old-reader", in: stack) + self.wait(for: [placeholderExpectation, staleExpectation], timeout: 10) + + XCTAssertFalse(values.contains("old-reader")) + withExtendedLifetime(window, {}) + } + } + + @objc + dynamic func test_ThatListReaders_RebindWhenPublisherChanges() { + + self.prepareStack { stack in + + self.prepareTestDataForStack(stack) + + let firstPublisher = self.listPublisher(matching: true, in: stack) + let secondPublisher = self.listPublisher(matching: false, in: stack) + let model = ListPublisherModel(firstPublisher) + + let initialExpectation = self.expectation(description: "initial") + let swappedExpectation = self.expectation(description: "swapped") + let finalExpectation = self.expectation(description: "final") + let staleExpectation = self.expectation(description: "stale") + staleExpectation.isInverted = true + + var didSeeInitial = false + var didSeeSwapped = false + var didSeeFinal = false + var values: [String] = [] + + let window = self.host( + ListReaderHarness(model: model) { value in + + values.append(value) + switch value { + + case "101,103,105" where !didSeeInitial: + didSeeInitial = true + initialExpectation.fulfill() + + case "102,104" where !didSeeSwapped: + didSeeSwapped = true + swappedExpectation.fulfill() + + case "102,104,110" where !didSeeFinal: + didSeeFinal = true + finalExpectation.fulfill() + + case "101,103,105,109": + staleExpectation.fulfill() + + default: + break + } + } + ) + + self.wait(for: [initialExpectation], timeout: 10) + + model.listPublisher = secondPublisher + self.wait(for: [swappedExpectation], timeout: 10) + + self.insertObject(withID: 109, string: "old-reader", boolean: true, in: stack) + self.insertObject(withID: 110, string: "new-reader", boolean: false, in: stack) + self.wait(for: [finalExpectation, staleExpectation], timeout: 10) + + XCTAssertFalse(values.contains("101,103,105,109")) + withExtendedLifetime(window, {}) + } + } + + + // MARK: Private + + private func host(_ view: Content) -> NSWindow { + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 8, height: 8), + styleMask: [.borderless], + backing: .buffered, + defer: false + ) + window.contentView = NSHostingView(rootView: view) + window.contentView?.layoutSubtreeIfNeeded() + window.displayIfNeeded() + return window + } + + private func objectPublisher( + withID identifier: Int, + in stack: DataStack + ) -> ObjectPublisher { + + let object = try! stack.fetchOne( + From(), + Where( + #keyPath(TestEntity1.testEntityID), + isEqualTo: NSNumber(value: identifier) + ) + )! + return stack.publishObject(object) + } + + private func listPublisher( + matching boolean: Bool, + in stack: DataStack + ) -> ListPublisher { + + return stack.publishList( + From(), + Where( + #keyPath(TestEntity1.testBoolean), + isEqualTo: NSNumber(value: boolean) + ), + OrderBy(.ascending(#keyPath(TestEntity1.testEntityID))) + ) + } + + private func updateObjectString( + withID identifier: Int, + to string: String, + in stack: DataStack + ) { + + try! stack.perform( + synchronous: { transaction in + + let object = try transaction.fetchOne( + From(), + Where( + #keyPath(TestEntity1.testEntityID), + isEqualTo: NSNumber(value: identifier) + ) + )! + object.testString = string + } + ) + } + + private func deleteObject( + withID identifier: Int, + in stack: DataStack + ) { + + try! stack.perform( + synchronous: { transaction in + + let object = try transaction.fetchOne( + From(), + Where( + #keyPath(TestEntity1.testEntityID), + isEqualTo: NSNumber(value: identifier) + ) + )! + transaction.delete(object) + } + ) + } + + private func insertObject( + withID identifier: Int, + string: String, + boolean: Bool, + in stack: DataStack + ) { + + try! stack.perform( + synchronous: { transaction in + + let object = transaction.create(Into()) + object.testEntityID = NSNumber(value: identifier) + object.testString = string + object.testBoolean = NSNumber(value: boolean) + } + ) + } +} + +#endif diff --git a/CoreStoreTests/TransactionTests.swift b/CoreStoreTests/TransactionTests.swift index c63f7d3b..289728b0 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 { - - events += 1 + return events.withLock { events 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 { + + 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 d82c40f2..b494392e 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 1368aaea..481e5267 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 5F035E4F300F726700E98F8F /* WithMainActorImmediate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F035E4E300F725800E98F8F /* WithMainActorImmediate.swift */; }; B531EFE724EA762D005F247D /* Menu.PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFE624EA762D005F247D /* Menu.PlaceholderView.swift */; }; B531EFE924EB5A53005F247D /* Modern.PokedexDemo.PokedexEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */; }; B531EFEB24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */; }; @@ -25,8 +26,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 */; }; @@ -39,12 +39,11 @@ B5A3916024E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3915F24E6925900E7E8BD /* Modern.PlacemarksDemo.MapView.swift */; }; B5A3916224E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3916124E697BA00E7E8BD /* Modern.PlacemarksDemo.MainView.swift */; }; B5A3916524E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3916424E698C700E7E8BD /* Modern.PlacemarksDemo.Place.swift */; }; - B5A3916B24E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916724E698F900E7E8BD /* CoreStore.framework */; }; + B5A3916B24E698F900E7E8BD /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916724E698F900E7E8BD /* CoreStore.framework */; platformFilter = ios; }; B5A3916C24E698F900E7E8BD /* CoreStore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3916724E698F900E7E8BD /* CoreStore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; @@ -117,6 +116,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 5F035E4E300F725800E98F8F /* WithMainActorImmediate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithMainActorImmediate.swift; sourceTree = ""; }; B531EFE624EA762D005F247D /* Menu.PlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.PlaceholderView.swift; sourceTree = ""; }; B531EFE824EB5A52005F247D /* Modern.PokedexDemo.PokedexEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.PokedexEntry.swift; sourceTree = ""; }; B531EFEA24EB5ECD005F247D /* Modern.PokedexDemo.Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PokedexDemo.Service.swift; sourceTree = ""; }; @@ -136,8 +136,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 +158,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 +255,7 @@ B5A3911B24E5429200E7E8BD /* Sources */ = { isa = PBXGroup; children = ( - B5A3911C24E5429200E7E8BD /* AppDelegate.swift */, - B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */, + B5A3911C24E5429200E7E8BD /* DemoApp.swift */, B5A3915524E6858A00E7E8BD /* Demos */, B5A3917A24E6A75F00E7E8BD /* Helpers */, ); @@ -371,7 +368,7 @@ B5A3917A24E6A75F00E7E8BD /* Helpers */ = { isa = PBXGroup; children = ( - B5A3917B24E6A76C00E7E8BD /* LazyView.swift */, + 5F035E4E300F725800E98F8F /* WithMainActorImmediate.swift */, B5A3917F24E787D900E7E8BD /* InstructionsView.swift */, B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */, B5A3915424E6857F00E7E8BD /* Menu */, @@ -668,14 +665,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 */, @@ -693,6 +688,7 @@ B5A54401250487C7000DC5E3 /* Advanced.EvolutionDemo.ListView.swift in Sources */, B5A543FF250487B1000DC5E3 /* Advanced.EvolutionDemo.MainView.swift in Sources */, B5D6F209250E14AA00DF5D2F /* Advanced.EvolutionDemo.Migrator.swift in Sources */, + 5F035E4F300F726700E98F8F /* WithMainActorImmediate.swift in Sources */, B5C18F3325138700001BEFB3 /* Advanced.EvolutionDemo.ProgressView.swift in Sources */, B5D6F1F8250E07FD00DF5D2F /* Advanced.EvolutionDemo.V1.swift in Sources */, B5D6F210250E1E3200DF5D2F /* Advanced.EvolutionDemo.V1.xcdatamodeld in Sources */, @@ -801,7 +797,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 +833,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 +893,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 +906,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 +961,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 +973,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/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme index 5f5a183d..9e265772 100644 --- a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -3,9 +3,23 @@ LastUpgradeVersion = "1600" version = "1.3"> + parallelizeBuildables = "NO" + buildImplicitDependencies = "NO"> + + + + 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 907769e3..00000000 --- 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 00000000..03b18efd --- /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 a8561e01..1d62b21c 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 ead93d33..87e6c42a 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,12 +49,12 @@ extension Advanced.EvolutionDemo { ) } self.mutateItemAtIndex = { index in - - let object = listPublisher.snapshot[index] + + let persistentID = listPublisher.snapshot[index].persistentID() dataStack.perform( asynchronous: { transaction in - - object + + persistentID .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 402eb068..2d37842f 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 ce3700b2..3332d79f 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: 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) + } + ) + } + } + .listStyle(.plain) + } + + // 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) - } - } - ) - return Advanced.EvolutionDemo.ListView( - period: .ageOfMammals, - dataStack: dataStack, - dataSource: Advanced.EvolutionDemo.V4.Creature.dataSource(in: 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 072995a8..22377621 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( + .ignoresSafeArea(.container, edges: .vertical) + } + else { + 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) + } + .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 2ae71ca4..f9787a15 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) @@ -35,12 +37,18 @@ extension Advanced.EvolutionDemo { modelVersion: Advanced.EvolutionDemo.V3.name, entities: [ Entity("Creature") + ], + versionLock: [ + "Creature": [0x23cc88fece8e3ee0, 0xe37ecbcbe52eab30, 0xd3cc2e5f39a37f16, 0x79feebe14f602495] ] ), CoreStoreSchema( modelVersion: Advanced.EvolutionDemo.V4.name, entities: [ Entity("Creature") + ], + versionLock: [ + "Creature": [0xe7eaceec2a84d084, 0xac104aa167d8afe8, 0x6651e5ece419d490, 0xf73ec7f3512197de] ] ) ], @@ -49,12 +57,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 +80,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 +97,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 +134,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 +146,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 3568ceaf..3ff66f73 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 36e940fc..6b43154d 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 41e2d009..7fbe3f8c 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 aab4fa4e..992ac178 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 da7989d1..04d652e2 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 729e4ad5..e1c77dfe 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.DetailViewController.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailViewController.swift index e97f7409..17b6ff68 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailViewController.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.DetailViewController.swift @@ -48,7 +48,7 @@ extension Classic.ColorsDemo { /** ⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ObjectMonitor`s safely remove deallocated observers automatically. */ - deinit { + isolated deinit { self.palette.removeObserver(self) } @@ -87,13 +87,16 @@ extension Classic.ColorsDemo { // MARK: ObjectObserver - func objectMonitor( + nonisolated func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: Classic.ColorsDemo.Palette, + didUpdateObject object: sending Classic.ColorsDemo.Palette, changedPersistentKeys: Set ) { - self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + withMainActorImmediate { + + self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + } } @@ -248,10 +251,11 @@ extension Classic.ColorsDemo { private dynamic func hueSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let persistentID = self.palette.object?.persistentID() Classic.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { (transaction) in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(persistentID) palette?.hue = value }, completion: { _ in } @@ -262,10 +266,11 @@ extension Classic.ColorsDemo { private dynamic func saturationSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let persistentID = self.palette.object?.persistentID() Classic.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { (transaction) in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(persistentID) palette?.saturation = value }, completion: { _ in } @@ -276,10 +281,11 @@ extension Classic.ColorsDemo { private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let persistentID = self.palette.object?.persistentID() Classic.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { (transaction) in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(persistentID) palette?.brightness = value }, completion: { _ in } diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.LIstView.swift index b9586857..84a4f281 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 +// MARK: - Preview + +#Preview { - static var previews: some View { - - let minimumSamples = 10 - try! Classic.ColorsDemo.dataStack.perform( - synchronous: { transaction in + 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() - } + 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.ListViewController.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.ListViewController.swift index 940c160e..3f24adb5 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.ListViewController.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.ListViewController.swift @@ -26,7 +26,7 @@ extension Classic.ColorsDemo { /** ⭐️ Sample 2: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListMonitor`s safely remove deallocated observers automatically. */ - deinit { + isolated deinit { self.listMonitor.removeObserver(self) } @@ -40,59 +40,86 @@ extension Classic.ColorsDemo { typealias ListEntityType = Classic.ColorsDemo.Palette - func listMonitorWillChange(_ monitor: ListMonitor) { + nonisolated func listMonitorWillChange(_ monitor: ListMonitor) { - self.tableView.beginUpdates() + MainActor.assumeIsolated { + + self.tableView.beginUpdates() + } } - func listMonitorDidChange(_ monitor: ListMonitor) { + nonisolated func listMonitorDidChange(_ monitor: ListMonitor) { - self.tableView.endUpdates() + MainActor.assumeIsolated { + + self.tableView.endUpdates() + } } - func listMonitorDidRefetch(_ monitor: ListMonitor) { + nonisolated func listMonitorDidRefetch(_ monitor: ListMonitor) { - self.tableView.reloadData() + MainActor.assumeIsolated { + + self.tableView.reloadData() + } } // MARK: ListObjectObserver - func listMonitor(_ monitor: ListMonitor, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) { + nonisolated func listMonitor(_ monitor: ListMonitor, didInsertObject object: ListEntityType, toIndexPath indexPath: IndexPath) { - self.tableView.insertRows(at: [indexPath], with: .automatic) + MainActor.assumeIsolated { + + self.tableView.insertRows(at: [indexPath], with: .automatic) + } } - func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { + nonisolated func listMonitor(_ monitor: ListMonitor, didDeleteObject object: ListEntityType, fromIndexPath indexPath: IndexPath) { - self.tableView.deleteRows(at: [indexPath], with: .automatic) + MainActor.assumeIsolated { + + self.tableView.deleteRows(at: [indexPath], with: .automatic) + } } - func listMonitor(_ monitor: ListMonitor, didUpdateObject object: ListEntityType, atIndexPath indexPath: IndexPath) { + nonisolated func listMonitor(_ monitor: ListMonitor, didUpdateObject object: sending ListEntityType, atIndexPath indexPath: IndexPath) { - if case let cell as Classic.ColorsDemo.ItemCell = self.tableView.cellForRow(at: indexPath) { - - cell.setPalette(object) + MainActor.assumeIsolated { + + if case let cell as Classic.ColorsDemo.ItemCell = self.tableView.cellForRow(at: indexPath) { + + cell.setPalette(object) + } } } - func listMonitor(_ monitor: ListMonitor, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) { + nonisolated func listMonitor(_ monitor: ListMonitor, didMoveObject object: ListEntityType, fromIndexPath: IndexPath, toIndexPath: IndexPath) { - self.tableView.deleteRows(at: [fromIndexPath], with: .automatic) - self.tableView.insertRows(at: [toIndexPath], with: .automatic) + MainActor.assumeIsolated { + + self.tableView.deleteRows(at: [fromIndexPath], with: .automatic) + self.tableView.insertRows(at: [toIndexPath], with: .automatic) + } } // MARK: ListSectionObserver - func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) { + nonisolated func listMonitor(_ monitor: ListMonitor, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int) { - self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic) + MainActor.assumeIsolated { + + self.tableView.insertSections(IndexSet(integer: sectionIndex), with: .automatic) + } } - func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) { + nonisolated func listMonitor(_ monitor: ListMonitor, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int) { - self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic) + MainActor.assumeIsolated { + + self.tableView.deleteSections(IndexSet(integer: sectionIndex), with: .automatic) + } } @@ -131,11 +158,11 @@ extension Classic.ColorsDemo { switch editingStyle { case .delete: - let object = self.listMonitor[indexPath] + let persistentID = self.listMonitor[indexPath].persistentID() Classic.ColorsDemo.dataStack.perform( asynchronous: { (transaction) in - transaction.delete(object) + transaction.delete(persistentID) }, completion: { _ in } ) diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift index a40a803c..99cf6967 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) + } ) + .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) + } } - 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 { + .navigationTitle("Colors (\(self.listHelper.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() + } + } + } } - - + + // 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() + + nonisolated func listMonitorDidChange(_ monitor: ListMonitor) { + let count = monitor.numberOfObjects() + + Task { @MainActor in + self.count = count + } } - - func listMonitorDidRefetch(_ monitor: ListMonitor) { - - self.objectWillChange.send() - self.count = monitor.numberOfObjects() + + nonisolated func listMonitorDidRefetch(_ monitor: ListMonitor) { + let count = monitor.numberOfObjects() + + Task { @MainActor in + self.count = count + } } - + // MARK: ListObjectObserver - - func listMonitor(_ monitor: ListMonitor, didDeleteObject object: Classic.ColorsDemo.Palette, fromIndexPath indexPath: IndexPath) { - - if self.selectedPalette == object { - self.setSelectedPalette(nil) + 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 a0122a09..6006c00f 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 47a42781..5b0b5ee2 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -10,7 +10,8 @@ import SwiftUI extension Modern.ColorsDemo { // MARK: - Modern.ColorsDemo.MainView - + + @MainActor struct MainView: View { // MARK: Internal @@ -20,7 +21,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 +32,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 +110,7 @@ extension Modern.ColorsDemo { Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + try transaction.deleteAll(From()) }, sourceIdentifier: TransactionSource.clear, @@ -108,10 +119,10 @@ extension Modern.ColorsDemo { } private func addColor() { - + Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + _ = transaction.create(Into()) }, sourceIdentifier: TransactionSource.add, @@ -120,12 +131,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 +147,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 b83e1b8d..8dc59dca 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 @@ -12,6 +11,7 @@ extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.DetailView + @MainActor struct DetailView: View { /** @@ -31,9 +31,10 @@ extension Modern.ColorsDemo.SwiftUI { @Binding private var brightness: Float - + init(_ palette: ObjectPublisher) { - + + let persistentID = palette.persistentID() self._palette = .init(palette) self._hue = Binding( get: { palette.hue ?? 0 }, @@ -42,7 +43,7 @@ extension Modern.ColorsDemo.SwiftUI { Modern.ColorsDemo.dataStack.perform( asynchronous: { (transaction) in - let palette = palette.asEditable(in: transaction) + let palette = persistentID.asEditable(in: transaction) palette?.hue = percentage }, completion: { _ in } @@ -56,7 +57,7 @@ extension Modern.ColorsDemo.SwiftUI { Modern.ColorsDemo.dataStack.perform( asynchronous: { (transaction) in - let palette = palette.asEditable(in: transaction) + let palette = persistentID.asEditable(in: transaction) palette?.saturation = percentage }, completion: { _ in } @@ -70,7 +71,7 @@ extension Modern.ColorsDemo.SwiftUI { Modern.ColorsDemo.dataStack.perform( asynchronous: { (transaction) in - let palette = palette.asEditable(in: transaction) + let palette = persistentID.asEditable(in: transaction) palette?.brightness = percentage }, completion: { _ in } @@ -78,7 +79,7 @@ extension Modern.ColorsDemo.SwiftUI { } ) } - + // MARK: View @@ -87,12 +88,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 +108,7 @@ extension Modern.ColorsDemo.SwiftUI { step: 1 / 359 ) } + HStack { Text("S: \(Int(palette.$saturation * 100))%") .frame(width: 80) @@ -111,6 +118,7 @@ extension Modern.ColorsDemo.SwiftUI { step: 1 / 100 ) } + HStack { Text("B: \(Int(palette.$brightness * 100))%") .frame(width: 80) @@ -131,30 +139,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 850813fa..e63f761f 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift @@ -11,6 +11,7 @@ extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.ItemView + @MainActor struct ItemView: View { /** @@ -34,41 +35,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 e11a4cfe..0a59f056 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,8 @@ import SwiftUI extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.ListView - + + @MainActor struct ListView: View { /** @@ -40,7 +41,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 +65,7 @@ extension Modern.ColorsDemo.SwiftUI { } } // .animation(.default) // breaks layout - .listStyle(PlainListStyle()) - .edgesIgnoringSafeArea([]) + .listStyle(.plain) } @@ -81,8 +81,8 @@ extension Modern.ColorsDemo.SwiftUI { ) Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - - transaction.delete(objectIDs: objectIDsToDelete) + + transaction.delete(persistentIDs: objectIDsToDelete) }, completion: { _ in } ) @@ -90,35 +90,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 bedbe521..b12e6f11 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 b2a5077e..d1e36261 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? + sourceIdentifier: (any Sendable)? ) { - self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + MainActor.assumeIsolated { + + self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + } } @@ -245,42 +248,48 @@ extension Modern.ColorsDemo.UIKit { private let saturationSlider: UISlider = .init() private let brightnessSlider: UISlider = .init() + @MainActor @objc private dynamic func hueSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let paletteID = self.palette.object?.persistentID() Modern.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { transaction in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(paletteID) palette?.hue = value }, completion: { _ in } ) } + @MainActor @objc private dynamic func saturationSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let paletteID = self.palette.object?.persistentID() Modern.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { transaction in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(paletteID) palette?.saturation = value }, completion: { _ in } ) } + @MainActor @objc private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) { let value = sender.value + let paletteID = self.palette.object?.persistentID() Modern.ColorsDemo.dataStack.perform( - asynchronous: { [weak self] (transaction) in + asynchronous: { transaction in - let palette = transaction.edit(self?.palette.object) + let palette = transaction.edit(paletteID) palette?.brightness = value }, completion: { _ in } @@ -288,5 +297,3 @@ extension Modern.ColorsDemo.UIKit { } } } - - 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 1965bc81..15b4e15c 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 46e5e6f7..297e2b95 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]) + + transaction.delete(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 ab8b7cc7..f31172ce 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 { @@ -74,7 +77,7 @@ extension Modern { // MARK: - TransactionSource - enum TransactionSource { + enum TransactionSource: Sendable { case add case delete diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.Geocoder.swift index a8c39346..ef4094ad 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 ?? "" + + 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 { - completion( - placemark.name, - CNPostalAddressFormatter.string( - from: address, - style: .mailingAddress - ) - ) + 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 dcc896fa..de920bf4 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,8 @@ import SwiftUI extension Modern.PlacemarksDemo { // MARK: - Modern.PlacemarksDemo.MainView - + + @MainActor struct MainView: View { /** @@ -22,10 +22,11 @@ extension Modern.PlacemarksDemo { */ private func demoAsynchronousTransaction(coordinate: CLLocationCoordinate2D) { + let persistentID = self.$place?.persistentID() Modern.PlacemarksDemo.dataStack.perform( asynchronous: { (transaction) in - let place = self.$place?.asEditable(in: transaction) + let place = persistentID?.asEditable(in: transaction) place?.annotation = .init(coordinate: coordinate) }, completion: { _ in } @@ -38,7 +39,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 +69,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 +102,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?.persistentID() == place.persistentID() 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() - } -} - - -#if DEBUG - -struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - Modern.PlacemarksDemo.MainView() + @State + private var geocoder = Modern.PlacemarksDemo.Geocoder() } } - -#endif diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift index 86764c18..7fc7c21a 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.Form.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Form.swift index 93990839..f693c381 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Form.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Form.swift @@ -34,7 +34,7 @@ extension Modern.PokedexDemo { // MARK: ImportableObject - typealias ImportSource = Dictionary + typealias ImportSource = Dictionary // MARK: ImportableUniqueObject diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift index c84f5d32..7abb5b3e 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -2,18 +2,18 @@ // 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 - + + @MainActor struct MainView: View { - + // MARK: Internal init( @@ -22,16 +22,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 +56,10 @@ extension Modern.PokedexDemo { .padding() } } - .navigationBarTitle("Pokedex") + .navigationTitle("Pokedex") } - - + + // MARK: Private @ListState( @@ -68,28 +68,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.PokedexEntry.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift index 89c02a1b..033977b6 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.PokedexEntry.swift @@ -36,7 +36,7 @@ extension Modern.PokedexDemo { // MARK: ImportableObject - typealias ImportSource = (index: Int, json: Dictionary) + typealias ImportSource = (index: Int, json: Dictionary) func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws { diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index a365b18d..a696f4d8 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(Modern.PokedexDemo.Service.Error) { - 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(.init(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 detailsPersistentID: PersistentID, + from data: Data + ) async throws -> ObjectSnapshot { - return .init { promise in + let speciesPersistentID = try await Modern.PokedexDemo.dataStack.async.perform { transaction in - Modern.PokedexDemo.dataStack.perform( - asynchronous: { transaction -> Modern.PokedexDemo.Species in - - let json: Dictionary = try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) + let json: Dictionary = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) + ) + guard + let species = try transaction.importUniqueObject( + Into(), + source: .init(json: json) ) - 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))) - } - } - ) + else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + transaction + .edit(Into(), detailsPersistentID)? + .species = species + return species.persistentID() } + guard + let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesPersistentID), + 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 detailsPersistentID: PersistentID, + 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 Sendable] } - 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(), detailsPersistentID)? + .forms = forms + } } - } - - - // MARK: Internal - - private(set) var isLoading: Bool = false { - - willSet { + catch { - self.objectWillChange.send() + throw self.mapError(.init(error)) } } - private(set) var lastError: (error: Modern.PokedexDemo.Service.Error, retry: () -> Void)? { - - willSet { - - self.objectWillChange.send() - } - } - + + // MARK: Internal + + 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,157 +176,239 @@ 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: String(species.$id), + detailsPersistentID: details.persistentID(), + 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) - } + let key = pokedexEntry.$id + guard self.detailTasks[key] == nil else { + + return + } + let speciesURL = pokedexEntry.$speciesURL + let detailsPersistentID = details.persistentID() + self.detailTasks[key] = Task { [weak self] in + + guard let self else { + + return + } + defer { + + self.detailTasks.removeValue(forKey: key) + } + await self.fetchSpecies( + key: key, + detailsPersistentID: detailsPersistentID, + speciesURL: speciesURL ) + } } - private func fetchFormsIfNeeded(for species: ObjectSnapshot) { + + // 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 { - guard - let details = species.$details?.snapshot, - details.$forms.isEmpty - else { + 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 } - self.cancellable["forms.\(species.$id)"] = species - .$formsURLs - .map( - { - URLSession.shared - .dataTaskPublisher(for: $0) - .mapError({ Modern.PokedexDemo.Service.Error.networkError($0) }) - .eraseToAnyPublisher() - } + 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, + detailsPersistentID: PersistentID, + speciesURL: URL + ) async { + + do { + + let (data, _) = try await URLSession.shared.data(from: speciesURL) + try Task.checkCancellation() + + let species = try await Self.importSpecies( + for: detailsPersistentID, + from: data ) - .reduce( - into: Just<[URLSession.DataTaskPublisher.Output]>([]) - .setFailureType(to: Modern.PokedexDemo.Service.Error.self) - .eraseToAnyPublisher(), - { (result, publisher) in - result = result - .zip(publisher, { $0 + [$1] }) - .eraseToAnyPublisher() - } + guard species.$details?.snapshot?.$forms.isEmpty == true else { + + return + } + await self.fetchForms( + detailsPersistentID: detailsPersistentID, + formsURLs: species.$formsURLs ) - .flatMap({ Self.importForms(for: details, from: $0) }) - .sink( - receiveCompletion: { completion in - - switch completion { - - case .finished: - break - - case .failure(let error): - print(error) - } - }, - receiveValue: { _ in } + } + 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, + detailsPersistentID: PersistentID, + 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( + detailsPersistentID: detailsPersistentID, + formsURLs: formsURLs + ) + } + } + + private func fetchForms( + detailsPersistentID: PersistentID, + 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: detailsPersistentID, + 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) - case otherError(Swift.Error) + case otherError(Swift::Error) case unexpected } } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Species.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Species.swift index e1ed4e5e..c6896490 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Species.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Species.swift @@ -65,7 +65,10 @@ extension Modern.PokedexDemo { // MARK: ImportableObject - typealias ImportSource = Dictionary + struct ImportSource: @unchecked Sendable { + + let json: Dictionary + } // MARK: ImportableUniqueObject @@ -82,14 +85,14 @@ extension Modern.PokedexDemo { static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType? { - let json = source + let json = source.json return try Modern.PokedexDemo.Service.parseJSON(json["id"]) } func update(from source: ImportSource, in transaction: BaseDataTransaction) throws { typealias Service = Modern.PokedexDemo.Service - let json = source + let json = source.json self.name = try Service.parseJSON(json["name"]) self.weight = try Service.parseJSON(json["weight"]) 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 e4c2cd7e..eebaa6de 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 -#endif +#Preview { + Modern.PokedexDemo.UIKit.ListView() +} diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListViewController.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListViewController.swift index cf2e7168..e1df82dc 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListViewController.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListViewController.swift @@ -50,7 +50,7 @@ extension Modern.PokedexDemo.UIKit { fatalError() } - deinit { + isolated deinit { self.listPublisher.removeObserver(self) } @@ -94,13 +94,25 @@ extension Modern.PokedexDemo.UIKit { private func startObservingList() { - self.listPublisher.addObserver(self) { (listPublisher) in - - self.dataSource.apply( - listPublisher.snapshot, - animatingDifferences: true - ) - } + self.listPublisher.addObserver( + self, + notifyInitial: false, + { [weak self] (listPublisher) in + + let snapshot = listPublisher.snapshot + withMainActorImmediate { + + guard let self else { + + return + } + self.dataSource.apply( + snapshot, + animatingDifferences: true + ) + } + } + ) self.dataSource.apply( self.listPublisher.snapshot, animatingDifferences: false diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.swift index a472b33a..c86a455c 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 63619381..7b336aa8 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 3037da0d..fc86b8b0 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 8f06df4c..298f97d8 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 a58e93f8..7aaf6a21 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 bdd3ae61..aeb301a9 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 fd74aa8e..00000000 --- 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 4f208fc8..a070ff25 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 - } -} - -#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 - } - ) + fileprivate let isEnabled: Bool } } - -#endif diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift index 36a3129d..946f4647 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -11,124 +11,95 @@ import SwiftUI extension Menu { // MARK: - Menu.MainView - + struct MainView: View { + + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass // MARK: View + @ViewBuilder 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() - } - ) - 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) + + if self.horizontalSizeClass == .compact { + NavigationStack { + + self.menuList + } + } + else { + NavigationSplitView( + sidebar: { + + self.menuList + }, + detail: { + + Menu.PlaceholderView() + } + ) + } + } + + + // MARK: Private + + @ViewBuilder + private var menuList: some View { + List { + + ForEach(Menu.Section.allCases, id: \.self) { section in + + SwiftUI::Section( + content: { + + ForEach(section.routes) { route in + + NavigationLink( + destination: { + LazyDestination { + route.destination + } }, - 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 + label: { + Menu.ItemView( + title: route.title, + subtitle: route.subtitle, + isEnabled: route.isEnabled ) - }, - detailView: { objectPublisher in - Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) } ) + .disabled(!route.isEnabled) } - ) - 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() - } - ) - } - 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) - } + }, + header: { + + Text(section.rawValue) + } + ) } - .listStyle(GroupedListStyle()) - .navigationBarTitle("CoreStore Demos") - Menu.PlaceholderView() } - .navigationViewStyle(DoubleColumnNavigationViewStyle()) + .navigationTitle("CoreStore Demos") + .listStyle(.sidebar) } - } -} - -#if DEBUG - -struct _Demo_Menu_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - Menu.MainView() + + // MARK: - LazyDestination + + private struct LazyDestination: View { + + init(@ViewBuilder content: @escaping () -> Content) { + + self.content = content + } + + var body: some View { + + self.content() + } + + private let content: () -> Content + } } } - -#endif diff --git a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift index 96183277..03f7bc90 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 34de26d4..32fd4766 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/Helpers/WithMainActorImmediate.swift b/Demo/Sources/Helpers/WithMainActorImmediate.swift new file mode 100644 index 00000000..22f6c43f --- /dev/null +++ b/Demo/Sources/Helpers/WithMainActorImmediate.swift @@ -0,0 +1,32 @@ +// +// WithMainActorImmediate.swift +// Demo +// +// Created by John Estropia on 2026/07/21. +// + +import Foundation + + +// MARK: - withMainActorImmediate + +func withMainActorImmediate( + _ task: @MainActor @Sendable @escaping () -> Void +) { + + if #available(iOS 26.0, *) { + + Task.immediate(operation: task) + } + else if Thread.isMainThread { + + MainActor.assumeIsolated { + + task() + } + } + else { + + Task.init(operation: task) + } +} diff --git a/Demo/Sources/SceneDelegate.swift b/Demo/Sources/SceneDelegate.swift deleted file mode 100644 index afb6e32f..00000000 --- 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/Package.swift b/Package.swift index fff7d7ba..97e0b4bd 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ import PackageDescription let package = Package( name: "CoreStore", platforms: [ - .macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9) + .macOS(.v14), .iOS(.v17), .tvOS(.v17), .watchOS(.v10) ], products: [ .library(name: "CoreStore", type: .dynamic, targets: ["CoreStore"]) diff --git a/Playground_macOS.playground/Contents.swift b/Playground_macOS.playground/Contents.swift index 05f6fa75..12a5d1ab 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/README.md b/README.md index eb07208c..b18a87b1 100644 --- a/README.md +++ b/README.md @@ -2201,6 +2201,23 @@ var body: some View { ) } ``` +The `placeholder:` overload also works with `keyPath:` projections: +```swift +let person: ObjectPublisher + +var body: some View { + ObjectReader( + self.person, + keyPath: \.fullName, + content: { fullName in + Text("Name: \(fullName)") + }, + placeholder: { + Text("Record not found") + } + ) +} +``` ### SwiftUI Property Wrappers @@ -2234,7 +2251,8 @@ If a `ListPublisher` instance is not available yet, the fetch can be done inline From() .sectionBy(\.age) .where(\.isMember == true) - .orderBy(.ascending(\.lastName)) + .orderBy(.ascending(\.lastName)), + in: Globals.dataStack ) var people: ListSnapshot @@ -2509,4 +2527,3 @@ I'd love to hear about apps using CoreStore. Send me a message and I'll welcome # License CoreStore is released under an MIT license. See the [LICENSE](https://raw.githubusercontent.com/JohnEstropia/CoreStore/master/LICENSE) file for more information - diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index 46e0891c..62d48d3a 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`. @@ -54,7 +55,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { `Result.success` indicates that the transaction succeeded, either because the save succeeded or because there were no changes to save. The associated `userInfo` is the value returned from the transaction closure. `Result.failure` indicates that the transaction either failed or was cancelled. The associated object for this value is a `CoreStoreError` enum value. */ - public typealias Result = Swift.Result + public typealias Result = Swift::Result // MARK: - @@ -78,6 +79,25 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { return super.create(into) } + /** + Returns an editable proxy of the object with the specified `PersistentID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `PersistentID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ persistentID: PersistentID? + ) -> O? { + + Internals.assert( + !self.isCommitted, + "Attempted to update an entity of type \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))." + ) + + return super.edit(persistentID) + } + /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. @@ -96,6 +116,26 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { return super.edit(object) } + /** + Returns an editable proxy of the object with the specified `PersistentID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `PersistentID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ into: Into, + _ persistentID: PersistentID + ) -> O? { + + Internals.assert( + !self.isCommitted, + "Attempted to update an entity of type \(Internals.typeName(into.entityClass)) from an already committed \(Internals.typeName(self))." + ) + + return super.edit(into, persistentID) + } + /** Returns an editable proxy of the object with the specified `NSManagedObjectID`. @@ -115,6 +155,23 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { return super.edit(into, objectID) } + + /** + Deletes the objects with the specified `NSManagedObjectID`s. + + - parameter objectIDs: the `NSManagedObjectID`s of the objects to delete + */ + public override func delete( + persistentIDs: S + ) where S.Iterator.Element == PersistentID { + + Internals.assert( + !self.isCommitted, + "Attempted to delete an entities from an already committed \(Internals.typeName(self))." + ) + + super.delete(persistentIDs: persistentIDs) + } /** Deletes the objects with the specified `NSManagedObjectID`s. @@ -175,7 +232,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { internal init( mainContext: NSManagedObjectContext, queue: DispatchQueue, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { super.init( @@ -188,7 +245,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { } internal func autoCommit( - _ completion: @escaping ( + _ completion: @escaping @MainActor @Sendable ( _ hasChanges: Bool, _ error: CoreStoreError? ) -> Void @@ -197,12 +254,14 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { self.isCommitted = true let group = DispatchGroup() group.enter() + + nonisolated(unsafe) let transaction = self self.context.saveAsynchronously( sourceIdentifier: self.sourceIdentifier, completion: { (hasChanges, error) -> Void in completion(hasChanges, error) - self.result = (hasChanges, error) + transaction.result = (hasChanges, error) group.leave() } ) diff --git a/Sources/BaseDataTransaction+Importing.swift b/Sources/BaseDataTransaction+Importing.swift index 0a7a82cd..615064b7 100644 --- a/Sources/BaseDataTransaction+Importing.swift +++ b/Sources/BaseDataTransaction+Importing.swift @@ -42,7 +42,7 @@ extension BaseDataTransaction { public func importObject( _ into: Into, source: O.ImportSource - ) throws(any Swift.Error) -> O? { + ) throws(any Swift::Error) -> O? { Internals.assert( self.isRunningInAllowedQueue(), @@ -73,7 +73,7 @@ extension BaseDataTransaction { public func importObject( _ object: O, source: O.ImportSource - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { Internals.assert( self.isRunningInAllowedQueue(), @@ -102,7 +102,7 @@ extension BaseDataTransaction { public func importObjects( _ into: Into, sourceArray: S - ) throws(any Swift.Error) -> [O] where S.Iterator.Element == O.ImportSource { + ) throws(any Swift::Error) -> [O] where S.Iterator.Element == O.ImportSource { Internals.assert( self.isRunningInAllowedQueue(), @@ -139,7 +139,7 @@ extension BaseDataTransaction { public func importUniqueObject( _ into: Into, source: O.ImportSource - ) throws(any Swift.Error) -> O? { + ) throws(any Swift::Error) -> O? { Internals.assert( self.isRunningInAllowedQueue(), @@ -194,8 +194,8 @@ extension BaseDataTransaction { sourceArray: S, preProcess: @escaping ( _ mapping: [O.UniqueIDType: O.ImportSource] - ) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } - ) throws(any Swift.Error) -> [O] where S.Iterator.Element == O.ImportSource { + ) throws(any Swift::Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } + ) throws(any Swift::Error) -> [O] where S.Iterator.Element == O.ImportSource { Internals.assert( self.isRunningInAllowedQueue(), diff --git a/Sources/BaseDataTransaction+Querying.swift b/Sources/BaseDataTransaction+Querying.swift index c74eaa06..5c1f2825 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 `PersistentID` for the object + - returns: the `DynamicObject` instance if the object exists in the transaction, or `nil` if not found. + */ + public func fetchExisting( + _ persistentID: PersistentID + ) -> 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 `PersistentID`. + + - parameter objectIDs: the `PersistentID` 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 == PersistentID { + + 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 e09bd9b6..18168f0a 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -26,9 +26,6 @@ import Foundation import CoreData - -// MARK: - BaseDataTransaction - /** The `BaseDataTransaction` is an abstract interface for `NSManagedObject` creates, updates, and deletes. All `BaseDataTransaction` subclasses manage a private `NSManagedObjectContext` which are direct children of the `NSPersistentStoreCoordinator`'s root `NSManagedObjectContext`. This means that all updates are saved first to the persistent store, and then propagated up to the read-only `NSManagedObjectContext`. */ @@ -115,6 +112,27 @@ public /*abstract*/ class BaseDataTransaction { } } + /** + Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. + + - parameter persistentID: the `PersistentID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public func edit( + _ persistentID: PersistentID? + ) -> 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`. @@ -136,6 +154,30 @@ public /*abstract*/ class BaseDataTransaction { return self.context.fetchExisting(object) } + /** + Returns an editable proxy of the object with the specified `PersistentID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `PersistentID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public func edit( + _ into: Into, + _ persistentID: PersistentID + ) -> O? { + + Internals.assert( + self.isRunningInAllowedQueue(), + "Attempted to update an entity of type \(Internals.typeName(into.entityClass)) outside its designated queue." + ) + Internals.assert( + into.inferStoreIfPossible + || (into.configuration ?? DataStack.defaultConfigurationName) == persistentID.managedObjectID.persistentStore?.configurationName, + "Attempted to update an entity of type \(Internals.typeName(into.entityClass)) but the specified persistent store do not match the `NSManagedObjectID`." + ) + return self.fetchExisting(persistentID) + } + /** Returns an editable proxy of the object with the specified `NSManagedObjectID`. @@ -159,6 +201,26 @@ public /*abstract*/ class BaseDataTransaction { ) return self.fetchExisting(objectID) } + + /** + Deletes the objects with the specified `PersistentID`s. + + - parameter persistentIDs: the `PersistentID`s of the objects to delete + */ + public func delete( + persistentIDs: S + ) where S.Iterator.Element == PersistentID { + + Internals.assert( + self.isRunningInAllowedQueue(), + "Attempted to delete an entity outside its designated queue." + ) + let context = self.context + persistentIDs.forEach { + + context.fetchExisting($0).map({ context.delete($0.cs_toRaw()) }) + } + } /** Deletes the objects with the specified `NSManagedObjectID`s. @@ -441,7 +503,7 @@ public /*abstract*/ class BaseDataTransaction { /** An arbitrary value that identifies the source of this transaction. Callers of the transaction can provide this value through the `DataStack.perform(...)` methods. */ - public let sourceIdentifier: Any? + public let sourceIdentifier: (any Sendable)? /** Allow external libraries to store custom data in the transaction. App code should rarely have a need for this. @@ -471,7 +533,7 @@ public /*abstract*/ class BaseDataTransaction { queue: DispatchQueue, supportsUndo: Bool, bypassesQueueing: Bool, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { let context = mainContext.temporaryContextInTransactionWithConcurrencyType( @@ -493,7 +555,10 @@ public /*abstract*/ class BaseDataTransaction { } else if context.undoManager == nil { - context.undoManager = UndoManager() + Internals.mainActorImmediate { + + context.undoManager = UndoManager() + } } } diff --git a/Sources/CoreStore+CustomDebugStringConvertible.swift b/Sources/CoreStore+CustomDebugStringConvertible.swift index e362e267..5d1e034a 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/CoreStore+Logging.swift b/Sources/CoreStore+Logging.swift index 242cfccc..5d9b0344 100644 --- a/Sources/CoreStore+Logging.swift +++ b/Sources/CoreStore+Logging.swift @@ -100,6 +100,6 @@ extension Internals { lineNumber: lineNumber, functionName: functionName ) - Swift.fatalError(message, file: fileName, line: UInt(lineNumber)) + Swift::fatalError(message, file: fileName, line: UInt(lineNumber)) } } diff --git a/Sources/CoreStoreDefaults.swift b/Sources/CoreStoreDefaults.swift index 26ddec69..df9d7f93 100644 --- a/Sources/CoreStoreDefaults.swift +++ b/Sources/CoreStoreDefaults.swift @@ -36,7 +36,29 @@ public enum CoreStoreDefaults { /** The `CoreStoreLogger` instance to be used. The default logger is an instance of a `DefaultLogger`. */ - public static var logger: CoreStoreLogger = DefaultLogger() + public static var logger: CoreStoreLogger { + + get { + + return self.loggerInstance.withLock { + + if let logger = $0 { + + return logger + } + let logger = DefaultLogger() + $0 = logger + return logger + } + } + set { + + self.loggerInstance.withLock { + + $0 = newValue + } + } + } /** The default `DataStack` instance to be used. If `defaultStack` is not set during the first time accessed, a default-configured `DataStack` will be created. @@ -47,21 +69,23 @@ public enum CoreStoreDefaults { public static var dataStack: DataStack { get { - - self.defaultStackBarrierQueue.sync(flags: .barrier) { - - if self.defaultStackInstance == nil { - - self.defaultStackInstance = DataStack() + + return self.defaultStackInstance.withLock { + + if let dataStack = $0 { + + return dataStack } + let dataStack = DataStack() + $0 = dataStack + return dataStack } - return self.defaultStackInstance! } set { - self.defaultStackBarrierQueue.async(flags: .barrier) { - - self.defaultStackInstance = newValue + self.defaultStackInstance.withLock { + + $0 = newValue } } } @@ -69,7 +93,6 @@ public enum CoreStoreDefaults { // MARK: Private - private static let defaultStackBarrierQueue = DispatchQueue.concurrent("com.coreStore.defaultStackBarrierQueue", qos: .userInteractive) - - private static var defaultStackInstance: DataStack? + 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 a7f9a804..d657fc7e 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -24,7 +24,7 @@ // import Foundation -import CoreData +@preconcurrency import CoreData // MARK: - CoreStoreError @@ -32,7 +32,7 @@ import CoreData /** All errors thrown from CoreStore are expressed in `CoreStoreError` enum values. */ -public enum CoreStoreError: Error, CustomNSError, Hashable { +public enum CoreStoreError: Swift::Error, CustomNSError, Hashable, Sendable { /** A failure occured because of an unknown error. @@ -67,7 +67,7 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { /** The transaction was terminated by a user-thrown `Error`. */ - case userError(error: Error) + case userError(error: Swift::Error) /** The transaction was cancelled by the user. @@ -82,7 +82,7 @@ public enum CoreStoreError: Error, CustomNSError, Hashable { /** Casts any `Error` to a known `CoreStoreError`, or wraps it in `CoreStoreError.internalError(NSError:)`. */ - public init(_ error: Error?) { + public init(_ error: Swift::Error?) { guard let error = error else { diff --git a/Sources/CoreStoreLogger.swift b/Sources/CoreStoreLogger.swift index 31e59469..0bf0a5c5 100644 --- a/Sources/CoreStoreLogger.swift +++ b/Sources/CoreStoreLogger.swift @@ -45,7 +45,7 @@ public enum LogLevel { /** Custom loggers should implement the `CoreStoreLogger` protocol and pass its instance to `CoreStoreDefaults.logger`. Calls to `log(...)`, `assert(...)`, and `abort(...)` are not tied to a specific queue/thread, so it is the implementer's job to handle thread-safety. */ -public protocol CoreStoreLogger { +public protocol CoreStoreLogger: Sendable { /** Handles log messages sent by the `CoreStore` framework. @@ -124,6 +124,6 @@ extension CoreStoreLogger { functionName: StaticString ) { - Swift.fatalError(message, file: fileName, line: UInt(lineNumber)) + Swift::fatalError(message, file: fileName, line: UInt(lineNumber)) } } diff --git a/Sources/CoreStoreManagedObject.swift b/Sources/CoreStoreManagedObject.swift index d663ecb8..b332859e 100644 --- a/Sources/CoreStoreManagedObject.swift +++ b/Sources/CoreStoreManagedObject.swift @@ -29,7 +29,8 @@ import Foundation // MARK: - CoreStoreManagedObject -@objc internal class CoreStoreManagedObject: NSManagedObject { +@objc +internal class CoreStoreManagedObject: NSManagedObject { internal typealias CustomGetter = @convention(block) (_ rawObject: Any) -> Any? internal typealias CustomSetter = @convention(block) (_ rawObject: Any, _ newValue: Any?) -> Void @@ -42,12 +43,3 @@ import Foundation return "_\(NSStringFromClass(CoreStoreManagedObject.self))__\(modelVersion)__\(NSStringFromClass(entity.type))__\(entity.entityName)" } } - - -// MARK: - Private - -private enum Static { - - static let queue = DispatchQueue.concurrent("com.coreStore.coreStoreManagerObjectBarrierQueue", qos: .userInteractive) - static var cache: [ObjectIdentifier: [KeyPathString: Set]] = [:] -} diff --git a/Sources/CoreStoreObject+Observing.swift b/Sources/CoreStoreObject+Observing.swift index 46fd5b28..a0e3cd03 100644 --- a/Sources/CoreStoreObject+Observing.swift +++ b/Sources/CoreStoreObject+Observing.swift @@ -439,7 +439,7 @@ fileprivate final class _CoreStoreObjectKeyValueObservation: NSObject, CoreStore // workaround for Erroneous (?) error when using bridging in the Foundation overlay @nonobjc - static var swizzler: Any? = Internals.with { + static let swizzler: Void? = Internals.with { let bridgeClass: AnyClass = _CoreStoreObjectKeyValueObservation.self let rootObserveImpl = class_getInstanceMethod( diff --git a/Sources/CoreStoreObject.swift b/Sources/CoreStoreObject.swift index 8d84b25d..e848c85d 100644 --- a/Sources/CoreStoreObject.swift +++ b/Sources/CoreStoreObject.swift @@ -266,6 +266,6 @@ fileprivate enum Static { // MARK: FilePrivate - fileprivate static var metaCache: [ObjectIdentifier: Any] = [:] - fileprivate static var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:] + fileprivate static nonisolated(unsafe) var metaCache: [ObjectIdentifier: Any] = [:] + fileprivate static nonisolated(unsafe) var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:] } diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index 7f5afb92..61961130 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 d75f8db2..4937ccd4 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,10 +105,10 @@ 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 + ) throws(any Swift::Error) -> Void /** The `CustomMapping.inferredTransformation` method can be used directly as the `transformer` if the changes can be inferred (i.e. lightweight). @@ -116,7 +116,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { public static func inferredTransformation( _ sourceObject: UnsafeSourceObject, _ createDestinationObject: () -> UnsafeDestinationObject - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { let destinationObject = createDestinationObject() destinationObject.enumerateAttributes { (attribute, sourceAttribute) in @@ -556,7 +556,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { let userInfo = mapping.userInfo! let transformer = userInfo[CustomEntityMigrationPolicy.UserInfoKey.transformer]! as! CustomMapping.Transformer @@ -588,7 +588,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { forDestination dInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { try super.createRelationships(forDestination: dInstance, in: mapping, manager: manager) } @@ -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+Concurrency.swift b/Sources/DataStack+Concurrency.swift index 45237120..a0add5c0 100644 --- a/Sources/DataStack+Concurrency.swift +++ b/Sources/DataStack+Concurrency.swift @@ -46,7 +46,7 @@ extension DataStack { /** Swift concurrency for the `DataStack` are exposed through this namespace. Extend this type if you need to add other `async` utilities for `DataStack`. */ - public struct AsyncNamespace { + public struct AsyncNamespace: Sendable { // MARK: Public @@ -85,7 +85,7 @@ extension DataStack.AsyncNamespace { */ public func addStorage( _ storage: T - ) async throws(any Swift.Error) -> T { + ) async throws(any Swift::Error) -> T { return try await Internals.withCheckedThrowingContinuation { continuation in @@ -118,13 +118,13 @@ extension DataStack.AsyncNamespace { */ public func addStorage( _ storage: T - ) -> AsyncThrowingStream, any Swift.Error> { + ) -> AsyncThrowingStream, any Swift::Error> { return .init( bufferingPolicy: .unbounded, { continuation in - - var progress: Progress? = nil + + nonisolated(unsafe) var progress: Progress? = nil progress = self.base.addStorage( storage, completion: { result in @@ -151,14 +151,17 @@ extension DataStack.AsyncNamespace { ) if let progress = progress { - progress.setProgressHandler { progress in + Internals.mainActorImmediate { @MainActor in + + progress.setProgressHandler { progress in - continuation.yield( - .migrating( - storage: storage, - progressObject: progress + continuation.yield( + .migrating( + storage: storage, + progressObject: progress + ) ) - ) + } } } } @@ -181,11 +184,11 @@ extension DataStack.AsyncNamespace { public func importObject( _ into: Into, source: O.ImportSource - ) async throws(any Swift.Error) -> O? { + ) async throws(any Swift::Error) -> O? { return try await Internals.withCheckedThrowingContinuation { continuation in - self.base.perform( + return self.base.perform( asynchronous: { (transaction) -> O? in return try transaction.importObject( @@ -196,10 +199,13 @@ extension DataStack.AsyncNamespace { success: { continuation.resume( - with: .success($0.flatMap(self.base.fetchExisting)) + returning: $0.flatMap(self.base.fetchExisting) ) }, - failure: continuation.resume(throwing:) + failure: { + + continuation.resume(throwing: $0) + } ) } } @@ -220,8 +226,9 @@ extension DataStack.AsyncNamespace { public func importObject( _ object: O, source: O.ImportSource - ) async throws(any Swift.Error) -> O? { + ) async throws(any Swift::Error) -> O? { + nonisolated(unsafe) let object = object return try await Internals.withCheckedThrowingContinuation { continuation in self.base.perform( @@ -240,10 +247,13 @@ extension DataStack.AsyncNamespace { success: { continuation.resume( - with: .success($0.flatMap(self.base.fetchExisting)) + returning: $0.flatMap(self.base.fetchExisting) ) }, - failure: continuation.resume(throwing:) + failure: { + + continuation.resume(throwing: $0) + } ) } } @@ -264,7 +274,7 @@ extension DataStack.AsyncNamespace { public func importUniqueObject( _ into: Into, source: O.ImportSource - ) async throws(any Swift.Error) -> O? { + ) async throws(any Swift::Error) -> O? { return try await Internals.withCheckedThrowingContinuation { continuation in @@ -279,10 +289,13 @@ extension DataStack.AsyncNamespace { success: { continuation.resume( - with: .success($0.flatMap(self.base.fetchExisting)) + returning: $0.flatMap(self.base.fetchExisting) ) }, - failure: continuation.resume(throwing:) + failure: { + + continuation.resume(throwing: $0) + } ) } } @@ -306,13 +319,13 @@ extension DataStack.AsyncNamespace { - returns: The imported objects correctly associated for the `DataStack`. - throws: A `CoreStoreError` value indicating the failure reason */ - public func importUniqueObjects( + public func importUniqueObjects( _ into: Into, sourceArray: S, preProcess: @escaping @Sendable ( _ mapping: [O.UniqueIDType: O.ImportSource] - ) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } - ) async throws(any Swift.Error) -> [O] + ) throws(any Swift::Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } + ) async throws(any Swift::Error) -> [O] where S.Iterator.Element == O.ImportSource { return try await Internals.withCheckedThrowingContinuation { continuation in @@ -329,10 +342,13 @@ extension DataStack.AsyncNamespace { success: { continuation.resume( - with: .success(self.base.fetchExisting($0)) + returning: self.base.fetchExisting($0) ) }, - failure: continuation.resume(throwing:) + failure: { + + continuation.resume(throwing: $0) + } ) } } @@ -357,9 +373,9 @@ extension DataStack.AsyncNamespace { - returns: The value returned from the `task` closure. - throws: A `CoreStoreError` value indicating the failure reason */ - public func perform( - _ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift.Error) -> Output - ) async throws(any Swift.Error) -> Output { + public func perform( + _ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift::Error) -> Output + ) async throws(any Swift::Error) -> Output { return try await Internals.withCheckedThrowingContinuation { continuation in diff --git a/Sources/DataStack+DataSources.swift b/Sources/DataStack+DataSources.swift index 617ef196..b3dedfeb 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 8f94f902..c7251f0d 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -49,7 +49,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) { self.coordinator.performAsynchronously { @@ -110,7 +110,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) -> Progress? { let fileURL = storage.fileURL @@ -162,7 +162,7 @@ extension DataStack { attributes: nil ) - let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + nonisolated(unsafe) let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( ofType: type(of: storage).storeType, at: fileURL as URL, options: storage.storeOptions @@ -192,12 +192,18 @@ extension DataStack { } catch { - completion(.failure(CoreStoreError(error))) + DispatchQueue.main.async { + + completion(.failure(CoreStoreError(error))) + } } return } - completion(.failure(CoreStoreError(error))) + DispatchQueue.main.async { + + completion(.failure(CoreStoreError(error))) + } return } @@ -212,7 +218,10 @@ extension DataStack { } catch { - completion(.failure(CoreStoreError(error))) + DispatchQueue.main.async { + + completion(.failure(CoreStoreError(error))) + } } } ) @@ -264,7 +273,7 @@ extension DataStack { */ public func upgradeStorageIfNeeded( _ storage: T, - completion: @escaping (MigrationResult) -> Void + completion: @escaping @MainActor @Sendable (MigrationResult) -> Void ) throws(CoreStoreError) -> Progress? { return try self.coordinator.performSynchronously { @@ -376,7 +385,7 @@ extension DataStack { private func upgradeStorageIfNeeded( _ storage: T, metadata: [String: Any], - completion: @escaping (MigrationResult) -> Void + completion: @escaping @MainActor @Sendable (MigrationResult) -> Void ) -> Progress? { guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else { @@ -423,9 +432,13 @@ extension DataStack { } let migrationTypes = migrationSteps.map { $0.migrationType } - var migrationResult: MigrationResult? + let migrationState: Internals.Mutex<(migrationResult: MigrationResult?, cancelled: Bool)> = .init( + ( + migrationResult: nil, + cancelled: false + ) + ) var operations = [Operation]() - var cancelled = false let progress = Progress(parent: nil, userInfo: nil) progress.totalUnitCount = numberOfMigrations @@ -437,15 +450,21 @@ 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 - guard let self = self, !cancelled else { + guard + let self = self, + !migrationState.withLock({ $0.cancelled }) + else { return } - autoreleasepool { + Internals.autoreleasepool { do { @@ -465,8 +484,14 @@ extension DataStack { migrationError, "Failed to migrate version model \"\(migrationType.sourceVersion)\" to version \"\(migrationType.destinationVersion)\"." ) - migrationResult = .failure(migrationError) - cancelled = true + migrationState.withLock { state in + + if !state.cancelled { + + state.migrationResult = .failure(migrationError) + state.cancelled = true + } + } } } @@ -485,6 +510,7 @@ extension DataStack { operations.forEach { migrationOperation.addDependency($0) } migrationOperation.addExecutionBlock { () -> Void in + let migrationResult = migrationState.withLock { $0.migrationResult } DispatchQueue.main.async { progress.setProgressHandler(nil) @@ -594,22 +620,28 @@ extension DataStack { let estimatedTime: TimeInterval = 60 * 3 // 3 mins let interval: TimeInterval = 1 let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount) - var fakeProgress: Float = 0 + let fakeProgress: Internals.Mutex = .init(0) - var recursiveCheck: () -> Void = {} - recursiveCheck = { [weak timerQueue] in + @Sendable + func recursiveCheck() { - guard let timerQueue = timerQueue, fakeProgress < 1 else { + let enqueueNext = fakeProgress.withLock { - return + guard $0 < 1.0 else { + + return false + } + progress.completedUnitCount = Int64(fakeTotalUnitCount * $0) + $0 += Float(interval / estimatedTime) + return true + } + if enqueueNext { + + timerQueue.asyncAfter( + deadline: .now() + interval, + execute: recursiveCheck + ) } - progress.completedUnitCount = Int64(fakeTotalUnitCount * fakeProgress) - fakeProgress += Float(interval / estimatedTime) - - timerQueue.asyncAfter( - deadline: .now() + interval, - execute: recursiveCheck - ) } timerQueue.async(execute: recursiveCheck) @@ -624,9 +656,11 @@ extension DataStack { ) ) } - timerQueue.sync { - - fakeProgress = 1 + 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 62bac1fc..b16333e0 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 { @@ -45,7 +46,7 @@ extension DataStack { Thread.isMainThread, "Attempted to observe objects from \(Internals.typeName(self)) outside the main thread." ) - return .init(objectID: object.cs_id(), context: self.unsafeContext()) + return .init(managedObjectID: object.cs_id(), context: self.unsafeContext()) } /** @@ -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,8 +128,9 @@ 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 (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ fetchClauses: FetchClause... ) { @@ -144,8 +149,9 @@ 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 (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ fetchClauses: [FetchClause] ) { @@ -187,8 +193,9 @@ 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 (ListMonitor) -> Void, + 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,8 +297,9 @@ 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 (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: FetchClause... @@ -310,8 +321,9 @@ 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 (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: [FetchClause] @@ -355,8 +367,9 @@ 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 (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B ) { diff --git a/Sources/DataStack+Querying.swift b/Sources/DataStack+Querying.swift index 6c48c153..572949cc 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 `PersistentID` for the object + - returns: the `DynamicObject` instance if the object exists in the `DataStack`, or `nil` if not found. + */ + public func fetchExisting( + _ persistentID: PersistentID + ) -> 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 `PersistentID`. + + - parameter objectIDs: the `PersistentID` 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 == PersistentID { + + return self.mainContext.fetchExisting(objectIDs) + } + /** Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `NSManagedObjectID`. diff --git a/Sources/DataStack+Reactive.swift b/Sources/DataStack+Reactive.swift index 7ae01e4b..780fe88b 100644 --- a/Sources/DataStack+Reactive.swift +++ b/Sources/DataStack+Reactive.swift @@ -47,7 +47,7 @@ extension DataStack { /** Combine utilities for the `DataStack` are exposed through this namespace. Extend this type if you need to add other Combine Publisher utilities for `DataStack`. */ - public struct ReactiveNamespace { + public struct ReactiveNamespace: Sendable { // MARK: Public @@ -99,6 +99,7 @@ extension DataStack.ReactiveNamespace { return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.addStorage( storage, completion: { (result) in @@ -179,6 +180,7 @@ extension DataStack.ReactiveNamespace { return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.perform( asynchronous: { (transaction) -> O? in @@ -222,8 +224,10 @@ extension DataStack.ReactiveNamespace { source: O.ImportSource ) -> Future { + nonisolated(unsafe) let object = object return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.perform( asynchronous: { (transaction) -> O? in @@ -273,6 +277,7 @@ extension DataStack.ReactiveNamespace { return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.perform( asynchronous: { (transaction) -> O? in @@ -316,16 +321,17 @@ extension DataStack.ReactiveNamespace { - parameter preProcess: a closure that lets the caller tweak the internal `UniqueIDType`-to-`ImportSource` mapping to be used for importing. Callers can remove from/add to/update `mapping` and return the updated array from the closure. - returns: A `Future` for the imported objects. The event values will be the object instances correctly associated for the `DataStack`. */ - public func importUniqueObjects( + public func importUniqueObjects( _ into: Into, sourceArray: S, - preProcess: @escaping ( + preProcess: @escaping @Sendable ( _ mapping: [O.UniqueIDType: O.ImportSource] - ) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } + ) throws(any Swift::Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } ) -> Future<[O], CoreStoreError> where S.Iterator.Element == O.ImportSource { return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.perform( asynchronous: { (transaction) -> [O] in @@ -370,14 +376,15 @@ extension DataStack.ReactiveNamespace { - parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`. - returns: A `Future` whose event value be the value returned from the `task` closure. */ - public func perform( - _ asynchronous: @escaping ( + public func perform( + _ asynchronous: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> Output + ) throws(any Swift::Error) -> Output ) -> Future { return .init { (promise) in + nonisolated(unsafe) let promise = promise self.base.perform( asynchronous: asynchronous, success: { promise(.success($0)) }, diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index e0e0988e..14fffcb7 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -38,12 +38,12 @@ extension DataStack { - 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. - parameter completion: the closure executed after the save completes. The `Result` argument of the closure will either wrap the return value of `task`, or any uncaught errors thrown from within `task`. Cancelled `task`s will be indicated by `.failure(error: CoreStoreError.userCancelled)`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`. */ - public func perform( - asynchronous task: @escaping ( + public func perform( + asynchronous task: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> T, - sourceIdentifier: Any? = nil, - completion: @escaping (AsynchronousDataTransaction.Result) -> Void + ) throws(any Swift::Error) -> T, + sourceIdentifier: (any Sendable)? = nil, + completion: @escaping @MainActor @Sendable (AsynchronousDataTransaction.Result) -> Void ) { self.perform( @@ -63,22 +63,22 @@ extension DataStack { - parameter failure: the closure executed if the save fails or if any errors are thrown within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`. */ public func perform( - asynchronous task: @escaping ( + asynchronous task: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> T, - sourceIdentifier: Any? = nil, - success: @escaping (T) -> Void, - failure: @escaping (CoreStoreError) -> Void + ) throws(any Swift::Error) -> T, + sourceIdentifier: (any Sendable)? = nil, + success: @escaping @MainActor @Sendable (sending T) -> Void, + failure: @escaping @MainActor @Sendable (CoreStoreError) -> Void ) { - let transaction = AsynchronousDataTransaction( + nonisolated(unsafe) let transaction = AsynchronousDataTransaction( mainContext: self.rootSavingContext, queue: self.childTransactionQueue, sourceIdentifier: sourceIdentifier ) transaction.transactionQueue.cs_async { - let userInfo: T + nonisolated(unsafe) let userInfo: T do { userInfo = try task(transaction) @@ -123,9 +123,9 @@ extension DataStack { public func perform( synchronous task: ( _ transaction: SynchronousDataTransaction - ) throws(any Swift.Error) -> T, + ) throws(any Swift::Error) -> T, waitForAllObservers: Bool = true, - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) throws(CoreStoreError) -> T { let transaction = SynchronousDataTransaction( @@ -172,7 +172,7 @@ extension DataStack { */ public func beginUnsafe( supportsUndo: Bool = false, - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) -> UnsafeDataTransaction { return UnsafeDataTransaction( @@ -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 cc2ec73d..b12d006e 100644 --- a/Sources/DataStack.AddStoragePublisher.swift +++ b/Sources/DataStack.AddStoragePublisher.swift @@ -90,7 +90,8 @@ extension DataStack { // MARK: - AddStorageSubscription - fileprivate final class AddStorageSubscription: Subscription where S.Input == Output, S.Failure == CoreStoreError { + fileprivate final class AddStorageSubscription: Subscription + where S.Input == Output, S.Failure == CoreStoreError { // MARK: FilePrivate @@ -114,15 +115,16 @@ extension DataStack { return } - var progress: Progress? = nil + 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 { @@ -150,21 +152,24 @@ extension DataStack { ) if let progress = progress { - progress.setProgressHandler { [weak self] progress in + Internals.mainActorImmediate { @MainActor in - guard - let self = self, - let subscriber = self.subscriber - else { + progress.setProgressHandler { progress in - return - } - _ = subscriber.receive( - .migrating( - storage: self.storage, - progressObject: progress + guard + let self = weakSelf, + let subscriber = self.subscriber + else { + + return + } + _ = subscriber.receive( + .migrating( + storage: self.storage, + progressObject: progress + ) ) - ) + } } } } diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index 0567b4d7..bfc6b74e 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 { +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 { - parameter completion: the closure to execute after all persistent stores are removed */ public func unsafeRemoveAllPersistentStores( - completion: @escaping () -> Void = {} + completion: @escaping @MainActor @Sendable () -> Void = {} ) { let coordinator = self.coordinator @@ -459,14 +459,13 @@ public final class DataStack: Equatable { // MARK: Internal - internal static var defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME" + internal static let defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME" internal let coordinator: NSPersistentStoreCoordinator internal let rootSavingContext: NSManagedObjectContext 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 { } 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,33 +502,32 @@ public final class DataStack: Equatable { 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) - } - else if !inferStoreIfPossible { + return (store: metadata.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false) + } + else if !inferStoreIfPossible { - return (store: nil, isAmbiguous: false) + return (store: nil, isAmbiguous: false) + } } - } - switch configurationsForEntity.count { + switch configurationsForEntity.count { - case 0: - return (store: nil, isAmbiguous: false) + case 0: + return (store: nil, isAmbiguous: false) - case 1 where inferStoreIfPossible: - return (store: self.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false) + case 1 where inferStoreIfPossible: + return (store: metadata.persistentStoresByFinalConfiguration[configurationsForEntity.first!], isAmbiguous: false) - default: - return (store: nil, isAmbiguous: true) + default: + return (store: nil, isAmbiguous: true) + } } } @@ -538,7 +535,7 @@ public final class DataStack: Equatable { _ storage: StorageInterface, finalURL: URL?, finalStoreOptions: [AnyHashable: Any]? - ) throws(any Swift.Error) -> NSPersistentStore { + ) throws(any Swift::Error) -> NSPersistentStore { let persistentStore = try self.coordinator.addPersistentStore( ofType: type(of: storage).storeType, @@ -550,26 +547,24 @@ public final class DataStack: Equatable { 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 { // 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/DefaultLogger.swift b/Sources/DefaultLogger.swift index 438f692f..e1563696 100644 --- a/Sources/DefaultLogger.swift +++ b/Sources/DefaultLogger.swift @@ -55,29 +55,29 @@ public final class DefaultLogger: CoreStoreLogger { functionName: StaticString ) { - #if DEBUG - let icon: String - let levelString: String - switch level { - - case .trace: - icon = "🔹" - levelString = "Trace" - - case .notice: - icon = "🔸" - levelString = "Notice" - - case .warning: - icon = "⚠️" - levelString = "Warning" - - case .fatal: - icon = "❗" - levelString = "Fatal" - } - Swift.print("\(icon) [CoreStore: \(levelString)] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") - #endif +#if DEBUG + let icon: String + let levelString: String + switch level { + + case .trace: + icon = "🔹" + levelString = "Trace" + + case .notice: + icon = "🔸" + levelString = "Notice" + + case .warning: + icon = "⚠️" + levelString = "Warning" + + case .fatal: + icon = "❗" + levelString = "Fatal" + } + Swift::print("\(icon) [CoreStore: \(levelString)] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") +#endif } /** @@ -97,9 +97,9 @@ public final class DefaultLogger: CoreStoreLogger { functionName: StaticString ) { - #if DEBUG - Swift.print("⚠️ [CoreStore: Error] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n \(error)\n") - #endif +#if DEBUG + Swift::print("⚠️ [CoreStore: Error] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n \(error)\n") +#endif } /** @@ -119,14 +119,14 @@ public final class DefaultLogger: CoreStoreLogger { functionName: StaticString ) { - #if DEBUG - if condition() { - - return - } - Swift.print("❗ [CoreStore: Assertion Failure] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message())\n") - Swift.fatalError(file: fileName, line: UInt(lineNumber)) - #endif +#if DEBUG + if condition() { + + return + } + Swift::print("❗ [CoreStore: Assertion Failure] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message())\n") + Swift::fatalError(file: fileName, line: UInt(lineNumber)) +#endif } /** @@ -145,7 +145,7 @@ public final class DefaultLogger: CoreStoreLogger { functionName: StaticString ) { - Swift.print("❗ [CoreStore: Fatal Error] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") - Swift.fatalError(file: fileName, line: UInt(lineNumber)) + Swift::print("❗ [CoreStore: Fatal Error] \((String(describing: fileName) as NSString).lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n") + Swift::fatalError(file: fileName, line: UInt(lineNumber)) } } diff --git a/Sources/DiffableDataSource.BaseAdapter.swift b/Sources/DiffableDataSource.BaseAdapter.swift index e3ffb46c..f87b68aa 100644 --- a/Sources/DiffableDataSource.BaseAdapter.swift +++ b/Sources/DiffableDataSource.BaseAdapter.swift @@ -95,7 +95,10 @@ extension DiffableDataSource { - parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from - parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object */ - public init(target: T, dataStack: DataStack) { + public init( + target: T, + dataStack: DataStack + ) { self.target = target self.dataStack = dataStack @@ -106,7 +109,10 @@ extension DiffableDataSource { Clears the target. - parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`. */ - open func purge(animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) { + open func purge( + animatingDifferences: Bool = true, + completion: @escaping @Sendable () -> Void = {} + ) { self.dispatcher.purge( target: self.target, @@ -136,7 +142,11 @@ extension DiffableDataSource { - parameter snapshot: the `ListSnapshot` used to reload the target with. This is typically from the `snapshot` property of a `ListPublisher`. - parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`. */ - open func apply(_ snapshot: ListSnapshot, animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) { + open func apply( + _ snapshot: ListSnapshot, + animatingDifferences: Bool = true, + completion: @escaping @Sendable () -> Void = {} + ) { let diffableSnapshot = snapshot.diffableSnapshot self.dispatcher.apply( @@ -208,6 +218,7 @@ extension DiffableDataSource { public func itemID(for indexPath: IndexPath) -> O.ObjectID? { return self.dispatcher.itemIdentifier(for: indexPath) + .map(O.ObjectID.init(managedObjectID:)) } /** @@ -218,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) } /** @@ -244,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-AppKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift index fb27d34c..6cf9d250 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift @@ -83,6 +83,7 @@ extension DiffableDataSource { - parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object */ @nonobjc + @MainActor public init( collectionView: NSCollectionView, dataStack: DataStack, @@ -102,6 +103,7 @@ extension DiffableDataSource { // MARK: - NSCollectionViewDataSource @objc + @MainActor public dynamic func numberOfSections( in collectionView: NSCollectionView ) -> Int { @@ -110,6 +112,7 @@ extension DiffableDataSource { } @objc + @MainActor public dynamic func collectionView( _ collectionView: NSCollectionView, numberOfItemsInSection section: Int @@ -119,12 +122,13 @@ extension DiffableDataSource { } @objc + @MainActor open dynamic func collectionView( _ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath ) -> NSCollectionViewItem { - 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") } @@ -140,6 +144,7 @@ extension DiffableDataSource { } @objc + @MainActor open dynamic func collectionView( _ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, @@ -181,57 +186,90 @@ extension DiffableDataSource { public var shouldSuspendBatchUpdates: Bool { - return self.base?.window == nil + return MainActor.assumeIsolated { + + return self.base?.window == nil + } } public func deleteSections(at indices: IndexSet, animated: Bool) { - - self.base?.deleteSections(indices) + + MainActor.assumeIsolated { + + self.base?.deleteSections(indices) + } } public func insertSections(at indices: IndexSet, animated: Bool) { - - self.base?.insertSections(indices) + + MainActor.assumeIsolated { + + self.base?.insertSections(indices) + } } public func reloadSections(at indices: IndexSet, animated: Bool) { - - self.base?.reloadSections(indices) + + MainActor.assumeIsolated { + + self.base?.reloadSections(indices) + } } public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) { - - self.base?.moveSection(index, toSection: newIndex) + + MainActor.assumeIsolated { + + self.base?.moveSection(index, toSection: newIndex) + } } public func deleteItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.deleteItems(at: Set(indexPaths)) + + MainActor.assumeIsolated { + + self.base?.deleteItems(at: Set(indexPaths)) + } } public func insertItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.insertItems(at: Set(indexPaths)) + + MainActor.assumeIsolated { + + self.base?.insertItems(at: Set(indexPaths)) + } } public func reloadItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.reloadItems(at: Set(indexPaths)) + + MainActor.assumeIsolated { + + self.base?.reloadItems(at: Set(indexPaths)) + } } public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) { - - self.base?.moveItem(at: indexPath, to: newIndexPath) + + MainActor.assumeIsolated { + + self.base?.moveItem(at: indexPath, to: newIndexPath) + } } - public func performBatchUpdates(updates: () -> Void, animated: Bool, completion: @escaping () -> Void) { - - self.base?.animator().performBatchUpdates(updates, completionHandler: { _ in completion() }) + public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) { + + MainActor.assumeIsolated { + + self.base?.animator().performBatchUpdates(updates, completionHandler: { _ in completion() }) + } } public func reloadData() { - - self.base?.reloadData() + + MainActor.assumeIsolated { + + self.base?.reloadData() + } } } } diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift index 108cdd04..95e8375e 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") } @@ -185,57 +185,90 @@ extension DiffableDataSource { public var shouldSuspendBatchUpdates: Bool { - return self.base?.window == nil + return MainActor.assumeIsolated { + + return self.base?.window == nil + } } public func deleteSections(at indices: IndexSet, animated: Bool) { - - self.base?.deleteSections(indices) + + MainActor.assumeIsolated { + + self.base?.deleteSections(indices) + } } public func insertSections(at indices: IndexSet, animated: Bool) { - - self.base?.insertSections(indices) + + MainActor.assumeIsolated { + + self.base?.insertSections(indices) + } } public func reloadSections(at indices: IndexSet, animated: Bool) { - - self.base?.reloadSections(indices) + + MainActor.assumeIsolated { + + self.base?.reloadSections(indices) + } } public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) { - - self.base?.moveSection(index, toSection: newIndex) + + MainActor.assumeIsolated { + + self.base?.moveSection(index, toSection: newIndex) + } } public func deleteItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.deleteItems(at: indexPaths) + + MainActor.assumeIsolated { + + self.base?.deleteItems(at: indexPaths) + } } public func insertItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.insertItems(at: indexPaths) + + Internals.mainActorImmediate { + + self.base?.insertItems(at: indexPaths) + } } public func reloadItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.reloadItems(at: indexPaths) + + Internals.mainActorImmediate { + + self.base?.reloadItems(at: indexPaths) + } } public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) { - - self.base?.moveItem(at: indexPath, to: newIndexPath) + + Internals.mainActorImmediate { + + self.base?.moveItem(at: indexPath, to: newIndexPath) + } } - public func performBatchUpdates(updates: () -> Void, animated: Bool, completion: @escaping () -> Void) { - - self.base?.performBatchUpdates(updates, completion: { _ in completion() }) + public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) { + + Internals.mainActorImmediate { + + self.base?.performBatchUpdates(updates, completion: { _ in completion() }) + } } public func reloadData() { - - self.base?.reloadData() + + Internals.mainActorImmediate { + + self.base?.reloadData() + } } } } diff --git a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift index 690d8923..5abf3f25 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 (UITableView, IndexPath, O) -> UITableViewCell? + cellProvider: @escaping @MainActor @Sendable (UITableView, IndexPath, O) -> UITableViewCell? ) { self.cellProvider = cellProvider @@ -97,6 +97,7 @@ extension DiffableDataSource { /** The target `UITableView` */ + @MainActor public var tableView: UITableView? { return self.target.base @@ -149,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") } @@ -240,61 +241,94 @@ extension DiffableDataSource { public var shouldSuspendBatchUpdates: Bool { - return self.base?.window == nil + return MainActor.assumeIsolated { + + return self.base?.window == nil + } } public func deleteSections(at indices: IndexSet, animated: Bool) { - - self.base?.deleteSections(indices, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.deleteSections(indices, with: .automatic) + } } public func insertSections(at indices: IndexSet, animated: Bool) { - - self.base?.insertSections(indices, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.insertSections(indices, with: .automatic) + } } public func reloadSections(at indices: IndexSet, animated: Bool) { - - self.base?.reloadSections(indices, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.reloadSections(indices, with: .automatic) + } } public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) { - - self.base?.moveSection(index, toSection: newIndex) + + MainActor.assumeIsolated { + + self.base?.moveSection(index, toSection: newIndex) + } } public func deleteItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.deleteRows(at: indexPaths, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.deleteRows(at: indexPaths, with: .automatic) + } } public func insertItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.insertRows(at: indexPaths, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.insertRows(at: indexPaths, with: .automatic) + } } public func reloadItems(at indexPaths: [IndexPath], animated: Bool) { - - self.base?.reloadRows(at: indexPaths, with: .automatic) + + MainActor.assumeIsolated { + + self.base?.reloadRows(at: indexPaths, with: .automatic) + } } public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) { - - self.base?.moveRow(at: indexPath, to: newIndexPath) + + MainActor.assumeIsolated { + + self.base?.moveRow(at: indexPath, to: newIndexPath) + } } - public func performBatchUpdates(updates: () -> Void, animated: Bool, completion: @escaping () -> Void) { - - guard let base = self.base else { - - return + public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) { + + MainActor.assumeIsolated { + + guard let base = self.base else { + + return + } + base.performBatchUpdates(updates, completion: { _ in completion() }) } - base.performBatchUpdates(updates, completion: { _ in completion() }) } public func reloadData() { - - self.base?.reloadData() + + MainActor.assumeIsolated { + + self.base?.reloadData() + } } } } diff --git a/Sources/DiffableDataSource.Target.swift b/Sources/DiffableDataSource.Target.swift index 84084447..a4717ab4 100644 --- a/Sources/DiffableDataSource.Target.swift +++ b/Sources/DiffableDataSource.Target.swift @@ -46,7 +46,7 @@ extension DiffableDataSource { /** The `DiffableDataSource.Target` protocol allows custom views to consume `ListSnapshot` diffable data similar to how `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` reloads data for their corresponding views. */ -public protocol DiffableDataSourceTarget { +public protocol DiffableDataSourceTarget: Sendable { // MARK: Public @@ -98,7 +98,7 @@ public protocol DiffableDataSourceTarget { /** Animates multiple insert, delete, reload, and move operations as a group. */ - func performBatchUpdates(updates: () -> Void, animated: Bool, completion: @escaping () -> Void) + func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) /** Reloads all sections and items. @@ -110,18 +110,24 @@ extension DiffableDataSource.Target { // MARK: Internal - internal func reload( + internal func reload( using stagedChangeset: Internals.DiffableDataUIDispatcher.StagedChangeset, animated: Bool, interrupt: ((Internals.DiffableDataUIDispatcher.Changeset) -> Bool)? = nil, - setData: (C) -> Void, + setData: @escaping @Sendable (C) -> Void, completion: @escaping () -> Void ) { let group = DispatchGroup() defer { - group.notify(queue: .main, execute: completion) + group.notify( + queue: .main, + execute: { + + completion() + } + ) } if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data { @@ -214,7 +220,10 @@ extension DiffableDataSource.Target { } }, animated: animated, - completion: group.leave + completion: { + + group.leave() + } ) } } diff --git a/Sources/Differentiable.swift b/Sources/Differentiable.swift index 6a5a442c..e3f6c14a 100644 --- a/Sources/Differentiable.swift +++ b/Sources/Differentiable.swift @@ -54,7 +54,7 @@ extension Differentiable where Self: AnyObject { @usableFromInline internal protocol DifferentiableSection: Differentiable { - associatedtype Collection: Swift.Collection where Collection.Element: Differentiable + associatedtype Collection: Swift::Collection where Collection.Element: Differentiable var elements: Collection { get } diff --git a/Sources/DispatchQueue+CoreStore.swift b/Sources/DispatchQueue+CoreStore.swift index ceed4de8..90187d3c 100644 --- a/Sources/DispatchQueue+CoreStore.swift +++ b/Sources/DispatchQueue+CoreStore.swift @@ -99,7 +99,7 @@ extension DispatchQueue { @nonobjc @inline(__always) internal func cs_async( - _ closure: @escaping () -> Void + _ closure: @escaping @Sendable () -> Void ) { self.async { autoreleasepool(invoking: closure) } @@ -107,7 +107,7 @@ extension DispatchQueue { @nonobjc @inline(__always) internal func cs_barrierSync( - _ closure: () throws(any Swift.Error) -> T + _ closure: () throws(any Swift::Error) -> T ) rethrows -> T { return try self.sync(flags: .barrier) { try autoreleasepool(invoking: closure) } @@ -115,7 +115,7 @@ extension DispatchQueue { @nonobjc @inline(__always) internal func cs_barrierAsync( - _ closure: @escaping () -> Void + _ closure: @escaping @Sendable () -> Void ) { self.async(flags: .barrier) { autoreleasepool(invoking: closure) } diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index d0e42a22..7293326a 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 = PersistentID + /** 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,12 +79,14 @@ 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 { @@ -99,6 +107,7 @@ extension NSManagedObject: DynamicObject { // MARK: DynamicObject + @_spi(Internals) public class func cs_forceCreate( entityDescription: NSEntityDescription, into context: NSManagedObjectContext, @@ -112,13 +121,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,7 +141,10 @@ extension NSManagedObject: DynamicObject { return dictionary } - public class func cs_fromRaw(object: NSManagedObject) -> Self { + @_spi(Internals) + public class func cs_fromRaw( + object: NSManagedObject + ) -> Self { #if swift(>=5.9) return unsafeDowncast(object, to: self) @@ -143,6 +156,7 @@ extension NSManagedObject: DynamicObject { #endif } + @_spi(Internals) public static func cs_matches( object: NSManagedObject ) -> Bool { @@ -150,12 +164,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 +184,7 @@ extension CoreStoreObject { // MARK: DynamicObject + @_spi(Internals) public class func cs_forceCreate( entityDescription: NSEntityDescription, into context: NSManagedObjectContext, @@ -182,9 +199,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 +258,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 +271,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,7 +310,10 @@ extension CoreStoreObject { return values } - public class func cs_fromRaw(object: NSManagedObject) -> Self { + @_spi(Internals) + public class func cs_fromRaw( + object: NSManagedObject + ) -> Self { if let coreStoreObject = object.coreStoreObject { @@ -310,6 +331,7 @@ extension CoreStoreObject { return coreStoreObject } + @_spi(Internals) public static func cs_matches( object: NSManagedObject ) -> Bool { @@ -321,12 +343,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/DynamicSchema.swift b/Sources/DynamicSchema.swift index ab7cfb1c..60c7dd54 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 0982efd2..05dc5b6d 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 6825db78..013e2050 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/Field.Relationship.swift b/Sources/Field.Relationship.swift index 7e985d7f..94953407 100644 --- a/Sources/Field.Relationship.swift +++ b/Sources/Field.Relationship.swift @@ -235,7 +235,7 @@ extension FieldContainer { self.keyPath = keyPath self.entityDescriptionValues = { - let range = (Swift.max(0, minCount) ... maxCount) + let range = (Swift::max(0, minCount) ... maxCount) return ( isToMany: isToMany, isOrdered: isOrdered, diff --git a/Sources/FieldOptionalType.swift b/Sources/FieldOptionalType.swift index f9f07b65..adfed81c 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 4a027ec6..06f59774 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 cc8e38f4..57b101ce 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 f00fb2ea..43fcfa58 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 6aa347f0..bef6f230 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/ImportableObject.swift b/Sources/ImportableObject.swift index a34916ca..c8756a57 100644 --- a/Sources/ImportableObject.swift +++ b/Sources/ImportableObject.swift @@ -57,7 +57,7 @@ public protocol ImportableObject: DynamicObject { /** The data type for the import source. This is most commonly an json type, `NSDictionary`, or another external source such as `NSUserDefaults`. */ - associatedtype ImportSource + associatedtype ImportSource: Sendable /** Return `true` if an object should be created from `source`. Return `false` to ignore and skip `source`. The default implementation returns `true`. @@ -80,7 +80,7 @@ public protocol ImportableObject: DynamicObject { func didInsert( from source: ImportSource, in transaction: BaseDataTransaction - ) throws(any Swift.Error) + ) throws(any Swift::Error) } diff --git a/Sources/ImportableUniqueObject.swift b/Sources/ImportableUniqueObject.swift index 1ebd5cd4..ebe5f913 100644 --- a/Sources/ImportableUniqueObject.swift +++ b/Sources/ImportableUniqueObject.swift @@ -105,7 +105,7 @@ public protocol ImportableUniqueObject: ImportableObject, Hashable { static func uniqueID( from source: ImportSource, in transaction: BaseDataTransaction - ) throws(any Swift.Error) -> UniqueIDType? + ) throws(any Swift::Error) -> UniqueIDType? /** Implements the actual importing of data from `source`. This method is called just after the object is created and assigned its unique ID as returned from `uniqueID(from:in:)`. Implementers should pull values from `source` and assign them to the receiver's attributes. Note that throwing from this method will cause subsequent imports that are part of the same `importUniqueObjects(:sourceArray:)` call to be cancelled. The default implementation simply calls `update(from:in:)`. @@ -116,7 +116,7 @@ public protocol ImportableUniqueObject: ImportableObject, Hashable { func didInsert( from source: ImportSource, in transaction: BaseDataTransaction - ) throws(any Swift.Error) + ) throws(any Swift::Error) /** Implements the actual importing of data from `source`. This method is called just after the existing object is fetched using its unique ID. Implementers should pull values from `source` and assign them to the receiver's attributes. Note that throwing from this method will cause subsequent imports that are part of the same `importUniqueObjects(:sourceArray:)` call to be cancelled. @@ -127,7 +127,7 @@ public protocol ImportableUniqueObject: ImportableObject, Hashable { func update( from source: ImportSource, in transaction: BaseDataTransaction - ) throws(any Swift.Error) + ) throws(any Swift::Error) } @@ -180,7 +180,7 @@ extension ImportableUniqueObject { public func didInsert( from source: Self.ImportSource, in transaction: BaseDataTransaction - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { try self.update(from: source, in: transaction) } diff --git a/Sources/InMemoryStore.swift b/Sources/InMemoryStore.swift index 3b3a5f20..7297d1dd 100644 --- a/Sources/InMemoryStore.swift +++ b/Sources/InMemoryStore.swift @@ -66,11 +66,15 @@ public final class InMemoryStore: StorageInterface { /** The options dictionary for the `NSPersistentStore`. For `InMemoryStore`s, this is always set to `nil`. */ - public let storeOptions: [AnyHashable: Any]? = nil + public var storeOptions: [AnyHashable: Any]? { + + return nil + } /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didAddToDataStack(_ dataStack: DataStack) { self.dataStack = dataStack @@ -79,6 +83,7 @@ public final class InMemoryStore: StorageInterface { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { self.dataStack = nil @@ -87,5 +92,5 @@ public final class InMemoryStore: StorageInterface { // MARK: Private - private weak var dataStack: DataStack? + private nonisolated(unsafe) weak var dataStack: DataStack? } diff --git a/Sources/Internals.AnyFieldCoder.swift b/Sources/Internals.AnyFieldCoder.swift index c3655148..3659c9c9 100644 --- a/Sources/Internals.AnyFieldCoder.swift +++ b/Sources/Internals.AnyFieldCoder.swift @@ -95,7 +95,7 @@ extension Internals { // MARK: FilePrivate - fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:] + fileprivate static nonisolated(unsafe) var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:] // MARK: - TransformableDefaultValueCodingBox diff --git a/Sources/Internals.Closure.swift b/Sources/Internals.Closure.swift index d904852c..628801f7 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.CoreStoreFetchRequest.swift b/Sources/Internals.CoreStoreFetchRequest.swift index d0e8f553..d8ea9700 100644 --- a/Sources/Internals.CoreStoreFetchRequest.swift +++ b/Sources/Internals.CoreStoreFetchRequest.swift @@ -36,7 +36,7 @@ extension Internals { // Bugfix for NSFetchRequest messing up memory management for `affectedStores` // http://stackoverflow.com/questions/14396375/nsfetchedresultscontroller-crashes-in-ios-6-if-affectedstores-is-specified - internal final class CoreStoreFetchRequest: NSFetchRequest { + internal final class CoreStoreFetchRequest: NSFetchRequest, @unchecked Sendable { @nonobjc internal func safeAffectedStores() -> [NSPersistentStore]? { diff --git a/Sources/Internals.CoreStoreFetchedResultsController.swift b/Sources/Internals.CoreStoreFetchedResultsController.swift index c0d89538..7e16ebbf 100644 --- a/Sources/Internals.CoreStoreFetchedResultsController.swift +++ b/Sources/Internals.CoreStoreFetchedResultsController.swift @@ -33,7 +33,7 @@ extension Internals { // MARK: - CoreStoreFetchedResultsController - internal final class CoreStoreFetchedResultsController: NSFetchedResultsController { + internal final class CoreStoreFetchedResultsController: NSFetchedResultsController, @unchecked Sendable { // MARK: Internal @@ -77,7 +77,7 @@ extension Internals { } @nonobjc - internal func performFetchFromSpecifiedStores() throws(any Swift.Error) { + internal func performFetchFromSpecifiedStores() throws(any Swift::Error) { try self.reapplyAffectedStores(self.typedFetchRequest, self.managedObjectContext) try self.performFetch() @@ -106,6 +106,6 @@ extension Internals { private let reapplyAffectedStores: ( _ fetchRequest: Internals.CoreStoreFetchRequest, _ context: NSManagedObjectContext - ) throws(any Swift.Error) -> Void + ) throws(any Swift::Error) -> Void } } diff --git a/Sources/Internals.DiffableDataSourceSnapshot.swift b/Sources/Internals.DiffableDataSourceSnapshot.swift index b7940ae4..5d3a0fb8 100644 --- a/Sources/Internals.DiffableDataSourceSnapshot.swift +++ b/Sources/Internals.DiffableDataSourceSnapshot.swift @@ -43,7 +43,7 @@ extension Internals { // MARK: - DiffableDataSourceSnapshot // Implementation based on https://github.com/ra1028/DiffableDataSources - internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol { + internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol, @unchecked Sendable { // MARK: Internal @@ -57,7 +57,7 @@ extension Internals { self.structure = .init( sections: sections, sectionIndexTransformer: sectionIndexTransformer, - fetchOffset: Swift.max(0, fetchOffset), + fetchOffset: Swift::max(0, fetchOffset), fetchLimit: (fetchLimit > 0) ? fetchLimit : nil ) } diff --git a/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift b/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift index 213cde6e..f468fbdb 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.Changeset.swift @@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - ChangeSet // Implementation based on https://github.com/ra1028/DifferenceKit - internal struct Changeset: Equatable where C: Equatable { + internal struct Changeset: Equatable, Sendable + where C: Equatable { var data: C var sectionDeleted: [Int] diff --git a/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift b/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift index dc487168..9db37116 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.DiffResult.swift @@ -36,7 +36,7 @@ extension Internals.DiffableDataUIDispatcher { // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline - internal struct DiffResult { + internal struct DiffResult: Sendable { @usableFromInline internal let deleted: [Index] @@ -213,7 +213,7 @@ extension Internals.DiffableDataUIDispatcher { // Implementation based on https://github.com/ra1028/DifferenceKit @usableFromInline - internal struct Trace { + internal struct Trace: Sendable { @usableFromInline internal var reference: I? diff --git a/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift b/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift index ce86cf49..b5702b2c 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.StagedChangeset.swift @@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - StagedChangeset // Implementation based on https://github.com/ra1028/DifferenceKit - internal struct StagedChangeset: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection where C: Equatable { + internal struct StagedChangeset: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection, Sendable + where C: Equatable { @usableFromInline var changesets: ContiguousArray> @@ -117,7 +118,8 @@ extension Internals.DiffableDataUIDispatcher { // MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable -extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable { +extension Internals.DiffableDataUIDispatcher.StagedChangeset +where C: RangeReplaceableCollection, C.Element: Differentiable { @inlinable internal init(source: C, target: C) { @@ -225,7 +227,8 @@ extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeRepla // MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection -extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection { +extension Internals.DiffableDataUIDispatcher.StagedChangeset +where C: RangeReplaceableCollection & Sendable, C.Element: DifferentiableSection & Sendable { @inlinable internal init(source: C, target: C) { diff --git a/Sources/Internals.DiffableDataUIDispatcher.swift b/Sources/Internals.DiffableDataUIDispatcher.swift index f41ad166..b6d783cb 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.swift @@ -41,7 +41,7 @@ extension Internals { // Implementation based on https://github.com/ra1028/DiffableDataSources @usableFromInline - internal final class DiffableDataUIDispatcher { + internal final class DiffableDataUIDispatcher: @unchecked Sendable { // MARK: Internal @@ -55,10 +55,10 @@ extension Internals { func purge( target: Target?, animatingDifferences: Bool, - performUpdates: @escaping ( + performUpdates: @escaping @Sendable ( Target, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, - @escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void + @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void ) -> Void ) { @@ -74,14 +74,14 @@ extension Internals { _ snapshot: DiffableDataSourceSnapshot, target: Target?, animatingDifferences: Bool, - performUpdates: @escaping ( + performUpdates: @escaping @Sendable ( Target, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, - @escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void + @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void ) -> Void ) { - self.dispatcher.dispatch { [weak self] in + self.dispatcher.dispatch { @MainActor [weak self] in guard let self = self else { @@ -97,13 +97,17 @@ extension Internals { return } - let performDiffingUpdates: () -> Void = { + let performDiffingUpdates: @MainActor @Sendable () -> Void = { let changeset = StagedChangeset(source: self.sections, target: newSections) - performUpdates(target, changeset) { sections in - - self.sections = sections - } + performUpdates( + target, + changeset, + { sections in + + self.sections = sections + } + ) } #if os(watchOS) || !canImport(QuartzCore) @@ -142,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 { @@ -156,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() { @@ -212,7 +216,7 @@ extension Internals { // MARK: - ElementPath @usableFromInline - internal struct ElementPath: Hashable { + internal struct ElementPath: Hashable, Sendable { @usableFromInline var element: Int @@ -231,18 +235,21 @@ extension Internals { // MARK: - MainThreadSerialDispatcher - fileprivate final class MainThreadSerialDispatcher { + fileprivate final class MainThreadSerialDispatcher: Sendable { // MARK: FilePrivate fileprivate init() {} - fileprivate func dispatch(_ action: @escaping () -> Void) { + fileprivate func dispatch(_ action: @escaping @MainActor @Sendable () -> Void) { let count = self.executingCount.incrementAndGet() if Thread.isMainThread && count == 1 { - action() + MainActor.assumeIsolated { + + action() + } self.executingCount.decrement() } else { @@ -267,36 +274,31 @@ extension Internals { // MARK: - AtomicInt - fileprivate class AtomicInt { + fileprivate final class AtomicInt: Sendable { // MARK: FilePrivate fileprivate func incrementAndGet() -> Int { - self.lock.wait() - defer { + return self.value.withLock { - self.lock.signal() + $0 += 1 + return $0 } - self.value += 1 - return self.value } fileprivate func decrement() { - self.lock.wait() - defer { + self.value.withLock { - self.lock.signal() + $0 -= 1 } - self.value -= 1 } // MARK: Private - private let lock = DispatchSemaphore(value: 1) - private var value = 0 + private let value: Internals.Mutex = .init(0) } } diff --git a/Sources/Internals.EntityIdentifier.swift b/Sources/Internals.EntityIdentifier.swift index 8129efe8..07304338 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.FetchedResultsControllerDelegate.swift b/Sources/Internals.FetchedResultsControllerDelegate.swift index d7401bb9..f63fd11e 100644 --- a/Sources/Internals.FetchedResultsControllerDelegate.swift +++ b/Sources/Internals.FetchedResultsControllerDelegate.swift @@ -64,7 +64,7 @@ extension Internals { // MARK: - FetchedResultsControllerDelegate - internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate { + internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate, @unchecked Sendable { // MARK: Internal diff --git a/Sources/Internals.Mutext.swift b/Sources/Internals.Mutext.swift new file mode 100644 index 00000000..c4b59fa1 --- /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: Swift::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: Swift::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 42a42474..72cac2f9 100644 --- a/Sources/Internals.NotificationObserver.swift +++ b/Sources/Internals.NotificationObserver.swift @@ -42,7 +42,7 @@ extension Internals { notificationName: Notification.Name, object: Any?, queue: OperationQueue? = nil, - closure: @escaping (_ note: Notification) -> Void + closure: @escaping @Sendable (_ note: Notification) -> Void ) { self.observer = NotificationCenter.default.addObserver( @@ -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 ebdc3dee..b70e7143 100644 --- a/Sources/Internals.SharedNotificationObserver.swift +++ b/Sources/Internals.SharedNotificationObserver.swift @@ -31,7 +31,7 @@ extension Internals { // MARK: - SharedNotificationObserver - internal final class SharedNotificationObserver { + internal final class SharedNotificationObserver: @unchecked Sendable { // MARK: Internal @@ -39,7 +39,7 @@ extension Internals { notificationName: Notification.Name, object: Any?, queue: OperationQueue? = nil, - sharedValue: @escaping (_ note: Notification) -> T + sharedValue: @escaping @Sendable (_ note: Notification) -> T ) { self.observer = NotificationCenter.default.addObserver( @@ -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 501f5e91..882279b6 100644 --- a/Sources/Internals.swift +++ b/Sources/Internals.swift @@ -121,6 +121,7 @@ internal enum Internals { return closure() } + @inline(__always) internal static func autoreleasepool( @@ -129,39 +130,77 @@ internal enum Internals { return ObjectiveC.autoreleasepool(invoking: closure) } - + @inline(__always) internal static func autoreleasepool( - _ closure: () throws(any Swift.Error) -> T - ) throws(any Swift.Error) -> T { + _ closure: () throws(any Swift::Error) -> T + ) throws(any Swift::Error) -> T { return try ObjectiveC.autoreleasepool(invoking: closure) } - + @inline(__always) - internal static func autoreleasepool( - _ closure: () throws(CoreStoreError) -> T - ) throws(CoreStoreError) -> T { - - do { - - return try ObjectiveC.autoreleasepool(invoking: closure) - } - catch { + internal static func withCheckedThrowingContinuation( + function: String = #function, + _ body: (CheckedContinuation) -> Void + ) async throws(any Swift::Error) -> sending T { - throw CoreStoreError(error) - } + return try await _Concurrency.withCheckedThrowingContinuation( + function: function, + body + ) } - + + +#if compiler(>=27) + @inline(__always) - internal static func withCheckedThrowingContinuation( + 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, - _ body: (CheckedContinuation) -> Void - ) async throws(any Swift.Error) -> T { - + _ body: (CheckedContinuation) -> Void + ) async throws(E) -> sending T { + return try await _Concurrency.withCheckedThrowingContinuation( function: function, body ) } + +#endif + + + @inline(__always) + internal static func mainActorImmediate( + _ body: @escaping @MainActor @Sendable () -> Void + ) { + + if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) { + + Task.immediate { @MainActor in + + body() + } + } + else if Thread.isMainThread { + + MainActor.assumeIsolated { + body() + } + } + else { + + Task { @MainActor in + + body() + } + } + } } diff --git a/Sources/Into.swift b/Sources/Into.swift index 10b675c4..341c6d7e 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 { +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 6f670098..16708a17 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 efd0e8af..8b4c36fd 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 { +public final class ListMonitor: Hashable, Sendable { // MARK: Public (Accessors) @@ -386,8 +386,10 @@ public final class ListMonitor: Hashable { - 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 { 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 { - 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 { 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 { 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 { 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 { 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 { 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 { - 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 { 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 { 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 { 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 { 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 { 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 { 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 { monitor, didDeleteSection: sectionInfo, fromSectionIndex: fromIndex, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -624,6 +630,7 @@ public final class ListMonitor: Hashable { - 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 { /** 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,9 +653,10 @@ public final class ListMonitor: Hashable { - 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? = nil + sourceIdentifier: (any Sendable)? = nil ) { self.refetch( @@ -666,9 +674,10 @@ public final class ListMonitor: Hashable { - 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? = nil + sourceIdentifier: (any Sendable)? = nil ) { self.refetch( @@ -751,7 +760,7 @@ public final class ListMonitor: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: @escaping (ListMonitor) -> Void + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void ) { self.init( @@ -786,7 +795,7 @@ public final class ListMonitor: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: @escaping (ListMonitor) -> Void + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void ) { self.init( @@ -803,7 +812,7 @@ public final class ListMonitor: Hashable { _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping (_ monitor: ListMonitor) -> Void + callback: @escaping @Sendable (_ monitor: ListMonitor) -> Void ) { Internals.setAssociatedRetainedObject( @@ -828,12 +837,13 @@ public final class ListMonitor: Hashable { _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping ( + callback: @escaping @Sendable ( _ monitor: ListMonitor, _ object: O, _ indexPath: IndexPath?, _ newIndexPath: IndexPath? - ) -> Void) { + ) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -864,7 +874,7 @@ public final class ListMonitor: Hashable { _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping ( + callback: @escaping @Sendable ( _ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ sectionIndex: Int @@ -892,36 +902,39 @@ public final class ListMonitor: Hashable { ) } + @MainActor internal func registerObserver( _ observer: U, - willChange: @escaping ( + willChange: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor ) -> Void, - didChange: @escaping ( + didChange: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor ) -> Void, - willRefetch: @escaping ( + willRefetch: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor ) -> Void, - didRefetch: @escaping ( + didRefetch: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor - ) -> Void) { + ) -> Void + ) { Internals.assert( Thread.isMainThread, "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." ) + nonisolated(unsafe) weak let weakObserver = observer as Optional self.registerChangeNotification( &self.willChangeListKey, name: Notification.Name.listMonitorWillChangeList, toObserver: observer, - callback: { [weak observer] (monitor) -> Void in + callback: { (monitor) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -932,9 +945,9 @@ public final class ListMonitor: Hashable { &self.didChangeListKey, name: Notification.Name.listMonitorDidChangeList, toObserver: observer, - callback: { [weak observer] (monitor) -> Void in + callback: { (monitor) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -945,9 +958,9 @@ public final class ListMonitor: Hashable { &self.willRefetchListKey, name: Notification.Name.listMonitorWillRefetchList, toObserver: observer, - callback: { [weak observer] (monitor) -> Void in + callback: { (monitor) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -958,9 +971,9 @@ public final class ListMonitor: Hashable { &self.didRefetchListKey, name: Notification.Name.listMonitorDidRefetchList, toObserver: observer, - callback: { [weak observer] (monitor) -> Void in + callback: { (monitor) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -969,46 +982,48 @@ public final class ListMonitor: Hashable { ) } + @MainActor internal func registerObserver( _ observer: U, - didInsertObject: @escaping ( + didInsertObject: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ object: O, _ toIndexPath: IndexPath ) -> Void, - didDeleteObject: @escaping ( + didDeleteObject: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ object: O, _ fromIndexPath: IndexPath ) -> Void, - didUpdateObject: @escaping ( + didUpdateObject: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ object: O, _ atIndexPath: IndexPath ) -> Void, - didMoveObject: @escaping ( + didMoveObject: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ object: O, _ fromIndexPath: IndexPath, _ toIndexPath: IndexPath - ) -> Void) { + ) -> Void + ) { Internals.assert( Thread.isMainThread, "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." ) - + nonisolated(unsafe) weak let weakObserver = observer as Optional self.registerObjectNotification( &self.didInsertObjectKey, name: Notification.Name.listMonitorDidInsertObject, toObserver: observer, - callback: { [weak observer] (monitor, object, indexPath, newIndexPath) -> Void in + callback: { (monitor, object, indexPath, newIndexPath) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1019,9 +1034,9 @@ public final class ListMonitor: Hashable { &self.didDeleteObjectKey, name: Notification.Name.listMonitorDidDeleteObject, toObserver: observer, - callback: { [weak observer] (monitor, object, indexPath, newIndexPath) -> Void in + callback: { (monitor, object, indexPath, newIndexPath) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1032,9 +1047,9 @@ public final class ListMonitor: Hashable { &self.didUpdateObjectKey, name: Notification.Name.listMonitorDidUpdateObject, toObserver: observer, - callback: { [weak observer] (monitor, object, indexPath, newIndexPath) -> Void in + callback: { (monitor, object, indexPath, newIndexPath) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1045,9 +1060,9 @@ public final class ListMonitor: Hashable { &self.didMoveObjectKey, name: Notification.Name.listMonitorDidMoveObject, toObserver: observer, - callback: { [weak observer] (monitor, object, indexPath, newIndexPath) -> Void in + callback: { (monitor, object, indexPath, newIndexPath) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1056,33 +1071,35 @@ public final class ListMonitor: Hashable { ) } + @MainActor internal func registerObserver( _ observer: U, - didInsertSection: @escaping ( + didInsertSection: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ toIndex: Int ) -> Void, - didDeleteSection: @escaping ( + didDeleteSection: @escaping @Sendable ( _ observer: U, _ monitor: ListMonitor, _ sectionInfo: NSFetchedResultsSectionInfo, _ fromIndex: Int - ) -> Void) { + ) -> Void + ) { Internals.assert( Thread.isMainThread, "Attempted to add an observer of type \(Internals.typeName(observer)) outside the main thread." ) - + nonisolated(unsafe) weak let weakObserver = observer as Optional self.registerSectionNotification( &self.didInsertSectionKey, name: Notification.Name.listMonitorDidInsertSection, toObserver: observer, - callback: { [weak observer] (monitor, sectionInfo, sectionIndex) -> Void in + callback: { (monitor, sectionInfo, sectionIndex) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1093,9 +1110,9 @@ public final class ListMonitor: Hashable { &self.didDeleteSectionKey, name: Notification.Name.listMonitorDidDeleteSection, toObserver: observer, - callback: { [weak observer] (monitor, sectionInfo, sectionIndex) -> Void in + callback: { (monitor, sectionInfo, sectionIndex) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -1104,6 +1121,7 @@ public final class ListMonitor: Hashable { ) } + @MainActor internal func unregisterObserver(_ observer: AnyObject) { Internals.assert( @@ -1125,9 +1143,10 @@ public final class ListMonitor: Hashable { Internals.setAssociatedRetainedObject(nilValue, forKey: &self.didDeleteSectionKey, inObject: observer) } + @MainActor internal func refetch( _ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { Internals.assert( @@ -1154,7 +1173,7 @@ public final class ListMonitor: Hashable { } let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( - context: self.fetchedResultsController.managedObjectContext, + context: self.managedObjectContext, from: self.from, sectionBy: self.sectionBy, applyFetchClauses: self.applyFetchClauses @@ -1216,32 +1235,34 @@ public final class ListMonitor: Hashable { // 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 let transactionQueue: DispatchQueue - private var willChangeListKey: Void? - private var didChangeListKey: Void? - private var willRefetchListKey: Void? - private var didRefetchListKey: Void? + private nonisolated(unsafe) var applyFetchClauses: (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void - private var didInsertObjectKey: Void? - private var didDeleteObjectKey: Void? - private var didUpdateObjectKey: Void? - private var didMoveObjectKey: 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 var didInsertSectionKey: Void? - private var didDeleteSectionKey: 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 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 nonisolated(unsafe) var didInsertSectionKey: Void? + private nonisolated(unsafe) var didDeleteSectionKey: Void? - private var isPersistentStoreChanging: Bool = false { + 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 +1323,21 @@ public final class ListMonitor: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: ((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 +1349,7 @@ public final class ListMonitor: Hashable { } self.transactionQueue = transactionQueue self.applyFetchClauses = applyFetchClauses - self.fetchedResultsControllerDelegate.handler = self + fetchedResultsControllerDelegate.handler = self guard let coordinator = context.parentStack?.coordinator else { @@ -1335,8 +1359,7 @@ public final class ListMonitor: Hashable { 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 +1380,7 @@ public final class ListMonitor: Hashable { 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 +1411,10 @@ public final class ListMonitor: Hashable { try! self.fetchedResultsController.performFetchFromSpecifiedStores() self.taskGroup.notify(queue: .main) { - createAsynchronously(self) + MainActor.assumeIsolated { + + createAsynchronously(self) + } } } } diff --git a/Sources/ListObserver.swift b/Sources/ListObserver.swift index 30940b1e..cb9294a0 100644 --- a/Sources/ListObserver.swift +++ b/Sources/ListObserver.swift @@ -39,7 +39,7 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ListObserver: AnyObject { +public protocol ListObserver: AnyObject, SendableMetatype { /** The `NSManagedObject` type for the observed list diff --git a/Sources/ListPublisher.SnapshotPublisher.swift b/Sources/ListPublisher.SnapshotPublisher.swift index 65e904f0..08f71a11 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,8 @@ extension ListPublisher { // MARK: - ListSnapshotSubscription - fileprivate final class ListSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { + fileprivate final class ListSnapshotSubscription: Subscription + where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -114,21 +115,26 @@ extension ListPublisher { return } - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in - - guard - let self = self, - let subscriber = self.subscriber - else { + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { + + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in - return + guard + let self = weakSelf, + let subscriber = self.subscriber + else { + + return + } + _ = subscriber.receive(publisher.snapshot) } - _ = subscriber.receive(publisher.snapshot) - } - ) + ) + } } @@ -138,16 +144,10 @@ extension ListPublisher { self.subscriber = nil - if Thread.isMainThread { - - self.publisher.removeObserver(self) - } - else { + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - DispatchQueue.main.async { - - self.publisher.removeObserver(self) - } + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index ec9d435f..9afcd366 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -94,10 +94,11 @@ public final class ListPublisher: Hashable { - parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`. - parameter callback: the closure to execute when changes occur */ + @MainActor public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ListPublisher) -> Void + _ callback: @escaping @Sendable (ListPublisher) -> Void ) { Internals.assert( @@ -128,11 +129,12 @@ public final class ListPublisher: Hashable { - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. - parameter callback: the closure to execute when changes occur */ + @MainActor public func addObserver( _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ listPublisher: ListPublisher, _ sourceIdentifier: Any? ) -> Void @@ -159,6 +161,7 @@ public final class ListPublisher: Hashable { - parameter observer: the object whose notifications will be unregistered */ + @MainActor public func removeObserver(_ observer: T) { Internals.assert( @@ -185,8 +188,8 @@ public final class ListPublisher: Hashable { */ public func refetch( _ clauseChain: B, - sourceIdentifier: Any? = nil - ) throws(any Swift.Error) where B.ObjectType == O { + sourceIdentifier: (any Sendable)? = nil + ) throws(any Swift::Error) where B.ObjectType == O { try self.refetch( from: clauseChain.from, @@ -214,8 +217,8 @@ public final class ListPublisher: Hashable { */ public func refetch( _ clauseChain: B, - sourceIdentifier: Any? = nil - ) throws(any Swift.Error) where B.ObjectType == O { + sourceIdentifier: (any Sendable)? = nil + ) throws(any Swift::Error) where B.ObjectType == O { try self.refetch( from: clauseChain.from, @@ -231,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 @@ -342,8 +346,8 @@ public final class ListPublisher: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - sourceIdentifier: Any? - ) throws(any Swift.Error) { + sourceIdentifier: (any Sendable)? + ) throws(any Swift::Error) { let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( context: self.fetchedResultsController.managedObjectContext, diff --git a/Sources/ListSnapshot.SectionInfo.swift b/Sources/ListSnapshot.SectionInfo.swift index fa12f9aa..a5086164 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 865d2ea7..455365b0 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 2c82d6b2..050064a7 100644 --- a/Sources/ListState.swift +++ b/Sources/ListState.swift @@ -23,9 +23,9 @@ // SOFTWARE. // -#if canImport(Combine) && canImport(SwiftUI) +#if canImport(Observation) && canImport(SwiftUI) -import Combine +import Observation import SwiftUI @@ -35,7 +35,7 @@ import SwiftUI A property wrapper type that can read `ListPublisher` changes. */ @propertyWrapper -public struct ListState: DynamicProperty { +public struct ListState: @MainActor DynamicProperty { // MARK: Public @@ -65,11 +65,13 @@ public struct ListState: DynamicProperty { - parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for */ + @MainActor public init( - _ listPublisher: ListPublisher + _ listPublisher: ListPublisher ) { - self.observer = .init(listPublisher: listPublisher) + self.sourceListPublisher = listPublisher + self._observer = .init(wrappedValue: .init(listPublisher: listPublisher)) } /** @@ -98,10 +100,11 @@ public struct ListState: DynamicProperty { - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses */ + @MainActor public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -139,10 +142,11 @@ public struct ListState: DynamicProperty { - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses */ + @MainActor public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -174,8 +178,9 @@ public struct ListState: DynamicProperty { - 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 init( - _ from: From, + _ from: From, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -212,8 +217,9 @@ public struct ListState: DynamicProperty { - 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 init( - _ from: From, + _ from: From, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -256,9 +262,10 @@ public struct ListState: DynamicProperty { - 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 init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -303,9 +310,10 @@ public struct ListState: DynamicProperty { - 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 init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -316,12 +324,14 @@ public struct ListState: DynamicProperty { // MARK: @propertyWrapper - public var wrappedValue: ListSnapshot { + @MainActor + public var wrappedValue: ListSnapshot { return self.observer.items } - public var projectedValue: ListPublisher { + @MainActor + public var projectedValue: ListPublisher { return self.observer.listPublisher } @@ -329,33 +339,78 @@ public struct ListState: DynamicProperty { // MARK: DynamicProperty + @MainActor public mutating func update() { self._observer.update() + self.observer.rebind(to: self.sourceListPublisher) } // MARK: Private - @ObservedObject + @State private var observer: Observer + private let sourceListPublisher: ListPublisher + // MARK: - Observer - private final class Observer: ObservableObject { + @MainActor + private final class Observer: Observation.Observable { - @Published - var items: ListSnapshot + private(set) var listPublisher: ListPublisher - let listPublisher: ListPublisher + nonisolated var items: ListSnapshot { + + get { + + self.registrar.access(self, keyPath: \.items) + return self.current.withLock({ $0 }) + } + set { + + self.registrar.withMutation(of: self, keyPath: \.items) { + + self.current.withLock({ $0 = newValue }) + } + } + } - init(listPublisher: ListPublisher) { + init(listPublisher: ListPublisher) { + self.listPublisher = listPublisher + self.current = .init(listPublisher.snapshot) + self.attachObserver() + } + + isolated deinit { + + self.listPublisher.removeObserver(self) + } + + func rebind(to listPublisher: ListPublisher) { + + guard self.listPublisher !== listPublisher else { + + return + } + self.listPublisher.removeObserver(self) self.listPublisher = listPublisher self.items = listPublisher.snapshot + self.attachObserver() + } + + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex> + + private func attachObserver() { - listPublisher.addObserver(self) { [weak self] (listPublisher) in + self.listPublisher.addObserver(self) { [weak self] listPublisher in guard let self = self else { @@ -364,11 +419,6 @@ public struct ListState: DynamicProperty { self.items = listPublisher.snapshot } } - - deinit { - - self.listPublisher.removeObserver(self) - } } } diff --git a/Sources/MigrationChain.swift b/Sources/MigrationChain.swift index 5301b70f..66f223d9 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/MigrationResult.swift b/Sources/MigrationResult.swift index 9943d9cc..b95b7489 100644 --- a/Sources/MigrationResult.swift +++ b/Sources/MigrationResult.swift @@ -43,4 +43,4 @@ import Foundation } ``` */ -public typealias MigrationResult = Swift.Result<[MigrationType], CoreStoreError> +public typealias MigrationResult = Swift::Result<[MigrationType], CoreStoreError> diff --git a/Sources/MigrationType.swift b/Sources/MigrationType.swift index 5cd92da3..23811fba 100644 --- a/Sources/MigrationType.swift +++ b/Sources/MigrationType.swift @@ -31,7 +31,7 @@ import Foundation /** The `MigrationType` specifies the type of migration required for a store. */ -public enum MigrationType: Hashable { +public enum MigrationType: Hashable, Sendable { /** Indicates that the persistent store matches the latest model version and no migration is needed diff --git a/Sources/NSManagedObject+Convenience.swift b/Sources/NSManagedObject+Convenience.swift index 3fce1a7f..bc934d96 100644 --- a/Sources/NSManagedObject+Convenience.swift +++ b/Sources/NSManagedObject+Convenience.swift @@ -106,7 +106,7 @@ extension NSManagedObject { @nonobjc @inline(__always) public func getValue( forKvcKey kvcKey: KeyPathString, - didGetValue: (Any?) throws(any Swift.Error) -> T + didGetValue: (Any?) throws(any Swift::Error) -> T ) rethrows -> T { self.willAccessValue(forKey: kvcKey) @@ -128,8 +128,8 @@ extension NSManagedObject { @nonobjc @inline(__always) public func getValue( forKvcKey kvcKey: KeyPathString, - willGetValue: () throws(any Swift.Error) -> Void, - didGetValue: (Any?) throws(any Swift.Error) -> T + willGetValue: () throws(any Swift::Error) -> Void, + didGetValue: (Any?) throws(any Swift::Error) -> T ) rethrows -> T { self.willAccessValue(forKey: kvcKey) @@ -196,7 +196,7 @@ extension NSManagedObject { public func setValue( _ value: T, forKvcKey KVCKey: KeyPathString, - willSetValue: (T) throws(any Swift.Error) -> Any?, + willSetValue: (T) throws(any Swift::Error) -> Any?, didSetValue: (Any?) -> Void = { _ in } ) rethrows { diff --git a/Sources/NSManagedObject+DynamicModel.swift b/Sources/NSManagedObject+DynamicModel.swift index 1ef45254..10798598 100644 --- a/Sources/NSManagedObject+DynamicModel.swift +++ b/Sources/NSManagedObject+DynamicModel.swift @@ -56,6 +56,6 @@ extension NSManagedObject { private struct PropertyKeys { - static var coreStoreObject: Void? + static nonisolated(unsafe) var coreStoreObject: Void? } } diff --git a/Sources/NSManagedObjectContext+CoreStore.swift b/Sources/NSManagedObjectContext+CoreStore.swift index a23572b6..2ee0fd27 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 } } @@ -192,8 +192,8 @@ extension NSManagedObjectContext { private struct PropertyKeys { - static var observerForWillSaveNotification: Void? - static var shouldCascadeSavesToParent: Void? + static nonisolated(unsafe) var observerForWillSaveNotification: Void? + static nonisolated(unsafe) var shouldCascadeSavesToParent: Void? } diff --git a/Sources/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift index 6ddbeffc..6cac0774 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -76,6 +76,14 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { } } + @nonobjc + public func fetchExisting( + _ persistentID: PersistentID + ) -> O? { + + return self.fetchExisting(persistentID.managedObjectID) + } + @nonobjc public func fetchExisting( _ objectID: NSManagedObjectID @@ -100,6 +108,14 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { return objects.compactMap({ self.fetchExisting($0.cs_id()) }) } + @nonobjc + public func fetchExisting( + _ persistentIDs: S + ) -> [O] where S.Iterator.Element == PersistentID { + + return persistentIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) + } + @nonobjc public func fetchExisting( _ objectIDs: S @@ -292,30 +308,23 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { internal func fetchObjectIDs( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> [NSManagedObjectID] { - - var fetchResults: [NSManagedObjectID]? - var fetchError: Error? - self.performAndWait { + + do { - do { - - fetchResults = try self.fetch(fetchRequest.dynamicCast()) - } - catch { + return try self.performAndWait { - fetchError = error + return try self.fetch(fetchRequest.staticCast()) } } - if let fetchResults = fetchResults { - - return fetchResults + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing query request." + ) + throw coreStoreError } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError } @@ -442,30 +451,24 @@ extension NSManagedObjectContext { internal func fetchOne( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> O? { - - var fetchResults: [O]? - var fetchError: (any Swift.Error)? - self.performAndWait { + + do { - do { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - return fetchResults.first } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing fetch request." + ) + throw coreStoreError + } } @nonobjc @@ -473,89 +476,74 @@ extension NSManagedObjectContext { _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> [O] { - var fetchResults: [O]? - var fetchError: (any Swift.Error)? - self.performAndWait { + do { - do { + return try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) - } - catch { - - fetchError = error + return try self.fetch(fetchRequest.staticCast()) } } - if let fetchResults = fetchResults { - - return fetchResults + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing fetch request." + ) + throw coreStoreError } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError } @nonobjc internal func fetchCount( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> Int { - - var count = 0 - var countError: (any Swift.Error)? - self.performAndWait { + + do { - do { + let count = try self.performAndWait { - count = try self.count(for: fetchRequest.staticCast()) + return try self.count(for: fetchRequest.staticCast()) } - catch { - - countError = error + guard count != NSNotFound else { + + throw CoreStoreError(nil) } + return count } - if count == NSNotFound { - - let coreStoreError = CoreStoreError(countError) + catch { + + let coreStoreError = CoreStoreError(error) Internals.log( coreStoreError, "Failed executing count request." ) throw coreStoreError } - return count } @nonobjc internal func fetchObjectID( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> NSManagedObjectID? { - - var fetchResults: [NSManagedObjectID]? - var fetchError: (any Swift.Error)? - self.performAndWait { + + do { - do { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - return fetchResults.first } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing fetch request." + ) + throw coreStoreError + } } @@ -566,35 +554,29 @@ extension NSManagedObjectContext { _ selectTerms: [SelectTerm], fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> U? { - - var fetchResults: [Any]? - var fetchError: (any Swift.Error)? - self.performAndWait { + + do { - do { - - fetchResults = try self.fetch(fetchRequest.staticCast()) - } - catch { + let fetchResults = try self.performAndWait { - fetchError = error + return try self.fetch(fetchRequest.staticCast()) } - } - if let fetchResults = fetchResults { - - if let rawResult = fetchResults.first as? NSDictionary, + if let rawResult = fetchResults.first, let rawObject = rawResult[selectTerms.first!.keyPathString] as? U.QueryableNativeType { return Select.ReturnType.cs_fromQueryableNativeType(rawObject) } return nil } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing query request." + ) + throw coreStoreError + } } @nonobjc @@ -603,64 +585,52 @@ extension NSManagedObjectContext { fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> Any? { - var fetchResults: [Any]? - var fetchError: (any Swift.Error)? - self.performAndWait { + do { - do { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - - if let rawResult = fetchResults.first as? NSDictionary, + if let rawResult = fetchResults.first, let rawObject = rawResult[selectTerms.first!.keyPathString] { return rawObject } return nil } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing query request." + ) + throw coreStoreError + } } @nonobjc internal func queryAttributes( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> [[String: Any]] { - - var fetchResults: [Any]? - var fetchError: (any Swift.Error)? - self.performAndWait { + + do { - do { - - fetchResults = try self.fetch(fetchRequest.staticCast()) - } - catch { + let fetchResults = try self.performAndWait { - fetchError = error + return try self.fetch(fetchRequest.staticCast()) } + return NSDictionary.cs_fromQueryResultsNativeType(fetchResults) } - if let fetchResults = fetchResults { + catch { - return NSDictionary.cs_fromQueryResultsNativeType(fetchResults) + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing query request." + ) + throw coreStoreError } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError } @@ -671,36 +641,29 @@ extension NSManagedObjectContext { _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> Int { - var numberOfDeletedObjects: Int? - var fetchError: (any Swift.Error)? - self.performAndWait { + do { - Internals.autoreleasepool { - - do { + return try self.performAndWait { + + return try Internals.autoreleasepool { let fetchResults = try self.fetch(fetchRequest.staticCast()) for object in fetchResults { self.delete(object) } - numberOfDeletedObjects = fetchResults.count - } - catch { - - fetchError = error + return fetchResults.count } } } - if let numberOfDeletedObjects = numberOfDeletedObjects { - - return numberOfDeletedObjects + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing delete request." + ) + throw coreStoreError } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing delete request." - ) - throw coreStoreError } } diff --git a/Sources/NSManagedObjectContext+Setup.swift b/Sources/NSManagedObjectContext+Setup.swift index f179adf2..c5362fdf 100644 --- a/Sources/NSManagedObjectContext+Setup.swift +++ b/Sources/NSManagedObjectContext+Setup.swift @@ -65,7 +65,7 @@ extension NSManagedObjectContext { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.persistentStoreCoordinator = coordinator - context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType) context.undoManager = nil context.setupForCoreStoreWithContextName("com.corestore.rootcontext") @@ -76,16 +76,21 @@ extension NSManagedObjectContext { object: coordinator, closure: { [weak context] (note) -> Void in - context?.perform { () -> Void in + guard let context = context else { + + return + } + nonisolated(unsafe) let note = note + context.perform { if let updatedObjectIDs = (note.userInfo?[NSUpdatedObjectsKey] as? Set) { for objectID in updatedObjectIDs { - context?.registeredObject(for: objectID)?.willAccessValue(forKey: nil) + context.registeredObject(for: objectID)?.willAccessValue(forKey: nil) } } - context?.mergeChanges(fromContextDidSave: note) + context.mergeChanges(fromContextDidSave: note) } } ) @@ -100,7 +105,7 @@ extension NSManagedObjectContext { let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.parent = rootContext - context.mergePolicy = NSRollbackMergePolicy + context.mergePolicy = NSMergePolicy(merge: .rollbackMergePolicyType) context.undoManager = nil context.setupForCoreStoreWithContextName("com.corestore.maincontext") context.observerForDidSaveNotification = Internals.NotificationObserver( @@ -108,16 +113,19 @@ extension NSManagedObjectContext { object: rootContext, closure: { [weak context] (note) -> Void in - guard let rootContext = note.object as? NSManagedObjectContext, - let context = context else { - - return + guard + let rootContext = note.object as? NSManagedObjectContext, + let context = context + else { + + return } let saveMetadata = rootContext.saveMetadata context.saveMetadata = saveMetadata - let mergeChanges = { () -> Void in + nonisolated(unsafe) let note = note + let mergeChanges = { @Sendable () -> Void in if let updatedObjects = (note.userInfo?[NSUpdatedObjectsKey] as? Set) { @@ -147,9 +155,9 @@ extension NSManagedObjectContext { private struct PropertyKeys { - static var parentStack: Void? - static var observerForDidSaveNotification: Void? - static var observerForDidImportUbiquitousContentChangesNotification: Void? + static nonisolated(unsafe) var parentStack: Void? + static nonisolated(unsafe) var observerForDidSaveNotification: Void? + static nonisolated(unsafe) var observerForDidImportUbiquitousContentChangesNotification: Void? } @nonobjc diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index 9162070f..ed9f77a0 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -142,15 +142,14 @@ extension NSManagedObjectContext { @nonobjc internal func saveSynchronously( waitForMerge: Bool, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) -> (hasChanges: Bool, error: CoreStoreError?) { - var result: (hasChanges: Bool, error: CoreStoreError?) = (false, nil) - self.performAndWait { + return self.performAndWait { guard self.hasChanges else { - return + return (false, nil) } do { @@ -168,8 +167,7 @@ extension NSManagedObjectContext { saveError, "Failed to save \(Internals.typeName(NSManagedObjectContext.self))." ) - result = (true, saveError) - return + return (true, saveError) } if let parentContext = self.parent, self.shouldCascadeSavesToParent { @@ -177,20 +175,19 @@ extension NSManagedObjectContext { waitForMerge: waitForMerge, sourceIdentifier: sourceIdentifier ) - result = (true, error) + return (true, error) } else { - result = (true, nil) + return (true, nil) } } - return result } @nonobjc internal func saveAsynchronously( - sourceIdentifier: Any?, - completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } + sourceIdentifier: (any Sendable)?, + completion: @escaping @MainActor @Sendable (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } ) { self.perform { @@ -259,11 +256,11 @@ extension NSManagedObjectContext { // MARK: Internal internal let isSavingSynchronously: Bool - internal let sourceIdentifier: Any? + internal let sourceIdentifier: (any Sendable)? internal init( isSavingSynchronously: Bool, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { self.isSavingSynchronously = isSavingSynchronously @@ -276,9 +273,9 @@ extension NSManagedObjectContext { private struct PropertyKeys { - static var parentTransaction: Void? - static var saveMetadata: Void? - static var isTransactionContext: Void? - static var isDataStackContext: Void? + static nonisolated(unsafe) var parentTransaction: Void? + static nonisolated(unsafe) var saveMetadata: Void? + static nonisolated(unsafe) var isTransactionContext: Void? + static nonisolated(unsafe) var isDataStackContext: Void? } } diff --git a/Sources/NSPersistentStore+Setup.swift b/Sources/NSPersistentStore+Setup.swift index a8c7483c..21b9f52d 100644 --- a/Sources/NSPersistentStore+Setup.swift +++ b/Sources/NSPersistentStore+Setup.swift @@ -59,7 +59,7 @@ extension NSPersistentStore { private struct PropertyKeys { - static var storageInterface: Void? + static nonisolated(unsafe) var storageInterface: Void? } } diff --git a/Sources/NSPersistentStoreCoordinator+Setup.swift b/Sources/NSPersistentStoreCoordinator+Setup.swift index 78a645f5..7ffa26c2 100644 --- a/Sources/NSPersistentStoreCoordinator+Setup.swift +++ b/Sources/NSPersistentStoreCoordinator+Setup.swift @@ -33,7 +33,7 @@ extension NSPersistentStoreCoordinator { @nonobjc internal func performAsynchronously( - _ closure: @escaping () -> Void + _ closure: @escaping @Sendable () -> Void ) { self.perform(closure) @@ -41,40 +41,28 @@ extension NSPersistentStoreCoordinator { @nonobjc internal func performSynchronously( - _ closure: @escaping () -> T + _ closure: @Sendable () -> T ) -> T { - var result: T? - self.performAndWait { - - result = closure() - } - return result! + return self.performAndWait(closure) } @nonobjc internal func performSynchronously( - _ closure: @escaping () throws(any Swift.Error) -> T + _ closure: @Sendable () throws(any Swift::Error) -> T ) throws(CoreStoreError) -> T { - - var closureError: (any Swift.Error)? - var result: T? - self.performAndWait { + + do { - do { - - result = try closure() - } - catch { + return try self.performAndWait { - closureError = error + return try closure() } } - if let closureError = closureError { + catch { - throw CoreStoreError(closureError) + throw CoreStoreError(error) } - return result! } @nonobjc @@ -85,6 +73,7 @@ extension NSPersistentStoreCoordinator { options: [AnyHashable: Any]? ) throws(CoreStoreError) -> NSPersistentStore { + nonisolated(unsafe) let options = options return try self.performSynchronously { return try self.addPersistentStore( diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index 2d7bf576..a8621918 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 { +public final class ObjectMonitor: Hashable, ObjectRepresentation, Sendable { /** Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. @@ -71,32 +71,37 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ - public func addObserver(_ observer: U) where U.ObjectEntityType == O { + @MainActor + public func addObserver(_ observer: U) + where U.ObjectEntityType == O { self.unregisterObserver(observer) self.registerObserver( observer, willChangeObject: { (observer, monitor, object) in + nonisolated(unsafe) let sending = object observer.objectMonitor( monitor, - willUpdateObject: object, + willUpdateObject: sending, sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier ) }, didDeleteObject: { (observer, monitor, object) in + nonisolated(unsafe) let sending = object observer.objectMonitor( monitor, - didDeleteObject: object, + didDeleteObject: sending, sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier ) }, didUpdateObject: { (observer, monitor, object, changedPersistentKeys) in + nonisolated(unsafe) let sending = object observer.objectMonitor( monitor, - didUpdateObject: object, + didUpdateObject: sending, changedPersistentKeys: changedPersistentKeys, sourceIdentifier: monitor.context.saveMetadata?.sourceIdentifier ) @@ -111,7 +116,9 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to unregister notifications to */ - public func removeObserver(_ observer: U) where U.ObjectEntityType == O { + @MainActor + public func removeObserver(_ observer: U) + where U.ObjectEntityType == O { self.unregisterObserver(observer) } @@ -165,11 +172,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // 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 @@ -180,43 +189,48 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati public typealias ObjectType = O + public func persistentID() -> O.ObjectID { + + return .init(managedObjectID: self.managedObjectID) + } + public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { - return dataStack.unsafeContext().objectPublisher(objectID: self.id) + return dataStack.unsafeContext().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) } // MARK: Internal internal init( - objectID: O.ObjectID, + managedObjectID: NSManagedObjectID, context: NSManagedObjectContext ) { let fetchRequest = Internals.CoreStoreFetchRequest() - fetchRequest.entity = objectID.entity + fetchRequest.entity = managedObjectID.entity fetchRequest.fetchLimit = 0 fetchRequest.resultType = .managedObjectResultType fetchRequest.sortDescriptors = [] @@ -226,13 +240,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati let fetchedResultsController = Internals.CoreStoreFetchedResultsController( context: context, fetchRequest: fetchRequest, - from: From([objectID.persistentStore?.configurationName]), - applyFetchClauses: Where("SELF", isEqualTo: objectID).applyToFetchRequest + from: From([managedObjectID.persistentStore?.configurationName]), + applyFetchClauses: Where("SELF", isEqualTo: managedObjectID).applyToFetchRequest ) let fetchedResultsControllerDelegate = Internals.FetchedResultsControllerDelegate() - self.id = objectID + self.managedObjectID = managedObjectID self.fetchedResultsController = fetchedResultsController self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate @@ -240,22 +254,26 @@ 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 ( + willChangeObject: @escaping @Sendable ( _ observer: U, _ monitor: ObjectMonitor, _ object: O ) -> Void, - didDeleteObject: @escaping ( + didDeleteObject: @escaping @Sendable ( _ observer: U, _ monitor: ObjectMonitor, _ object: O ) -> Void, - didUpdateObject: @escaping ( + didUpdateObject: @escaping @Sendable ( _ observer: U, _ monitor: ObjectMonitor, _ object: O, @@ -267,13 +285,17 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati Thread.isMainThread, "Attempted to add an observer of type \(Internals.typeName(observer as AnyObject)) outside the main thread." ) + nonisolated(unsafe) weak let weakObserver = observer as Optional self.registerChangeNotification( &self.willChangeObjectKey, name: Notification.Name.objectMonitorWillChangeObject, toObserver: observer, - callback: { [weak observer] (monitor) -> Void in + callback: { (monitor) -> Void in - guard let object = monitor.object, let observer = observer else { + guard + let observer = weakObserver, + let object = monitor.object + else { return } @@ -284,9 +306,9 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati &self.didDeleteObjectKey, name: Notification.Name.objectMonitorDidDeleteObject, toObserver: observer, - callback: { [weak observer] (monitor, object) -> Void in + callback: { (monitor, object) -> Void in - guard let observer = observer else { + guard let observer = weakObserver else { return } @@ -297,31 +319,38 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati &self.didUpdateObjectKey, name: Notification.Name.objectMonitorDidUpdateObject, toObserver: observer, - callback: { [weak self, weak observer] (monitor, object) -> Void in + callback: { [weak self] (monitor, object) -> Void in - guard let self = self, let observer = observer else { + guard + let self = self, + let observer = weakObserver + else { 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 +372,14 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: Private - private let id: O.ObjectID + private let managedObjectID: 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 { @@ -361,7 +390,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping (_ monitor: ObjectMonitor) -> Void + callback: @escaping @Sendable (_ monitor: ObjectMonitor) -> Void ) { Internals.setAssociatedRetainedObject( @@ -386,7 +415,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping (_ monitor: ObjectMonitor, _ object: O) -> Void + callback: @escaping @Sendable (_ monitor: ObjectMonitor, _ object: O) -> Void ) { Internals.setAssociatedRetainedObject( @@ -395,11 +424,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati object: self, closure: { [weak self] (note) in - guard let self = self, + guard + let self = self, let userInfo = note.userInfo, - let object = userInfo[String(describing: NSManagedObject.self)] as! NSManagedObject? else { - - return + let object = userInfo[String(describing: NSManagedObject.self)] as! NSManagedObject? + else { + + return } callback(self, O.cs_fromRaw(object: object)) } diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index 109ed92c..b1dcd684 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -36,7 +36,7 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ObjectObserver: AnyObject { +public protocol ObjectObserver: AnyObject, SendableMetatype { /** The `DynamicObject` type for the observed object @@ -53,8 +53,8 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType, - sourceIdentifier: Any? + willUpdateObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) /** @@ -66,7 +66,7 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType + willUpdateObject object: sending ObjectEntityType ) /** @@ -80,9 +80,9 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: ObjectEntityType, + didUpdateObject object: sending ObjectEntityType, changedPersistentKeys: Set, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) /** @@ -95,7 +95,7 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: ObjectEntityType, + didUpdateObject object: sending ObjectEntityType, changedPersistentKeys: Set ) @@ -109,8 +109,8 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - didDeleteObject object: ObjectEntityType, - sourceIdentifier: Any? + didDeleteObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) /** @@ -122,7 +122,7 @@ public protocol ObjectObserver: AnyObject { */ func objectMonitor( _ monitor: ObjectMonitor, - didDeleteObject object: ObjectEntityType + didDeleteObject object: sending ObjectEntityType ) } @@ -133,8 +133,8 @@ extension ObjectObserver { public func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType, - sourceIdentifier: Any? + willUpdateObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) { self.objectMonitor( @@ -145,14 +145,14 @@ extension ObjectObserver { public func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType + willUpdateObject object: sending ObjectEntityType ) {} public func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: ObjectEntityType, + didUpdateObject object: sending ObjectEntityType, changedPersistentKeys: Set, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { self.objectMonitor( @@ -164,14 +164,14 @@ extension ObjectObserver { public func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: ObjectEntityType, + didUpdateObject object: sending ObjectEntityType, changedPersistentKeys: Set ) {} public func objectMonitor( _ monitor: ObjectMonitor, - didDeleteObject object: ObjectEntityType, - sourceIdentifier: Any? + didDeleteObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) { self.objectMonitor( @@ -182,6 +182,6 @@ extension ObjectObserver { public func objectMonitor( _ monitor: ObjectMonitor, - didDeleteObject object: ObjectEntityType + didDeleteObject object: sending ObjectEntityType ) {} } diff --git a/Sources/ObjectPublisher.SnapshotPublisher.swift b/Sources/ObjectPublisher.SnapshotPublisher.swift index 2a78d241..80e6348d 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,8 @@ extension ObjectPublisher { // MARK: - ObjectSnapshotSubscription - fileprivate final class ObjectSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { + fileprivate final class ObjectSnapshotSubscription: Subscription + where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -114,21 +115,26 @@ extension ObjectPublisher { return } - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in - - guard - let self = self, - let subscriber = self.subscriber - else { + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { + + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in - return + guard + let self = weakSelf, + let subscriber = self.subscriber + else { + + return + } + _ = subscriber.receive(publisher.snapshot) } - _ = subscriber.receive(publisher.snapshot) - } - ) + ) + } } @@ -138,16 +144,10 @@ extension ObjectPublisher { self.subscriber = nil - if Thread.isMainThread { - - self.publisher.removeObserver(self) - } - else { + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - DispatchQueue.main.async { - - self.publisher.removeObserver(self) - } + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 32b11b32..c239cccd 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) @@ -76,10 +76,11 @@ public final class ObjectPublisher: ObjectRepresentation, Hash - parameter notifyInitial: if `true`, the callback is executed immediately with the current publisher state. Otherwise only succeeding updates will notify the observer. Default value is `false`. - parameter callback: the closure to execute when changes occur */ + @MainActor public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ObjectPublisher) -> Void + _ callback: @escaping @Sendable (ObjectPublisher) -> Void ) { Internals.assert( @@ -112,11 +113,12 @@ public final class ObjectPublisher: ObjectRepresentation, Hash - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. - parameter callback: the closure to execute when changes occur */ + @MainActor public func addObserver( _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ objectPublisher: ObjectPublisher, _ sourceIdentifier: Any? ) -> Void @@ -145,6 +147,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash - parameter observer: the object whose notifications will be unregistered */ + @MainActor public func removeObserver(_ observer: T) { Internals.assert( @@ -157,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 @@ -172,6 +177,11 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public typealias ObjectType = O + public func persistentID() -> O.ObjectID { + + return .init(managedObjectID: self.managedObjectID) + } + public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() @@ -179,17 +189,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? { @@ -199,7 +209,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? { @@ -209,7 +219,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self.lazySnapshot } - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } @@ -217,7 +227,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 } @@ -226,24 +236,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:) ) } @@ -258,43 +268,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) - } - 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(managedObjectID, context) } - 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/ObjectReader.swift b/Sources/ObjectReader.swift index 6231dece..fe74ab9b 100644 --- a/Sources/ObjectReader.swift +++ b/Sources/ObjectReader.swift @@ -106,7 +106,7 @@ public struct ObjectReader, Value>, @ViewBuilder content: @escaping (Value) -> Content, @ViewBuilder placeholder: @escaping () -> Placeholder - ) where Placeholder == EmptyView { + ) { self._object = .init(objectPublisher) self.content = { diff --git a/Sources/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index 0ad37874..38ff5bc9 100644 --- a/Sources/ObjectRepresentation.swift +++ b/Sources/ObjectRepresentation.swift @@ -34,14 +34,16 @@ import CoreData public protocol AnyObjectRepresentation { /** - The internal ID for the object. + Used internally by CoreStore. Do not call directly. */ - func objectID() -> NSManagedObjectID + @_spi(Internals) + func cs_dataStack() -> DataStack? /** - Used internally by CoreStore. Do not call directly. + The internal `NSManagedObjectID` for the object. Do not call directly. */ - func cs_dataStack() -> DataStack? + @_spi(Internals) + func cs_id() -> NSManagedObjectID } @@ -57,6 +59,11 @@ public protocol ObjectRepresentation: AnyObjectRepresentation { */ associatedtype ObjectType: DynamicObject + /** + The `Sendable` ID to use for referencing the object between isolation contexts. + */ + func persistentID() -> ObjectType.ObjectID + /** An instance that may be observed for object changes. */ @@ -83,6 +90,7 @@ public protocol ObjectRepresentation: AnyObjectRepresentation { func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? } + extension NSManagedObject: ObjectRepresentation {} extension CoreStoreObject: ObjectRepresentation {} @@ -90,6 +98,14 @@ extension CoreStoreObject: ObjectRepresentation {} extension DynamicObject where Self: ObjectRepresentation { // MARK: Public + + /** + The `Sendable` ID to use for referencing the object between isolation contexts. + */ + public func persistentID() -> Self.ObjectID { + + return .init(managedObjectID: self.cs_id()) + } /** An `ObjectPublisher` wrapper for the exact same object @@ -98,7 +114,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 +124,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 +148,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 +174,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 b4b93ad0..042329e3 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 @@ -67,32 +69,37 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public typealias ObjectType = O + public func persistentID() -> ObjectType.ObjectID { + + return .init(managedObjectID: self.managedObjectID) + } + 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 +107,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 +116,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 +154,6 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, // MARK: Private - private let id: O.ObjectID private let context: NSManagedObjectContext private var generation: UUID @@ -344,16 +350,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 +371,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 +389,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 ae79bf6a..8f072a6c 100644 --- a/Sources/ObjectState.swift +++ b/Sources/ObjectState.swift @@ -23,9 +23,9 @@ // SOFTWARE. // -#if canImport(Combine) && canImport(SwiftUI) +#if canImport(Observation) && canImport(SwiftUI) -import Combine +import Observation import SwiftUI @@ -35,7 +35,7 @@ import SwiftUI A property wrapper type that can read `ObjectPublisher` changes. */ @propertyWrapper -public struct ObjectState: DynamicProperty { +public struct ObjectState: @MainActor DynamicProperty { // MARK: Public @@ -62,19 +62,23 @@ public struct ObjectState: DynamicProperty { - parameter objectPublisher: The `ObjectPublisher` that the `ObjectState` will observe changes for */ + @MainActor public init(_ objectPublisher: ObjectPublisher?) { - self.observer = .init(objectPublisher: objectPublisher) + self.sourceObjectPublisher = objectPublisher + self._observer = .init(wrappedValue: .init(objectPublisher: objectPublisher)) } // MARK: @propertyWrapper + @MainActor public var wrappedValue: ObjectSnapshot? { return self.observer.item } + @MainActor public var projectedValue: ObjectPublisher? { return self.observer.objectPublisher @@ -83,43 +87,68 @@ public struct ObjectState: DynamicProperty { // MARK: DynamicProperty + @MainActor public mutating func update() { self._observer.update() + self.observer.rebind(to: self.sourceObjectPublisher) } // MARK: Private - @ObservedObject + @State private var observer: Observer + private let sourceObjectPublisher: ObjectPublisher? + // MARK: - Observer - private final class Observer: ObservableObject { + @MainActor + private final class Observer: Observation.Observable { - @Published - var item: ObjectSnapshot? + private(set) var objectPublisher: ObjectPublisher? - let objectPublisher: ObjectPublisher? + nonisolated var item: ObjectSnapshot? { + + get { + + self.registrar.access(self, keyPath: \.item) + return self.current.withLock({ $0 }) + } + set { + + self.registrar.withMutation(of: self, keyPath: \.item) { + + self.current.withLock({ $0 = newValue }) + } + } + } init(objectPublisher: ObjectPublisher?) { - - guard - let dataStack = objectPublisher?.cs_dataStack(), - let objectPublisher = objectPublisher?.asPublisher(in: dataStack) - else { - - self.objectPublisher = nil - self.item = nil + + self.objectPublisher = nil + self.current = .init(nil) + self.rebind(to: objectPublisher) + } + + isolated deinit { + + self.objectPublisher?.removeObserver(self) + } + + func rebind(to objectPublisher: ObjectPublisher?) { + + let objectPublisher = Self.canonicalPublisher(for: objectPublisher) + guard self.objectPublisher != objectPublisher else { + return } - + self.objectPublisher?.removeObserver(self) self.objectPublisher = objectPublisher - self.item = objectPublisher.snapshot - - objectPublisher.addObserver(self) { [weak self] (objectPublisher) in + self.item = objectPublisher?.snapshot + objectPublisher?.addObserver(self) { [weak self] objectPublisher in guard let self = self else { @@ -129,9 +158,24 @@ public struct ObjectState: DynamicProperty { } } - deinit { + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex?> + + private static func canonicalPublisher( + for objectPublisher: ObjectPublisher? + ) -> ObjectPublisher? { - self.objectPublisher?.removeObserver(self) + guard + let objectPublisher = objectPublisher, + let dataStack = objectPublisher.cs_dataStack() + else { + + return nil + } + return objectPublisher.asPublisher(in: dataStack) } } } diff --git a/Sources/OrderBy.swift b/Sources/OrderBy.swift index d1225f6d..8a9ef292 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/PersistentID.swift b/Sources/PersistentID.swift new file mode 100644 index 00000000..2a745b6c --- /dev/null +++ b/Sources/PersistentID.swift @@ -0,0 +1,142 @@ +// +// 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 PersistentID: 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 persistentID() -> ObjectType.ObjectID { + + return self + } + + 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/Progress+Convenience.swift b/Sources/Progress+Convenience.swift index 9f383b49..2a46d64b 100644 --- a/Sources/Progress+Convenience.swift +++ b/Sources/Progress+Convenience.swift @@ -36,7 +36,8 @@ extension Progress { - parameter closure: the closure to execute on progress change */ @nonobjc - public func setProgressHandler(_ closure: ((_ progress: Progress) -> Void)?) { + @MainActor + public func setProgressHandler(_ closure: (@MainActor @Sendable (_ progress: Progress) -> Void)?) { self.progressObserver.progressHandler = closure } @@ -46,15 +47,19 @@ extension Progress { private struct PropertyKeys { - static var progressObserver: Void? + static nonisolated(unsafe) var progressObserver: Void? } @nonobjc + @MainActor private var progressObserver: ProgressObserver { get { - let object: ProgressObserver? = Internals.getAssociatedObjectForKey(&PropertyKeys.progressObserver, inObject: self) + let object: ProgressObserver? = Internals.getAssociatedObjectForKey( + &PropertyKeys.progressObserver, + inObject: self + ) if let observer = object { return observer @@ -76,11 +81,12 @@ extension Progress { // MARK: - ProgressObserver @objc -private final class ProgressObserver: NSObject { +private final class ProgressObserver: NSObject, Sendable { private unowned let progress: Progress - fileprivate var progressHandler: ((_ progress: Progress) -> Void)? { + @MainActor + fileprivate var progressHandler: (@MainActor @Sendable (_ progress: Progress) -> Void)? { didSet { @@ -121,7 +127,12 @@ private final class ProgressObserver: NSObject { } } - 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 26bb8800..da4e772d 100644 --- a/Sources/Relationship.ToManyOrdered.swift +++ b/Sources/Relationship.ToManyOrdered.swift @@ -196,14 +196,14 @@ 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) { self.keyPath = keyPath self.entityDescriptionValues = { - let range = (Swift.max(0, minCount) ... maxCount) + let range = (Swift::max(0, minCount) ... maxCount) return ( isToMany: true, isOrdered: true, diff --git a/Sources/Relationship.ToManyUnordered.swift b/Sources/Relationship.ToManyUnordered.swift index f07ca917..21dd0c82 100644 --- a/Sources/Relationship.ToManyUnordered.swift +++ b/Sources/Relationship.ToManyUnordered.swift @@ -196,14 +196,14 @@ 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) { self.keyPath = keyPath self.entityDescriptionValues = { - let range = (Swift.max(0, minCount) ... maxCount) + let range = (Swift::max(0, minCount) ... maxCount) return ( isToMany: true, isOrdered: false, diff --git a/Sources/Relationship.ToOne.swift b/Sources/Relationship.ToOne.swift index fb5d47d4..61e36d18 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 2e2e8e2f..61f69ed8 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -165,14 +165,18 @@ public final class SQLiteStore: LocalStorage { [NSSQLitePragmasOption: ["journal_mode": "WAL"]] ``` */ - public let storeOptions: [AnyHashable: Any]? = [ - NSSQLitePragmasOption: ["journal_mode": "WAL"], - NSBinaryStoreInsecureDecodingCompatibilityOption: true - ] + public var storeOptions: [AnyHashable: Any]? { + + return [ + NSSQLitePragmasOption: ["journal_mode": "WAL"], + NSBinaryStoreInsecureDecodingCompatibilityOption: true + ] + } /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didAddToDataStack(_ dataStack: DataStack) { self.dataStack = dataStack @@ -181,6 +185,7 @@ public final class SQLiteStore: LocalStorage { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { self.dataStack = nil @@ -202,7 +207,7 @@ public final class SQLiteStore: LocalStorage { /** 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` @@ -228,9 +233,10 @@ public final class SQLiteStore: LocalStorage { /** 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) { + ) throws(any Swift::Error) { _ = try withExtendedLifetime(NSPersistentStoreCoordinator(managedObjectModel: soureModelHint)) { (coordinator: NSPersistentStoreCoordinator) in @@ -249,15 +255,16 @@ public final class SQLiteStore: LocalStorage { /** 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? - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { func deleteFiles( storeURL: URL, extraFiles: [String] = [] - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { let fileManager = FileManager.default let extraFiles: [String] = [ @@ -372,5 +379,5 @@ public final class SQLiteStore: LocalStorage { // 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 8432abcd..61d14e7c 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 420ef1c3..9d16ad4a 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 7b00e352..8a05e043 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 ef4a229e..177ca7bf 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/SetupResult.swift b/Sources/SetupResult.swift index 27c0940f..473de0b6 100644 --- a/Sources/SetupResult.swift +++ b/Sources/SetupResult.swift @@ -47,4 +47,4 @@ import CoreData ) ``` */ -public typealias SetupResult = Swift.Result where StorageInterfaceType: StorageInterface +public typealias SetupResult = Swift::Result where StorageInterfaceType: StorageInterface diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 3ab45298..1ac74f6d 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -31,7 +31,7 @@ import CoreData /** The `StorageInterface` represents the data store managed (or to be managed) by the `DataStack`. When added to the `DataStack`, the `StorageInterface` serves as the interface for the `NSPersistentStore`. This may be a database file, an in-memory store, etc. */ -public protocol StorageInterface: AnyObject { +public protocol StorageInterface: AnyObject, Sendable { /** The string identifier for the `NSPersistentStore`'s `type` property. This is the same string CoreStore will use to create the `NSPersistentStore` from the `NSPersistentStoreCoordinator`'s `addPersistentStoreWithType(...)` method. @@ -54,11 +54,13 @@ public protocol StorageInterface: AnyObject { /** 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) } @@ -68,7 +70,7 @@ public protocol StorageInterface: AnyObject { /** The `LocalStorageOptions` provides settings that tells the `DataStack` how to setup the persistent store for `LocalStorage` implementers. */ -public struct LocalStorageOptions: OptionSet, ExpressibleByNilLiteral { +public struct LocalStorageOptions: OptionSet, ExpressibleByNilLiteral, Sendable { /** Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch @@ -146,17 +148,19 @@ 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) + ) throws(any Swift::Error) /** 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? - ) throws(any Swift.Error) + ) throws(any Swift::Error) } extension LocalStorage { diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index ced1489a..965bfe35 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 `PersistentID` 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: PersistentID? + ) -> 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`. @@ -85,6 +103,26 @@ public final class SynchronousDataTransaction: BaseDataTransaction { return super.edit(object) } + /** + Returns an editable proxy of the object with the specified `PersistentID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `PersistentID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ into: Into, + _ persistentID: PersistentID + ) -> O? { + + Internals.assert( + !self.isCommitted, + "Attempted to update an entity of type \(Internals.typeName(into.entityClass)) from an already committed \(Internals.typeName(self))." + ) + + return super.edit(into, persistentID) + } + /** Returns an editable proxy of the object with the specified `NSManagedObjectID`. @@ -104,6 +142,23 @@ public final class SynchronousDataTransaction: BaseDataTransaction { return super.edit(into, objectID) } + + /** + Deletes the objects with the specified `NSManagedObjectID`s. + + - parameter objectIDs: the `NSManagedObjectID`s of the objects to delete + */ + public override func delete( + persistentIDs: S + ) where S.Iterator.Element == PersistentID { + + Internals.assert( + !self.isCommitted, + "Attempted to delete an entities from an already committed \(Internals.typeName(self))." + ) + + super.delete(persistentIDs: persistentIDs) + } /** Deletes the objects with the specified `NSManagedObjectID`s. @@ -164,7 +219,7 @@ public final class SynchronousDataTransaction: BaseDataTransaction { internal init( mainContext: NSManagedObjectContext, queue: DispatchQueue, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { super.init( diff --git a/Sources/Tweak.swift b/Sources/Tweak.swift index 1a63bd11..2b6fb21b 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 a313b327..108c6271 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/UnsafeDataTransaction+Observing.swift b/Sources/UnsafeDataTransaction+Observing.swift index 0c24c917..a62c7a45 100644 --- a/Sources/UnsafeDataTransaction+Observing.swift +++ b/Sources/UnsafeDataTransaction+Observing.swift @@ -41,7 +41,7 @@ extension UnsafeDataTransaction { _ object: O ) -> ObjectMonitor { - return .init(objectID: object.cs_id(), context: self.unsafeContext()) + return .init(managedObjectID: object.cs_id(), context: self.unsafeContext()) } /** @@ -119,7 +119,7 @@ extension UnsafeDataTransaction { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ public func monitorList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ fetchClauses: FetchClause... ) { @@ -135,7 +135,7 @@ extension UnsafeDataTransaction { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ public func monitorList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ fetchClauses: [FetchClause] ) { @@ -174,7 +174,7 @@ extension UnsafeDataTransaction { - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses */ public func monitorList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B ) { @@ -265,7 +265,7 @@ extension UnsafeDataTransaction { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ public func monitorSectionedList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: FetchClause... @@ -283,7 +283,7 @@ extension UnsafeDataTransaction { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ public func monitorSectionedList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, _ sectionBy: SectionBy, _ fetchClauses: [FetchClause] @@ -323,7 +323,7 @@ extension UnsafeDataTransaction { - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses */ public func monitorSectionedList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B ) { diff --git a/Sources/UnsafeDataTransaction.swift b/Sources/UnsafeDataTransaction.swift index 0dd43094..b6f23561 100644 --- a/Sources/UnsafeDataTransaction.swift +++ b/Sources/UnsafeDataTransaction.swift @@ -32,7 +32,7 @@ import CoreData /** The `UnsafeDataTransaction` provides an interface for non-contiguous `NSManagedObject` or `CoreStoreObject` creates, updates, and deletes. This is useful for making temporary changes, such as partially filled forms. An unsafe transaction object should typically be only used from the main queue. */ -public final class UnsafeDataTransaction: BaseDataTransaction { +public final class UnsafeDataTransaction: BaseDataTransaction, @unchecked Sendable { // MARK: - @@ -42,7 +42,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction { - parameter completion: the block executed after the save completes. Success or failure is reported by the optional `error` argument of the block. */ public func commit( - _ completion: @escaping (_ error: CoreStoreError?) -> Void + _ completion: @escaping @Sendable (_ error: CoreStoreError?) -> Void ) { self.context.saveAsynchronously( @@ -114,7 +114,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction { - throws: an error thrown from `closure`, or an error thrown by Core Data (usually validation errors or conflict errors) */ public func flush( - closure: () throws(any Swift.Error) -> Void + closure: () throws(any Swift::Error) -> Void ) rethrows { try closure() @@ -142,7 +142,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction { */ public func beginUnsafe( supportsUndo: Bool = false, - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) -> UnsafeDataTransaction { return UnsafeDataTransaction( @@ -160,7 +160,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction { mainContext: NSManagedObjectContext, queue: DispatchQueue, supportsUndo: Bool, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { super.init( diff --git a/Sources/UserInfo.swift b/Sources/UserInfo.swift index 133eee31..05dfc571 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 53b0065b..5413f42b 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 496e7732..e3aaf1a0 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 af693210..6ed7f068 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? }