From 49f65601cde7f485296bd99d369044457af2c732 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 7 Jul 2026 11:18:17 +0900 Subject: [PATCH 1/7] Initial prototype for Swift 6 mode --- CoreStore.xcodeproj/project.pbxproj | 24 +- CoreStoreTests/BaseTestCase.swift | 36 ++- CoreStoreTests/ConvenienceTests.swift | 2 + CoreStoreTests/DynamicModelTests.swift | 36 ++- CoreStoreTests/GroupByTests.swift | 1 + CoreStoreTests/ListPublisherTests.swift | 14 +- CoreStoreTests/ObjectObserverTests.swift | 93 +++--- CoreStoreTests/ObjectPublisherTests.swift | 2 + CoreStoreTests/SetupTests.swift | 1 + .../Modern/ColorsDemo/Modern.ColorsDemo.swift | 2 +- Package.swift | 2 +- Sources/AsynchronousDataTransaction.swift | 8 +- Sources/BaseDataTransaction.swift | 9 +- Sources/CoreStoreDefaults.swift | 52 +++- Sources/CoreStoreError.swift | 4 +- Sources/CoreStoreLogger.swift | 2 +- Sources/CoreStoreManagedObject.swift | 12 +- Sources/CoreStoreObject+Observing.swift | 2 +- Sources/CoreStoreObject.swift | 4 +- Sources/DataStack+Concurrency.swift | 56 ++-- Sources/DataStack+Migration.swift | 90 ++++-- Sources/DataStack+Observing.swift | 12 +- Sources/DataStack+Reactive.swift | 17 +- Sources/DataStack+Transaction.swift | 24 +- Sources/DataStack.AddStoragePublisher.swift | 32 +- Sources/DataStack.swift | 6 +- Sources/DiffableDataSource.BaseAdapter.swift | 16 +- ...aSource.CollectionViewAdapter-AppKit.swift | 82 ++++-- ...taSource.CollectionViewAdapter-UIKit.swift | 77 +++-- ...bleDataSource.TableViewAdapter-UIKit.swift | 86 ++++-- Sources/DiffableDataSource.Target.swift | 16 +- Sources/DispatchQueue+CoreStore.swift | 4 +- Sources/ImportableObject.swift | 2 +- Sources/InMemoryStore.swift | 7 +- Sources/Internals.AnyFieldCoder.swift | 2 +- Sources/Internals.CoreStoreFetchRequest.swift | 2 +- ...ls.CoreStoreFetchedResultsController.swift | 2 +- ...Internals.DiffableDataSourceSnapshot.swift | 2 +- ...s.DiffableDataUIDispatcher.Changeset.swift | 3 +- ....DiffableDataUIDispatcher.DiffResult.swift | 4 +- ...ableDataUIDispatcher.StagedChangeset.swift | 9 +- .../Internals.DiffableDataUIDispatcher.swift | 58 ++-- ...als.FetchedResultsControllerDelegate.swift | 2 +- Sources/Internals.NotificationObserver.swift | 2 +- ...Internals.SharedNotificationObserver.swift | 4 +- Sources/Internals.swift | 67 ++++- Sources/Into.swift | 2 +- Sources/ListMonitor.swift | 46 +-- Sources/ListObserver.swift | 2 +- Sources/ListPublisher.SnapshotPublisher.swift | 41 ++- Sources/ListPublisher.swift | 9 +- Sources/ListState.swift | 44 ++- Sources/MigrationType.swift | 2 +- Sources/NSManagedObject+DynamicModel.swift | 2 +- .../NSManagedObjectContext+CoreStore.swift | 4 +- Sources/NSManagedObjectContext+Querying.swift | 277 +++++++----------- Sources/NSManagedObjectContext+Setup.swift | 34 ++- .../NSManagedObjectContext+Transaction.swift | 31 +- Sources/NSPersistentStore+Setup.swift | 2 +- .../NSPersistentStoreCoordinator+Setup.swift | 33 +-- Sources/ObjectMonitor.swift | 16 +- Sources/ObjectObserver.swift | 4 +- .../ObjectPublisher.SnapshotPublisher.swift | 41 ++- Sources/ObjectPublisher.swift | 3 + Sources/ObjectState.swift | 40 ++- Sources/Progress+Convenience.swift | 16 +- Sources/SQLiteStore.swift | 13 +- Sources/StorageInterface.swift | 4 +- Sources/SynchronousDataTransaction.swift | 2 +- Sources/UnsafeDataTransaction+Observing.swift | 12 +- Sources/UnsafeDataTransaction.swift | 8 +- 71 files changed, 996 insertions(+), 682 deletions(-) diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index b4326833..697b110c 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -3016,8 +3016,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 +3027,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 +3083,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 +3092,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 +3366,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 +3401,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", diff --git a/CoreStoreTests/BaseTestCase.swift b/CoreStoreTests/BaseTestCase.swift index f12acde4..13c3968f 100644 --- a/CoreStoreTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTestCase.swift @@ -24,6 +24,7 @@ // import XCTest +import os @testable import CoreStore @@ -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(initialState: 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: OSAllocatedUnfairLock<[Expectation: XCTestExpectation]> private func fulfill(_ expectation: Expectation) { - if let instance = self.expectations[expectation] { + self.expectations.withLock { - instance.fulfill() - self.expectations[expectation] = nil - } - else { - - 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..0bc9e6a2 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 { @@ -203,7 +215,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 +235,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 +393,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 +418,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 +439,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 +476,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/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/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..2e09cd13 100644 --- a/CoreStoreTests/ObjectObserverTests.swift +++ b/CoreStoreTests/ObjectObserverTests.swift @@ -24,6 +24,7 @@ // import XCTest +import os @testable import CoreStore @@ -34,6 +35,7 @@ import CoreStore class ObjectObserverTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatObjectObservers_CanReceiveUpdateNotifications() { self.prepareStack { (stack) in @@ -54,23 +56,27 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - var events = 0 + let events: OSAllocatedUnfairLock = .init(initialState: 0) _ = 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 as NSDictionary?) ?? [:]), + ["object": object] as NSDictionary + ) + defer { + + events += 1 + } + return events == 0 } - return events == 0 } ) _ = self.expectation( @@ -78,28 +84,32 @@ class ObjectObserverTests: BaseTestDataTestCase { object: observer, handler: { (note) -> Bool in - XCTAssertEqual(events, 1) - XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - [ - "object": object, - "changedPersistentKeys": 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 { + nonisolated(unsafe) let note = note + return events.withLock { events in + + XCTAssertEqual(events, 1) + XCTAssertEqual( + ((note.userInfo as NSDictionary?) ?? [:]), + [ + "object": object, + "changedPersistentKeys": 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") - events += 1 + defer { + + events += 1 + } + return events == 1 } - return events == 1 } ) let saveExpectation = self.expectation(description: "save") @@ -131,6 +141,7 @@ class ObjectObserverTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatObjectObservers_CanReceiveDeleteNotifications() { self.prepareStack { (stack) in @@ -151,23 +162,27 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - var events = 0 + let events: OSAllocatedUnfairLock = .init(initialState: 0) _ = 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 as NSDictionary?) ?? [:]), + ["object": object] as NSDictionary + ) + defer { + + events += 1 + } + return events == 0 } - return events == 0 } ) let saveExpectation = self.expectation(description: "save") @@ -202,7 +217,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..1bf8a0b5 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 @@ -93,6 +94,7 @@ class ObjectPublisherTests: BaseTestDataTestCase { } @objc + @MainActor dynamic func test_ThatObjectPublishers_CanReceiveDeleteNotifications() { self.prepareStack { (stack) in diff --git a/CoreStoreTests/SetupTests.swift b/CoreStoreTests/SetupTests.swift index e9841dcc..7b56d127 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -35,6 +35,7 @@ import CoreStore class SetupTests: BaseTestDataTestCase { @objc + @MainActor dynamic func test_ThatDataStacks_ConfigureCorrectly() { do { diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift index ab8b7cc7..3becb11e 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift @@ -74,7 +74,7 @@ extension Modern { // MARK: - TransactionSource - enum TransactionSource { + enum TransactionSource: Sendable { case add case delete diff --git a/Package.swift b/Package.swift index 7cf2578a..3ca46187 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", targets: ["CoreStore"]) diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index 46e0891c..ce012277 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -175,7 +175,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { internal init( mainContext: NSManagedObjectContext, queue: DispatchQueue, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { super.init( @@ -188,7 +188,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { } internal func autoCommit( - _ completion: @escaping ( + _ completion: @escaping @MainActor ( _ hasChanges: Bool, _ error: CoreStoreError? ) -> Void @@ -197,12 +197,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.swift b/Sources/BaseDataTransaction.swift index e09bd9b6..df88ee7d 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -441,7 +441,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 +471,7 @@ public /*abstract*/ class BaseDataTransaction { queue: DispatchQueue, supportsUndo: Bool, bypassesQueueing: Bool, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { let context = mainContext.temporaryContextInTransactionWithConcurrencyType( @@ -493,7 +493,10 @@ public /*abstract*/ class BaseDataTransaction { } else if context.undoManager == nil { - context.undoManager = UndoManager() + Internals.mainActorImmediate { + + context.undoManager = UndoManager() + } } } diff --git a/Sources/CoreStoreDefaults.swift b/Sources/CoreStoreDefaults.swift index 26ddec69..f66dec44 100644 --- a/Sources/CoreStoreDefaults.swift +++ b/Sources/CoreStoreDefaults.swift @@ -24,6 +24,7 @@ // import Foundation +import os // MARK: - CoreStoreDefaults @@ -36,7 +37,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 +70,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 +94,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: OSAllocatedUnfairLock = .init(initialState: nil) + private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil) } diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index a7f9a804..4f463e03 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: Error, CustomNSError, Hashable, @unchecked Sendable { /** A failure occured because of an unknown error. diff --git a/Sources/CoreStoreLogger.swift b/Sources/CoreStoreLogger.swift index 31e59469..7e6c1173 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. 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/DataStack+Concurrency.swift b/Sources/DataStack+Concurrency.swift index 45237120..f6c50fc9 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 @@ -123,8 +123,8 @@ extension DataStack.AsyncNamespace { 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 + ) ) - ) + } } } } @@ -185,7 +188,7 @@ extension DataStack.AsyncNamespace { 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) + } ) } } @@ -222,6 +228,7 @@ extension DataStack.AsyncNamespace { source: O.ImportSource ) 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) + } ) } } @@ -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,7 +319,7 @@ 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 ( @@ -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,7 +373,7 @@ extension DataStack.AsyncNamespace { - returns: The value returned from the `task` closure. - throws: A `CoreStoreError` value indicating the failure reason */ - public func perform( + public func perform( _ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift.Error) -> Output ) async throws(any Swift.Error) -> Output { diff --git a/Sources/DataStack+Migration.swift b/Sources/DataStack+Migration.swift index 8f94f902..14e50050 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -24,7 +24,8 @@ // import Foundation -import CoreData +@preconcurrency import CoreData +import os // MARK: - DataStack @@ -49,7 +50,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping (SetupResult) -> Void + completion: @escaping @MainActor (SetupResult) -> Void ) { self.coordinator.performAsynchronously { @@ -110,7 +111,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping (SetupResult) -> Void + completion: @escaping @MainActor (SetupResult) -> Void ) -> Progress? { let fileURL = storage.fileURL @@ -162,7 +163,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 +193,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 +219,10 @@ extension DataStack { } catch { - completion(.failure(CoreStoreError(error))) + DispatchQueue.main.async { + + completion(.failure(CoreStoreError(error))) + } } } ) @@ -264,7 +274,7 @@ extension DataStack { */ public func upgradeStorageIfNeeded( _ storage: T, - completion: @escaping (MigrationResult) -> Void + completion: @escaping @MainActor (MigrationResult) -> Void ) throws(CoreStoreError) -> Progress? { return try self.coordinator.performSynchronously { @@ -376,7 +386,7 @@ extension DataStack { private func upgradeStorageIfNeeded( _ storage: T, metadata: [String: Any], - completion: @escaping (MigrationResult) -> Void + completion: @escaping @MainActor (MigrationResult) -> Void ) -> Progress? { guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else { @@ -423,9 +433,13 @@ extension DataStack { } let migrationTypes = migrationSteps.map { $0.migrationType } - var migrationResult: MigrationResult? + let migrationState: OSAllocatedUnfairLock<(migrationResult: MigrationResult?, cancelled: Bool)> = .init( + initialState: ( + migrationResult: nil, + cancelled: false + ) + ) var operations = [Operation]() - var cancelled = false let progress = Progress(parent: nil, userInfo: nil) progress.totalUnitCount = numberOfMigrations @@ -440,12 +454,15 @@ extension DataStack { 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 +482,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 + } + } } } @@ -488,7 +511,10 @@ extension DataStack { DispatchQueue.main.async { progress.setProgressHandler(nil) - completion(migrationResult ?? .success(migrationTypes)) + completion( + migrationState.withLock { $0.migrationResult } + ?? .success(migrationTypes) + ) return } } @@ -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: OSAllocatedUnfairLock = .init(initialState: 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) @@ -626,7 +658,7 @@ extension DataStack { } timerQueue.sync { - fakeProgress = 1 + 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..2ec7f30c 100644 --- a/Sources/DataStack+Observing.swift +++ b/Sources/DataStack+Observing.swift @@ -125,7 +125,7 @@ extension DataStack { - 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... ) { @@ -145,7 +145,7 @@ extension DataStack { - 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] ) { @@ -188,7 +188,7 @@ extension DataStack { - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses */ public func monitorList( - createAsynchronously: @escaping (ListMonitor) -> Void, + createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B ) { @@ -288,7 +288,7 @@ extension DataStack { - 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... @@ -311,7 +311,7 @@ extension DataStack { - 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] @@ -356,7 +356,7 @@ extension DataStack { - 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/DataStack+Reactive.swift b/Sources/DataStack+Reactive.swift index 7ae01e4b..f4151248 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 } ) -> 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 ) -> 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..2994b440 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 + sourceIdentifier: (any Sendable)? = nil, + completion: @escaping @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 + sourceIdentifier: (any Sendable)? = nil, + success: @escaping @Sendable (sending T) -> Void, + failure: @escaping @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) @@ -125,7 +125,7 @@ extension DataStack { _ transaction: SynchronousDataTransaction ) 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( diff --git a/Sources/DataStack.AddStoragePublisher.swift b/Sources/DataStack.AddStoragePublisher.swift index cc2ec73d..62caaac5 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, @unchecked Sendable + where S.Input == Output, S.Failure == CoreStoreError { // MARK: FilePrivate @@ -114,7 +115,7 @@ extension DataStack { return } - var progress: Progress? = nil + nonisolated(unsafe) var progress: Progress? = nil progress = self.dataStack.addStorage( self.storage, completion: { [weak self] result in @@ -150,21 +151,24 @@ extension DataStack { ) if let progress = progress { - progress.setProgressHandler { [weak self] progress in + Internals.mainActorImmediate { @MainActor [weak self] 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 = self, + 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..be5a3f73 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, @unchecked 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 () -> Void = {} ) { let coordinator = self.coordinator @@ -459,7 +459,7 @@ 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 diff --git a/Sources/DiffableDataSource.BaseAdapter.swift b/Sources/DiffableDataSource.BaseAdapter.swift index e3ffb46c..c4ced632 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( diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift index fb27d34c..e7a488cc 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,6 +122,7 @@ extension DiffableDataSource { } @objc + @MainActor open dynamic func collectionView( _ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath @@ -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..73014d1b 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift @@ -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..a3f39367 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 (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 @@ -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..961d5cab 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 { diff --git a/Sources/DispatchQueue+CoreStore.swift b/Sources/DispatchQueue+CoreStore.swift index ceed4de8..6e20400a 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) } @@ -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/ImportableObject.swift b/Sources/ImportableObject.swift index a34916ca..2f7f3b97 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`. diff --git a/Sources/InMemoryStore.swift b/Sources/InMemoryStore.swift index 3b3a5f20..8794d729 100644 --- a/Sources/InMemoryStore.swift +++ b/Sources/InMemoryStore.swift @@ -31,7 +31,7 @@ import CoreData /** A storage interface that is backed only in memory. */ -public final class InMemoryStore: StorageInterface { +public final class InMemoryStore: StorageInterface, @unchecked Sendable { /** Initializes an `InMemoryStore` for the specified configuration @@ -66,7 +66,10 @@ 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. 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.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..7abbbce1 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 diff --git a/Sources/Internals.DiffableDataSourceSnapshot.swift b/Sources/Internals.DiffableDataSourceSnapshot.swift index b7940ae4..9a5d4c97 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 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..f9777a53 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.swift @@ -32,6 +32,8 @@ import QuartzCore #endif +import os + // MARK: - Internals @@ -41,7 +43,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 +57,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 +76,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 +99,17 @@ extension Internals { return } - let performDiffingUpdates: () -> Void = { + let performDiffingUpdates: @MainActor () -> 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) @@ -212,7 +218,7 @@ extension Internals { // MARK: - ElementPath @usableFromInline - internal struct ElementPath: Hashable { + internal struct ElementPath: Hashable, Sendable { @usableFromInline var element: Int @@ -231,18 +237,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 () -> Void) { let count = self.executingCount.incrementAndGet() if Thread.isMainThread && count == 1 { - action() + MainActor.assumeIsolated { + + action() + } self.executingCount.decrement() } else { @@ -267,36 +276,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: OSAllocatedUnfairLock = .init(initialState: 0) } } 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.NotificationObserver.swift b/Sources/Internals.NotificationObserver.swift index 42a42474..e6f36700 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( diff --git a/Sources/Internals.SharedNotificationObserver.swift b/Sources/Internals.SharedNotificationObserver.swift index ebdc3dee..2a56c0ad 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( diff --git a/Sources/Internals.swift b/Sources/Internals.swift index 501f5e91..a281d70a 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,7 +130,7 @@ internal enum Internals { return ObjectiveC.autoreleasepool(invoking: closure) } - + @inline(__always) internal static func autoreleasepool( _ closure: () throws(any Swift.Error) -> T @@ -137,31 +138,65 @@ internal enum Internals { 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 { - - throw CoreStoreError(error) - } + 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 { + ) async throws(any Swift.Error) -> sending T { return try await _Concurrency.withCheckedThrowingContinuation( function: function, body ) } + + @inline(__always) + internal static func withCheckedThrowingContinuation( + function: String = #function, + _ body: (CheckedContinuation) -> Void + ) async throws(E) -> sending T { + + return try await _Concurrency.withCheckedThrowingContinuation( + function: function, + body + ) + } + + + @inline(__always) + internal static func mainActorImmediate( + _ body: @escaping @MainActor () -> 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..eba923dd 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, @unchecked Sendable { /** The associated `NSManagedObject` or `CoreStoreObject` entity class diff --git a/Sources/ListMonitor.swift b/Sources/ListMonitor.swift index efd0e8af..e5053be3 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, @unchecked Sendable { // MARK: Public (Accessors) @@ -648,7 +648,7 @@ public final class ListMonitor: Hashable { */ public func refetch( _ fetchClauses: FetchClause..., - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) { self.refetch( @@ -668,7 +668,7 @@ public final class ListMonitor: Hashable { */ public func refetch( _ fetchClauses: [FetchClause], - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) { self.refetch( @@ -751,7 +751,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 +786,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 +803,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,7 +828,7 @@ public final class ListMonitor: Hashable { _ notificationKey: UnsafeRawPointer, name: Notification.Name, toObserver observer: AnyObject, - callback: @escaping ( + callback: @escaping @Sendable ( _ monitor: ListMonitor, _ object: O, _ indexPath: IndexPath?, @@ -864,7 +864,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,21 +892,21 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver( + 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) { @@ -969,27 +969,27 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver( + 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, @@ -1056,15 +1056,15 @@ public final class ListMonitor: Hashable { ) } - internal func registerObserver( + 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, @@ -1127,7 +1127,7 @@ public final class ListMonitor: Hashable { internal func refetch( _ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { Internals.assert( @@ -1302,7 +1302,7 @@ public final class ListMonitor: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: ((ListMonitor) -> Void)? + createAsynchronously: (@Sendable (ListMonitor) -> Void)? ) { self.isSectioned = (sectionBy != nil) diff --git a/Sources/ListObserver.swift b/Sources/ListObserver.swift index 30940b1e..6ef6512c 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, Sendable { /** The `NSManagedObject` type for the observed list diff --git a/Sources/ListPublisher.SnapshotPublisher.swift b/Sources/ListPublisher.SnapshotPublisher.swift index 65e904f0..36285d17 100644 --- a/Sources/ListPublisher.SnapshotPublisher.swift +++ b/Sources/ListPublisher.SnapshotPublisher.swift @@ -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, @unchecked Sendable + where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -114,21 +115,24 @@ extension ListPublisher { return } - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in - - guard - let self = self, - let subscriber = self.subscriber - else { + Internals.mainActorImmediate { [self] in + + self.publisher.addObserver( + self, + notifyInitial: self.emitInitialValue, + { [weak self] (publisher) in - return + guard + let self = self, + let subscriber = self.subscriber + else { + + return + } + _ = subscriber.receive(publisher.snapshot) } - _ = subscriber.receive(publisher.snapshot) - } - ) + ) + } } @@ -138,17 +142,10 @@ extension ListPublisher { self.subscriber = nil - if Thread.isMainThread { + Internals.mainActorImmediate { self.publisher.removeObserver(self) } - else { - - DispatchQueue.main.async { - - self.publisher.removeObserver(self) - } - } } diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index ec9d435f..5657f343 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -94,6 +94,7 @@ 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, @@ -128,6 +129,7 @@ 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, @@ -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,7 +188,7 @@ public final class ListPublisher: Hashable { */ public func refetch( _ clauseChain: B, - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) throws(any Swift.Error) where B.ObjectType == O { try self.refetch( @@ -214,7 +217,7 @@ public final class ListPublisher: Hashable { */ public func refetch( _ clauseChain: B, - sourceIdentifier: Any? = nil + sourceIdentifier: (any Sendable)? = nil ) throws(any Swift.Error) where B.ObjectType == O { try self.refetch( @@ -342,7 +345,7 @@ public final class ListPublisher: Hashable { from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) throws(any Swift.Error) { let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( diff --git a/Sources/ListState.swift b/Sources/ListState.swift index 2c82d6b2..3c9c9866 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 @@ -65,11 +65,12 @@ public struct ListState: DynamicProperty { - parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for */ + @MainActor public init( _ listPublisher: ListPublisher ) { - self.observer = .init(listPublisher: listPublisher) + self._observer = .init(wrappedValue: .init(listPublisher: listPublisher)) } /** @@ -98,6 +99,7 @@ public struct ListState: DynamicProperty { - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses */ + @MainActor public init( _ clauseChain: B, in dataStack: DataStack @@ -139,6 +141,7 @@ public struct ListState: DynamicProperty { - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses */ + @MainActor public init( _ clauseChain: B, in dataStack: DataStack @@ -174,6 +177,7 @@ 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, _ fetchClauses: FetchClause..., @@ -212,6 +216,7 @@ 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, _ fetchClauses: [FetchClause], @@ -256,6 +261,7 @@ 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, @@ -303,6 +309,7 @@ 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, @@ -316,11 +323,13 @@ public struct ListState: DynamicProperty { // MARK: @propertyWrapper + @MainActor public var wrappedValue: ListSnapshot { return self.observer.items } + @MainActor public var projectedValue: ListPublisher { return self.observer.listPublisher @@ -337,23 +346,40 @@ public struct ListState: DynamicProperty { // MARK: Private - @ObservedObject + @State private var observer: Observer // MARK: - Observer - private final class Observer: ObservableObject { + @MainActor + private final class Observer: Observation.Observable { - @Published - var items: ListSnapshot + private let registrar = ObservationRegistrar() + private var _items: ListSnapshot let listPublisher: ListPublisher + var items: ListSnapshot { + + get { + + self.registrar.access(self, keyPath: \.items) + return self._items + } + set { + + self.registrar.withMutation(of: self, keyPath: \.items) { + + self._items = newValue + } + } + } + init(listPublisher: ListPublisher) { self.listPublisher = listPublisher - self.items = listPublisher.snapshot + self._items = listPublisher.snapshot listPublisher.addObserver(self) { [weak self] (listPublisher) in @@ -365,7 +391,7 @@ public struct ListState: DynamicProperty { } } - deinit { + isolated deinit { self.listPublisher.removeObserver(self) } 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+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..b2d2517f 100644 --- a/Sources/NSManagedObjectContext+CoreStore.swift +++ b/Sources/NSManagedObjectContext+CoreStore.swift @@ -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..601ccb7b 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -292,30 +292,23 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { internal func fetchObjectIDs( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> [NSManagedObjectID] { - - var fetchResults: [NSManagedObjectID]? - var fetchError: Error? - self.performAndWait { + + do { - do { + return try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.dynamicCast()) - } - 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 query request." + ) + throw coreStoreError } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError } @@ -442,30 +435,24 @@ extension NSManagedObjectContext { internal func fetchOne( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> O? { - - var fetchResults: [O]? - 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 { - 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 +460,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 { - - fetchResults = try self.fetch(fetchRequest.staticCast()) - } - catch { + let fetchResults = try self.performAndWait { - fetchError = error + return try self.fetch(fetchRequest.staticCast()) } - } - 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 +538,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 +569,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 { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) - } - catch { - - 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 +625,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..ab24259a 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 (_ 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..8b6c8e08 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..83a7e734 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, @unchecked Sendable { /** Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. @@ -71,7 +71,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ - public func addObserver(_ observer: U) where U.ObjectEntityType == O { + public func addObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) self.registerObserver( @@ -243,19 +243,19 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] } - internal func registerObserver( + 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, @@ -361,7 +361,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 +386,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( diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index 109ed92c..3149e29f 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -36,12 +36,12 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ObjectObserver: AnyObject { +public protocol ObjectObserver: AnyObject & Sendable { /** The `DynamicObject` type for the observed object */ - associatedtype ObjectEntityType: DynamicObject + associatedtype ObjectEntityType: DynamicObject & Sendable /** Handles processing just before a change to the observed `object` occurs. (Optional) diff --git a/Sources/ObjectPublisher.SnapshotPublisher.swift b/Sources/ObjectPublisher.SnapshotPublisher.swift index 2a78d241..a610caa6 100644 --- a/Sources/ObjectPublisher.SnapshotPublisher.swift +++ b/Sources/ObjectPublisher.SnapshotPublisher.swift @@ -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, @unchecked Sendable + where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -114,21 +115,24 @@ extension ObjectPublisher { return } - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in - - guard - let self = self, - let subscriber = self.subscriber - else { + Internals.mainActorImmediate { [self] in + + self.publisher.addObserver( + self, + notifyInitial: self.emitInitialValue, + { [weak self] (publisher) in - return + guard + let self = self, + let subscriber = self.subscriber + else { + + return + } + _ = subscriber.receive(publisher.snapshot) } - _ = subscriber.receive(publisher.snapshot) - } - ) + ) + } } @@ -138,17 +142,10 @@ extension ObjectPublisher { self.subscriber = nil - if Thread.isMainThread { + Internals.mainActorImmediate { self.publisher.removeObserver(self) } - else { - - DispatchQueue.main.async { - - self.publisher.removeObserver(self) - } - } } diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 32b11b32..6a6388ec 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -76,6 +76,7 @@ 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, @@ -112,6 +113,7 @@ 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, @@ -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( diff --git a/Sources/ObjectState.swift b/Sources/ObjectState.swift index ae79bf6a..efa336f4 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 @@ -62,19 +62,22 @@ 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._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 @@ -91,19 +94,36 @@ public struct ObjectState: DynamicProperty { // MARK: Private - @ObservedObject + @State private var observer: Observer // MARK: - Observer - private final class Observer: ObservableObject { + @MainActor + private final class Observer: Observation.Observable { - @Published - var item: ObjectSnapshot? + private let registrar = ObservationRegistrar() + private var _item: ObjectSnapshot? let objectPublisher: ObjectPublisher? + var item: ObjectSnapshot? { + + get { + + self.registrar.access(self, keyPath: \.item) + return self._item + } + set { + + self.registrar.withMutation(of: self, keyPath: \.item) { + + self._item = newValue + } + } + } + init(objectPublisher: ObjectPublisher?) { guard @@ -112,12 +132,12 @@ public struct ObjectState: DynamicProperty { else { self.objectPublisher = nil - self.item = nil + self._item = nil return } self.objectPublisher = objectPublisher - self.item = objectPublisher.snapshot + self._item = objectPublisher.snapshot objectPublisher.addObserver(self) { [weak self] (objectPublisher) in @@ -129,7 +149,7 @@ public struct ObjectState: DynamicProperty { } } - deinit { + isolated deinit { self.objectPublisher?.removeObserver(self) } diff --git a/Sources/Progress+Convenience.swift b/Sources/Progress+Convenience.swift index 9f383b49..55992513 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 (_ 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, @unchecked Sendable { private unowned let progress: Progress - fileprivate var progressHandler: ((_ progress: Progress) -> Void)? { + @MainActor + fileprivate var progressHandler: (@MainActor (_ progress: Progress) -> Void)? { didSet { diff --git a/Sources/SQLiteStore.swift b/Sources/SQLiteStore.swift index 2e2e8e2f..3f762b89 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -33,7 +33,7 @@ import CoreData - Warning: The default SQLite file location for the `LegacySQLiteStore` and `SQLiteStore` are different. If the app was depending on CoreStore's default directories prior to 2.0.0, make sure to use the `SQLiteStore.legacy(...)` factory methods to create the `SQLiteStore` instead of using initializers directly. */ -public final class SQLiteStore: LocalStorage { +public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Initializes an SQLite store interface from the given SQLite file URL. When this instance is passed to the `DataStack`'s `addStorage()` methods, a new SQLite file will be created if it does not exist. @@ -165,10 +165,13 @@ 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. diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 3ab45298..51bc608c 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. @@ -68,7 +68,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 diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index ced1489a..c1dbad59 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -164,7 +164,7 @@ public final class SynchronousDataTransaction: BaseDataTransaction { internal init( mainContext: NSManagedObjectContext, queue: DispatchQueue, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { super.init( diff --git a/Sources/UnsafeDataTransaction+Observing.swift b/Sources/UnsafeDataTransaction+Observing.swift index 0c24c917..b56ae47a 100644 --- a/Sources/UnsafeDataTransaction+Observing.swift +++ b/Sources/UnsafeDataTransaction+Observing.swift @@ -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..1452a15b 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( @@ -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( From 890e150b9503a78f4dfe12f8da282ae13278ca43 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 15 Jul 2026 11:50:43 +0900 Subject: [PATCH 2/7] WIP --- CoreStore.xcodeproj/project.pbxproj | 24 +- CoreStoreTests/BaseTestCase.swift | 6 +- CoreStoreTests/DynamicModelTests.swift | 1 + CoreStoreTests/FetchTests.swift | 44 +- CoreStoreTests/FromTests.swift | 4 + CoreStoreTests/ImportTests.swift | 24 +- CoreStoreTests/ListObserverTests.swift | 416 +++++++------ CoreStoreTests/ObjectObserverTests.swift | 40 +- CoreStoreTests/ObjectPublisherTests.swift | 6 +- CoreStoreTests/QueryTests.swift | 9 + CoreStoreTests/SetupTests.swift | 1 + CoreStoreTests/TransactionTests.swift | 85 ++- CoreStoreTests/TweakTests.swift | 5 +- Demo/Demo.xcodeproj/project.pbxproj | 26 +- Demo/Info.plist | 21 - Demo/Sources/AppDelegate.swift | 33 - Demo/Sources/DemoApp.swift | 15 + .../Advanced.EvolutionDemo.CreatureType.swift | 1 + ...ed.EvolutionDemo.CreaturesDataSource.swift | 135 ++--- .../Advanced.EvolutionDemo.ItemView.swift | 51 +- .../Advanced.EvolutionDemo.ListView.swift | 103 +--- .../Advanced.EvolutionDemo.MainView.swift | 91 ++- .../Advanced.EvolutionDemo.Migrator.swift | 154 +++-- .../Advanced.EvolutionDemo.ProgressView.swift | 178 +++--- .../Advanced.EvolutionDemo.V1.Creature.swift | 15 +- .../Advanced.EvolutionDemo.V2.Creature.swift | 15 +- .../Advanced.EvolutionDemo.V3.Creature.swift | 31 +- .../Advanced.EvolutionDemo.V4.Creature.swift | 33 +- .../Classic.ColorsDemo.DetailView.swift | 38 +- .../Classic.ColorsDemo.LIstView.swift | 48 +- .../Classic.ColorsDemo.MainView.swift | 250 ++++---- .../ColorsDemo/Classic.ColorsDemo.swift | 2 + .../Modern.ColorsDemo.MainView.swift | 150 ++--- ...Modern.ColorsDemo.SwiftUI.DetailView.swift | 41 +- .../Modern.ColorsDemo.SwiftUI.ItemView.swift | 42 +- .../Modern.ColorsDemo.SwiftUI.ListView.swift | 59 +- .../Modern.ColorsDemo.UIKit.DetailView.swift | 36 +- ...olorsDemo.UIKit.DetailViewController.swift | 14 +- .../Modern.ColorsDemo.UIKit.ListView.swift | 60 +- ....ColorsDemo.UIKit.ListViewController.swift | 28 +- .../Modern/ColorsDemo/Modern.ColorsDemo.swift | 3 + .../Modern.PlacemarksDemo.Geocoder.swift | 73 ++- .../Modern.PlacemarksDemo.MainView.swift | 87 +-- .../Modern.PlacemarksDemo.swift | 8 +- .../Modern.PokedexDemo.MainView.swift | 47 +- .../Modern.PokedexDemo.Service.swift | 565 +++++++++--------- .../Modern.PokedexDemo.UIKit.ListView.swift | 35 +- .../PokedexDemo/Modern.PokedexDemo.swift | 6 +- .../Modern.TimeZonesDemo.ItemView.swift | 22 +- .../Modern.TimeZonesDemo.ListView.swift | 28 +- .../Modern.TimeZonesDemo.MainView.swift | 61 +- .../TimeZonesDemo/Modern.TimeZonesDemo.swift | 4 +- Demo/Sources/Helpers/ImageDownloader.swift | 83 +-- Demo/Sources/Helpers/LazyView.swift | 29 - Demo/Sources/Helpers/Menu/Menu.ItemView.swift | 51 +- Demo/Sources/Helpers/Menu/Menu.MainView.swift | 146 ++--- .../Helpers/Menu/Menu.PlaceholderView.swift | 33 +- Demo/Sources/Helpers/Menu/Menu.swift | 197 +++++- Demo/Sources/SceneDelegate.swift | 36 -- Playground_macOS.playground/Contents.swift | 4 +- Sources/AsynchronousDataTransaction.swift | 22 +- Sources/BaseDataTransaction+Querying.swift | 26 + Sources/BaseDataTransaction.swift | 21 + ...reStore+CustomDebugStringConvertible.swift | 4 +- Sources/CoreStoreDefaults.swift | 5 +- Sources/CoreStoreError.swift | 2 +- Sources/CoreStoreObject.swift | 2 + Sources/CoreStoreSchema.swift | 14 +- Sources/CustomSchemaMappingProvider.swift | 11 +- Sources/DataStack+DataSources.swift | 7 +- Sources/DataStack+Migration.swift | 34 +- Sources/DataStack+Observing.swift | 13 + Sources/DataStack+Querying.swift | 26 + Sources/DataStack+Transaction.swift | 7 +- Sources/DataStack.AddStoragePublisher.swift | 11 +- Sources/DataStack.swift | 93 ++- Sources/DiffableDataSource.BaseAdapter.swift | 8 +- ...taSource.CollectionViewAdapter-UIKit.swift | 2 +- ...bleDataSource.TableViewAdapter-UIKit.swift | 4 +- Sources/DiffableDataSource.Target.swift | 5 +- Sources/DynamicObject.swift | 54 +- Sources/DynamicObjectID.swift | 137 +++++ Sources/DynamicSchema.swift | 3 +- Sources/Entity.swift | 6 + Sources/FIeldRelationshipType.swift | 25 +- Sources/FieldOptionalType.swift | 1 + Sources/ForEach+SwiftUI.swift | 16 +- Sources/From+Querying.swift | 26 +- Sources/From.swift | 6 +- Sources/GroupBy.swift | 2 +- Sources/InMemoryStore.swift | 6 +- Sources/Internals.Closure.swift | 4 +- .../Internals.DiffableDataUIDispatcher.swift | 14 +- Sources/Internals.EntityIdentifier.swift | 2 +- Sources/Internals.Mutext.swift | 115 ++++ Sources/Internals.NotificationObserver.swift | 21 + ...Internals.SharedNotificationObserver.swift | 2 +- Sources/Internals.swift | 24 +- Sources/Into.swift | 2 +- Sources/KeyPath+Querying.swift | 4 +- Sources/ListMonitor.swift | 126 ++-- Sources/ListPublisher.SnapshotPublisher.swift | 21 +- Sources/ListPublisher.swift | 5 +- Sources/ListSnapshot.SectionInfo.swift | 7 +- Sources/ListSnapshot.swift | 52 +- Sources/ListState.swift | 45 +- Sources/MigrationChain.swift | 2 +- .../NSManagedObjectContext+CoreStore.swift | 8 +- Sources/NSManagedObjectContext+Querying.swift | 8 + .../NSManagedObjectContext+Transaction.swift | 2 +- Sources/ObjectMonitor.swift | 56 +- Sources/ObjectObserver.swift | 4 +- .../ObjectPublisher.SnapshotPublisher.swift | 21 +- Sources/ObjectPublisher.swift | 97 +-- Sources/ObjectRepresentation.swift | 24 +- Sources/ObjectSnapshot.swift | 59 +- Sources/ObjectState.swift | 19 +- Sources/OrderBy.swift | 4 +- Sources/Progress+Convenience.swift | 13 +- Sources/Relationship.ToManyOrdered.swift | 2 +- Sources/Relationship.ToManyUnordered.swift | 2 +- Sources/Relationship.ToOne.swift | 2 +- Sources/SQLiteStore.swift | 10 +- Sources/SchemaHistory.swift | 28 +- Sources/SchemaMappingProvider.swift | 3 +- Sources/SectionBy.swift | 22 +- Sources/Select.swift | 3 +- Sources/StorageInterface.swift | 4 + Sources/SynchronousDataTransaction.swift | 20 +- Sources/Tweak.swift | 14 +- Sources/UnsafeDataModelSchema.swift | 2 +- Sources/UserInfo.swift | 37 +- Sources/Where.Expression.swift | 4 +- Sources/Where.swift | 4 +- Sources/XcodeDataModelSchema.swift | 9 +- 135 files changed, 2874 insertions(+), 2505 deletions(-) delete mode 100644 Demo/Sources/AppDelegate.swift create mode 100644 Demo/Sources/DemoApp.swift delete mode 100644 Demo/Sources/Helpers/LazyView.swift delete mode 100644 Demo/Sources/SceneDelegate.swift create mode 100644 Sources/DynamicObjectID.swift create mode 100644 Sources/Internals.Mutext.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 697b110c..76e70ec8 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -19,6 +19,14 @@ 53EB03E72A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; 53EB03E82A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; 53EB03E92A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; + 5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; + 5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; + 5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; + 5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; + 5F17507E2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; }; + 5F17507F2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; }; + 5F1750802FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; }; + 5F1750812FFE35AF00BA577F /* Internals.Mutext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */; }; 82BA18931C4BBCBA00A0916E /* CoreStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 82BA18891C4BBCBA00A0916E /* CoreStore.framework */; }; 82BA18A01C4BBD1400A0916E /* CoreStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F03A53519C5C6DA005002A5 /* CoreStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 82BA18A21C4BBD1D00A0916E /* CoreStoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D1E22B19FA9FBC003B2874 /* CoreStoreError.swift */; }; @@ -883,6 +891,8 @@ 2F03A53E19C5C6DA005002A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2F03A54C19C5C872005002A5 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 53EB03E52A3AD5D400179430 /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; + 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicObjectID.swift; sourceTree = ""; }; + 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Internals.Mutext.swift; sourceTree = ""; }; 82BA18891C4BBCBA00A0916E /* CoreStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 82BA18921C4BBCBA00A0916E /* CoreStoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreStoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 82BA18DE1C4BBE2600A0916E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.1.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; @@ -1453,6 +1463,7 @@ B5831B6E1F3355C300A9F647 /* Legacy Properties */, B52F74391E9B8724005F3DAC /* Dynamic Schema */, B5D339DC1E9489C700C880DE /* DynamicObject.swift */, + 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */, B52F742E1E9B50D0005F3DAC /* SchemaHistory.swift */, B5D339E61E9493A500C880DE /* Entity.swift */, B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */, @@ -1725,6 +1736,7 @@ B54A6A541BA15F2A007870FD /* Internals.FetchedResultsControllerDelegate.swift */, B5BF7FCA234D80910070E741 /* Internals.LazyNonmutating.swift */, B5FAD6AB1B51285300714891 /* Internals.MigrationManager.swift */, + 5F17507D2FFE35AB00BA577F /* Internals.Mutext.swift */, B5E84F2B1AFF849C0064E85B /* Internals.NotificationObserver.swift */, B5277676234F265F0056BE9F /* Internals.SharedNotificationObserver.swift */, B50E17602351FA66004F033C /* Internals.Closure.swift */, @@ -2131,6 +2143,7 @@ B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */, + 5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, B50C3EFE23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5C7959925D7D8B300BDACC1 /* ListReader.swift in Sources */, @@ -2148,6 +2161,7 @@ B5944EF625E269F9001D1D81 /* ObjectPublisher+Reactive.swift in Sources */, B50392F91C478FF3009900CA /* NSManagedObject+Transaction.swift in Sources */, B5202CFA1C04688100DED140 /* NSFetchedResultsController+Convenience.swift in Sources */, + 5F1750802FFE35AF00BA577F /* Internals.Mutext.swift in Sources */, B5E84F251AFF84860064E85B /* ObjectObserver.swift in Sources */, B5E84F2F1AFF849C0064E85B /* Internals.NotificationObserver.swift in Sources */, B5F1DA8D1B9AA97D007C5CBB /* ImportableObject.swift in Sources */, @@ -2348,6 +2362,7 @@ B5831F432212700400D8604C /* Where.Expression.swift in Sources */, B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B5FE4DA81C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, + 5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, B50C3EFF23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5C7959A25D7D8B300BDACC1 /* ListReader.swift in Sources */, @@ -2365,6 +2380,7 @@ 82BA18D51C4BBD7100A0916E /* NSManagedObjectContext+Setup.swift in Sources */, B5E41EC11EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5A1DAC91F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, + 5F17507F2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */, B5FE4DA31C8481E100FA6A91 /* StorageInterface.swift in Sources */, B5C7959025D7D18000BDACC1 /* ListState.swift in Sources */, B56923C51EB823B4007C4DC9 /* NSEntityDescription+Migration.swift in Sources */, @@ -2600,6 +2616,7 @@ B52DD1AD1BE1F93900949AFE /* Where.swift in Sources */, B5F8496F234898240029D57B /* ListSnapshot.swift in Sources */, B5831F452212700500D8604C /* Where.Expression.swift in Sources */, + 5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, B5B866DE25E9012F00335476 /* ListPublisher+Reactive.swift in Sources */, B514EF1423A8DB1E0093DBA4 /* DiffableDataSource.BaseAdapter.swift in Sources */, B51260961E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, @@ -2617,6 +2634,7 @@ B5220E131D1305ED009BC71E /* SectionBy.swift in Sources */, B5C7959225D7D18000BDACC1 /* ListState.swift in Sources */, B5E41EC31EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, + 5F1750812FFE35AF00BA577F /* Internals.Mutext.swift in Sources */, B5A1DACB1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, B52DD1B81BE1F94000949AFE /* DataStack+Migration.swift in Sources */, B5D8CA792346EAEF0055D7D1 /* DataStack+DataSources.swift in Sources */, @@ -2817,6 +2835,7 @@ B5831F442212700500D8604C /* Where.Expression.swift in Sources */, B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */, + 5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, B5C7959B25D7D8B300BDACC1 /* ListReader.swift in Sources */, B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */, B51260951E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, @@ -2834,6 +2853,7 @@ B56321B31BD6521C006C9394 /* NSManagedObjectContext+Setup.swift in Sources */, B5E41EC21EA9BB37006240F0 /* DynamicSchema+Convenience.swift in Sources */, B5C7959125D7D18000BDACC1 /* ListState.swift in Sources */, + 5F17507E2FFE35AF00BA577F /* Internals.Mutext.swift in Sources */, B5A1DACA1F111BFA003CF369 /* KeyPath+Querying.swift in Sources */, B56321AE1BD6521C006C9394 /* Internals.NotificationObserver.swift in Sources */, B56321931BD65216006C9394 /* DataStack+Querying.swift in Sources */, @@ -3027,7 +3047,7 @@ SWIFT_COMPILATION_MODE = singlefile; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 17.0; VERSIONING_SYSTEM = "apple-generic"; @@ -3092,7 +3112,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; diff --git a/CoreStoreTests/BaseTestCase.swift b/CoreStoreTests/BaseTestCase.swift index 13c3968f..cb8a7d80 100644 --- a/CoreStoreTests/BaseTestCase.swift +++ b/CoreStoreTests/BaseTestCase.swift @@ -24,11 +24,11 @@ // import XCTest -import os @testable import CoreStore + #if !SWIFT_PACKAGE extension Bundle { @@ -196,7 +196,7 @@ final class TestLogger: CoreStoreLogger { init(_ expectations: [Expectation: XCTestExpectation]) { - self.expectations = .init(initialState: expectations) + self.expectations = .init(expectations) } @@ -236,7 +236,7 @@ final class TestLogger: CoreStoreLogger { // MARK: Private - private let expectations: OSAllocatedUnfairLock<[Expectation: XCTestExpectation]> + private let expectations: Internals.Mutex<[Expectation: XCTestExpectation]> private func fulfill(_ expectation: Expectation) { diff --git a/CoreStoreTests/DynamicModelTests.swift b/CoreStoreTests/DynamicModelTests.swift index 0bc9e6a2..62716749 100644 --- a/CoreStoreTests/DynamicModelTests.swift +++ b/CoreStoreTests/DynamicModelTests.swift @@ -201,6 +201,7 @@ class Person: CoreStoreObject { class DynamicModelTests: BaseTestDataTestCase { + @MainActor @objc dynamic func test_ThatDynamicModels_CanBeDeclaredCorrectly() { diff --git a/CoreStoreTests/FetchTests.swift b/CoreStoreTests/FetchTests.swift index 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/ImportTests.swift b/CoreStoreTests/ImportTests.swift index 3111c67c..4c98ce2b 100644 --- a/CoreStoreTests/ImportTests.swift +++ b/CoreStoreTests/ImportTests.swift @@ -72,6 +72,7 @@ class ImportTests: BaseTestDataTestCase { XCTAssertEqual(Double(MAXFLOAT).cs_toQueryableNativeType(), NSNumber(value: Double(MAXFLOAT))) } + @MainActor @objc dynamic func test_ThatImportObject_CanSkipImport() { @@ -107,6 +108,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportObject_CanThrowError() { @@ -160,6 +162,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportObject_CanImportCorrectly() { @@ -220,6 +223,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportObjects_CanSkipImport() { @@ -275,6 +279,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportObjects_CanThrowError() { @@ -339,6 +344,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportObjects_CanImportCorrectly() { @@ -397,6 +403,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportUniqueObject_CanSkipImport() { @@ -466,6 +473,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportUniqueObjects_ImportsLastOfImportSourcesWithSameIDs() { @@ -523,7 +531,8 @@ class ImportTests: BaseTestDataTestCase { } } } - + + @MainActor @objc dynamic func test_ThatImportUniqueObjects_MaintainsOrderOfInputSourceArray() { @@ -585,6 +594,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportUniqueObject_CanThrowError() { @@ -680,7 +690,8 @@ class ImportTests: BaseTestDataTestCase { } } } - + + @MainActor @objc dynamic func test_ThatImportUniqueObject_CanImportCorrectly() { @@ -757,7 +768,8 @@ class ImportTests: BaseTestDataTestCase { } } } - + + @MainActor @objc dynamic func test_ThatImportUniqueObjects_CanSkipImport() { @@ -815,7 +827,8 @@ class ImportTests: BaseTestDataTestCase { } } } - + + @MainActor @objc dynamic func test_ThatImportUniqueObjects_CanThrowError() { @@ -977,6 +990,7 @@ class ImportTests: BaseTestDataTestCase { } } + @MainActor @objc dynamic func test_ThatImportUniqueObjects_CanImportCorrectly() { @@ -1065,7 +1079,7 @@ extension TestEntity1: ImportableUniqueObject { // MARK: ImportableObject - typealias ImportSource = [String: Any] + typealias ImportSource = [String: any Sendable] static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool { diff --git a/CoreStoreTests/ListObserverTests.swift b/CoreStoreTests/ListObserverTests.swift index 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/ObjectObserverTests.swift b/CoreStoreTests/ObjectObserverTests.swift index 2e09cd13..c2383410 100644 --- a/CoreStoreTests/ObjectObserverTests.swift +++ b/CoreStoreTests/ObjectObserverTests.swift @@ -24,7 +24,6 @@ // import XCTest -import os @testable import CoreStore @@ -56,7 +55,8 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - let events: OSAllocatedUnfairLock = .init(initialState: 0) + let events: Internals.Mutex = .init(0) + let persistentID = object.persistentID() _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"), @@ -68,8 +68,8 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(events, 0) XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - ["object": object] as NSDictionary + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID ) defer { @@ -89,16 +89,17 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(events, 1) XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - [ - "object": object, - "changedPersistentKeys": Set( - [ - #keyPath(TestEntity1.testNumber), - #keyPath(TestEntity1.testString) - ] - ) - ] as NSDictionary + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID + ) + XCTAssertEqual( + note.userInfo?["changedPersistentKeys"] as? Set, + Set( + [ + #keyPath(TestEntity1.testNumber), + #keyPath(TestEntity1.testString) + ] + ) ) let object = note.userInfo?["object"] as? TestEntity1 XCTAssertEqual(object?.testNumber, NSNumber(value: 10)) @@ -116,7 +117,7 @@ class ObjectObserverTests: BaseTestDataTestCase { stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() @@ -162,7 +163,8 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(monitor.object, object) XCTAssertFalse(monitor.isObjectDeleted) - let events: OSAllocatedUnfairLock = .init(initialState: 0) + let events: Internals.Mutex = .init(0) + let persistentID = object.persistentID() _ = self.expectation( forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"), @@ -174,8 +176,8 @@ class ObjectObserverTests: BaseTestDataTestCase { XCTAssertEqual(events, 0) XCTAssertEqual( - ((note.userInfo as NSDictionary?) ?? [:]), - ["object": object] as NSDictionary + (note.userInfo?["object"] as? TestEntity1)?.persistentID(), + persistentID ) defer { @@ -189,7 +191,7 @@ class ObjectObserverTests: BaseTestDataTestCase { stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() diff --git a/CoreStoreTests/ObjectPublisherTests.swift b/CoreStoreTests/ObjectPublisherTests.swift index 1bf8a0b5..98191d38 100644 --- a/CoreStoreTests/ObjectPublisherTests.swift +++ b/CoreStoreTests/ObjectPublisherTests.swift @@ -63,10 +63,11 @@ class ObjectPublisherTests: BaseTestDataTestCase { } let saveExpectation = self.expectation(description: "save") + let persistentID = object.persistentID() stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() @@ -123,10 +124,11 @@ class ObjectPublisherTests: BaseTestDataTestCase { } let saveExpectation = self.expectation(description: "save") + let persistentID = object.persistentID() stack.perform( asynchronous: { (transaction) -> Bool in - guard let object = transaction.edit(object) else { + guard let object = transaction.edit(persistentID) else { XCTFail() try transaction.cancel() diff --git a/CoreStoreTests/QueryTests.swift b/CoreStoreTests/QueryTests.swift index 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 7b56d127..0530d8a0 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -27,6 +27,7 @@ import CoreData import XCTest @testable +@_spi(Internals) import CoreStore diff --git a/CoreStoreTests/TransactionTests.swift b/CoreStoreTests/TransactionTests.swift index 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..1f0df5e3 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -25,8 +25,7 @@ B5931B4825124940007DA58E /* Advanced.EvolutionDemo.V4.FromV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4725124940007DA58E /* Advanced.EvolutionDemo.V4.FromV3.swift */; }; B5931B4A25124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4925124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift */; }; B5931B4C251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5931B4B251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift */; }; - B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* AppDelegate.swift */; }; - B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */; }; + B5A3911D24E5429200E7E8BD /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3911C24E5429200E7E8BD /* DemoApp.swift */; }; B5A3912124E5429200E7E8BD /* Menu.MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3912024E5429200E7E8BD /* Menu.MainView.swift */; }; B5A3912324E5429300E7E8BD /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912224E5429300E7E8BD /* Images.xcassets */; }; B5A3912624E5429300E7E8BD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */; }; @@ -44,7 +43,6 @@ B5A3917524E6990200E7E8BD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917424E6990200E7E8BD /* MapKit.framework */; }; B5A3917724E6990700E7E8BD /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917624E6990700E7E8BD /* CoreLocation.framework */; }; B5A3917924E6991600E7E8BD /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5A3917824E6991600E7E8BD /* SwiftUI.framework */; }; - B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917B24E6A76C00E7E8BD /* LazyView.swift */; }; B5A3917E24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */; }; B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3917F24E787D900E7E8BD /* InstructionsView.swift */; }; B5A3918324E7A21800E7E8BD /* Modern.TimeZonesDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */; }; @@ -136,8 +134,7 @@ B5931B4925124993007DA58E /* Advanced.EvolutionDemo.V3.FromV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.EvolutionDemo.V3.FromV4.swift; sourceTree = ""; }; B5931B4B251249EE007DA58E /* Advanced.EvolutionDemo.V2.FromV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.EvolutionDemo.V2.FromV3.swift; sourceTree = ""; }; B5A3911924E5429200E7E8BD /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - B5A3911C24E5429200E7E8BD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + B5A3911C24E5429200E7E8BD /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; B5A3912024E5429200E7E8BD /* Menu.MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.MainView.swift; sourceTree = ""; }; B5A3912224E5429300E7E8BD /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; B5A3912524E5429300E7E8BD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -159,7 +156,6 @@ B5A3917424E6990200E7E8BD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; B5A3917624E6990700E7E8BD /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; }; B5A3917824E6991600E7E8BD /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.6.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; - B5A3917B24E6A76C00E7E8BD /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; B5A3917D24E7728400E7E8BD /* Modern.PlacemarksDemo.Geocoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.PlacemarksDemo.Geocoder.swift; sourceTree = ""; }; B5A3917F24E787D900E7E8BD /* InstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsView.swift; sourceTree = ""; }; B5A3918224E7A21800E7E8BD /* Modern.TimeZonesDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modern.TimeZonesDemo.swift; sourceTree = ""; }; @@ -257,8 +253,7 @@ B5A3911B24E5429200E7E8BD /* Sources */ = { isa = PBXGroup; children = ( - B5A3911C24E5429200E7E8BD /* AppDelegate.swift */, - B5A3911E24E5429200E7E8BD /* SceneDelegate.swift */, + B5A3911C24E5429200E7E8BD /* DemoApp.swift */, B5A3915524E6858A00E7E8BD /* Demos */, B5A3917A24E6A75F00E7E8BD /* Helpers */, ); @@ -371,7 +366,6 @@ B5A3917A24E6A75F00E7E8BD /* Helpers */ = { isa = PBXGroup; children = ( - B5A3917B24E6A76C00E7E8BD /* LazyView.swift */, B5A3917F24E787D900E7E8BD /* InstructionsView.swift */, B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */, B5A3915424E6857F00E7E8BD /* Menu */, @@ -668,14 +662,12 @@ buildActionMask = 2147483647; files = ( B5A543F624FBF13A000DC5E3 /* Advanced.swift in Sources */, - B5A3911D24E5429200E7E8BD /* AppDelegate.swift in Sources */, + B5A3911D24E5429200E7E8BD /* DemoApp.swift in Sources */, B5A3915924E685EC00E7E8BD /* Classic.swift in Sources */, B5E32C9024FA41F9003F46AD /* ImageDownloader.swift in Sources */, B5A3918024E787D900E7E8BD /* InstructionsView.swift in Sources */, - B5A3917C24E6A76C00E7E8BD /* LazyView.swift in Sources */, B5A3913424E6170500E7E8BD /* Menu.swift in Sources */, B5A3915B24E685FE00E7E8BD /* Modern.swift in Sources */, - B5A3911F24E5429200E7E8BD /* SceneDelegate.swift in Sources */, B5A543FB2504840E000DC5E3 /* Advanced.EvolutionDemo.swift in Sources */, B5A543D724FB7478000DC5E3 /* Classic.ColorsDemo.swift in Sources */, B5A543DB24FB7513000DC5E3 /* Classic.ColorsDemo.xcdatamodeld in Sources */, @@ -801,7 +793,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; ONLY_ACTIVE_ARCH = YES; }; name = Debug; @@ -837,7 +829,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; }; name = Release; }; @@ -897,6 +889,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -909,7 +902,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -964,6 +957,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -975,7 +969,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; diff --git a/Demo/Info.plist b/Demo/Info.plist index 49e8f4a1..5b5ff9d4 100644 --- a/Demo/Info.plist +++ b/Demo/Info.plist @@ -20,29 +20,8 @@ 1 LSRequiresIPhoneOS - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - UILaunchStoryboardName LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - UIStatusBarTintParameters UINavigationBar diff --git a/Demo/Sources/AppDelegate.swift b/Demo/Sources/AppDelegate.swift deleted file mode 100644 index 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..2efc9e20 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift @@ -3,27 +3,29 @@ // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. import CoreStore -import Combine +import Observation // MARK: - Advanced.EvolutionDemo extension Advanced.EvolutionDemo { - + // MARK: - Advanced.EvolutionDemo.CreaturesDataSource - + /** A type-erasing adapter to support different `ListPublisher` types */ - final class CreaturesDataSource: ObservableObject { - + @MainActor + @Observable + final class CreaturesDataSource { + // MARK: Internal - - init( + + init( listPublisher: ListPublisher, dataStack: DataStack ) { - + self.numberOfItems = { listPublisher.snapshot.numberOfItems } @@ -31,13 +33,13 @@ extension Advanced.EvolutionDemo { listPublisher.snapshot[index].object?.description } self.addItems = { count in - + dataStack.perform( asynchronous: { transaction in - + let nextDNACode = try transaction.fetchCount(From()) for offset in 0 ..< count { - + let object = transaction.create(Into()) object.dnaCode = .init(nextDNACode + offset) object.mutate(in: transaction) @@ -47,11 +49,11 @@ extension Advanced.EvolutionDemo { ) } self.mutateItemAtIndex = { index in - + let object = listPublisher.snapshot[index] dataStack.perform( asynchronous: { transaction in - + object .asEditable(in: transaction)? .mutate(in: transaction) @@ -60,109 +62,68 @@ extension Advanced.EvolutionDemo { ) } self.deleteAllItems = { - + dataStack.perform( asynchronous: { transaction in - + try transaction.deleteAll(From()) }, completion: { _ in } ) } - listPublisher.addObserver(self) { [weak self] (listPublisher) in - - self?.objectWillChange.send() + listPublisher.addObserver(self) { [weak self] _ in + + Task { @MainActor in + self?.refreshID += 1 + } } } - - init( - listPublisher: ListPublisher, - dataStack: DataStack - ) { - - self.numberOfItems = { - listPublisher.snapshot.numberOfItems - } - self.itemDescriptionAtIndex = { index in - listPublisher.snapshot[index].object?.description - } - self.addItems = { count in - - dataStack.perform( - asynchronous: { transaction in - - let nextDNACode = try transaction.fetchCount(From()) - for offset in 0 ..< count { - - let object = transaction.create(Into()) - object.dnaCode = .init(nextDNACode + offset) - object.mutate(in: transaction) - } - }, - completion: { _ in } - ) - } - self.mutateItemAtIndex = { index in - - let object = listPublisher.snapshot[index] - dataStack.perform( - asynchronous: { transaction in - - object - .asEditable(in: transaction)? - .mutate(in: transaction) - }, - completion: { _ in } - ) - } - self.deleteAllItems = { - - dataStack.perform( - asynchronous: { transaction in - - try transaction.deleteAll(From()) - }, - completion: { _ in } - ) - } - listPublisher.addObserver(self) { [weak self] (listPublisher) in - - self?.objectWillChange.send() - } - } - + func numberOfCreatures() -> Int { - + + _ = self.refreshID return self.numberOfItems() } - + func creatureDescription(at index: Int) -> String? { - + + _ = self.refreshID return self.itemDescriptionAtIndex(index) } - + func mutate(at index: Int) { - + self.mutateItemAtIndex(index) } - + func add(count: Int) { - + self.addItems(count) } - + func clear() { - + self.deleteAllItems() } - - + + // MARK: Private - + + private var refreshID: Int = 0 + + @ObservationIgnored private let numberOfItems: () -> Int + + @ObservationIgnored private let itemDescriptionAtIndex: (Int) -> String? + + @ObservationIgnored private let mutateItemAtIndex: (Int) -> Void + + @ObservationIgnored private let addItems: (Int) -> Void + + @ObservationIgnored private let deleteAllItems: () -> Void } } diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ItemView.swift index 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..6c906c1f 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift @@ -4,7 +4,7 @@ import CoreStore import Foundation -import Combine +import Observation // MARK: - Advanced.EvolutionDemo @@ -13,8 +13,10 @@ extension Advanced.EvolutionDemo { // MARK: - Advanced.EvolutionDemo.Migrator - final class Migrator: ObservableObject { - + @MainActor + @Observable + final class Migrator { + /** ⭐️ Sample 1: Creating a complex `DataStack` that contains all schema histories. The `exactCurrentModelVersion` will specify the target version (if required), and `migrationChain` will provide the upgrade/downgrade progressive migration path. */ @@ -22,7 +24,7 @@ extension Advanced.EvolutionDemo { exactCurrentModelVersion: ModelVersion?, migrationChain: MigrationChain ) -> DataStack { - + let xcodeV1ToV2ModelSchema = XcodeDataModelSchema.from( modelName: "Advanced.EvolutionDemo.V1", bundle: Bundle(for: Advanced.EvolutionDemo.V1.Creature.self) @@ -49,12 +51,12 @@ extension Advanced.EvolutionDemo { ) ) } - + /** ⭐️ Sample 2: Creating a complex `SQLiteStore` that contains all schema mappings for both upgrade and downgrade cases. */ private func accessSQLiteStore() -> SQLiteStore { - + let upgradeMappings: [SchemaMappingProvider] = [ Advanced.EvolutionDemo.V2.FromV1.mapping, Advanced.EvolutionDemo.V3.FromV2.mapping, @@ -72,15 +74,15 @@ extension Advanced.EvolutionDemo { localStorageOptions: [] ) } - + /** ⭐️ Sample 3: Find the model version used by an existing `SQLiteStore`, or just return the latest version if the store is not created yet. */ private func findCurrentVersion() -> ModelVersion { - + let allVersions = Advanced.EvolutionDemo.GeologicalPeriod.allCases - .map({ $0.version }) - + .map(\.version) + // Since we are only interested in finding current version, we'll assume an upgrading `MigrationChain` let dataStack = self.createDataStack( exactCurrentModelVersion: nil, @@ -89,42 +91,36 @@ extension Advanced.EvolutionDemo { let migrations = try! dataStack.requiredMigrationsForStorage( self.accessSQLiteStore() ) - + // If no migrations are needed, it means either the store is not created yet, or the store is already at the latest model version. In either case, we already know that the store will use the latest version return migrations.first?.sourceVersion - ?? allVersions.last! + ?? allVersions.last! } - - + + // MARK: Internal - + var currentPeriod: Advanced.EvolutionDemo.GeologicalPeriod = Advanced.EvolutionDemo.GeologicalPeriod.allCases.last! { - + didSet { - + self.selectModelVersion(self.currentPeriod) } } - + private(set) var current: ( period: Advanced.EvolutionDemo.GeologicalPeriod, dataStack: DataStack, dataSource: Advanced.EvolutionDemo.CreaturesDataSource - )? { - - willSet { - - self.objectWillChange.send() - } - } - + )? + private(set) var isBusy: Bool = false - + private(set) var progress: Progress? - - + + init() { - + self.synchronizeCurrentVersion() } @@ -132,11 +128,11 @@ extension Advanced.EvolutionDemo { // MARK: Private private func synchronizeCurrentVersion() { - + guard let currentPeriod = Advanced.EvolutionDemo.GeologicalPeriod(rawValue: self.findCurrentVersion()) else { - + self.selectModelVersion(self.currentPeriod) return } @@ -144,106 +140,106 @@ extension Advanced.EvolutionDemo { } private func selectModelVersion(_ period: Advanced.EvolutionDemo.GeologicalPeriod) { - + let currentPeriod = self.current?.period guard period != currentPeriod else { return } - - self.objectWillChange.send() - + self.isBusy = true // explicitly trigger `NSPersistentStore` cleanup by deallocating the `DataStack` self.current = nil - + let migrationChain: MigrationChain switch (currentPeriod?.version, period.version) { - + case (nil, let newVersion): migrationChain = [newVersion] - + case (let currentVersion?, let newVersion): let upgradeMigrationChain = Advanced.EvolutionDemo.GeologicalPeriod.allCases - .map({ $0.version }) + .map(\.version) let currentVersionIndex = upgradeMigrationChain.firstIndex(of: currentVersion)! let newVersionIndex = upgradeMigrationChain.firstIndex(of: newVersion)! - + migrationChain = MigrationChain( currentVersionIndex > newVersionIndex - ? upgradeMigrationChain.reversed() - : upgradeMigrationChain + ? upgradeMigrationChain.reversed() + : upgradeMigrationChain ) } let dataStack = self.createDataStack( exactCurrentModelVersion: period.version, migrationChain: migrationChain ) - - let completion = { [weak self] () -> Void in - - guard let self = self else { - - return - } - self.objectWillChange.send() - defer { - - self.isBusy = false - } - self.current = ( - period: period, - dataStack: dataStack, - dataSource: period.creatureType.dataSource(in: dataStack) - ) - self.currentPeriod = period - } - + self.progress = dataStack.addStorage( self.accessSQLiteStore(), completion: { [weak self] result in - - guard let self = self else { - + + guard let self else { + return } guard case .success = result else { - - self.objectWillChange.send() + self.isBusy = false return } if self.progress == nil { - - self.spawnCreatures(in: dataStack, period: period, completion: completion) + + self.spawnCreatures(in: dataStack, period: period) { [weak self] in + self?.completeSelection(period: period, dataStack: dataStack) + } } else { - - completion() + + self.completeSelection(period: period, dataStack: dataStack) } } ) } - + + private func completeSelection( + period: Advanced.EvolutionDemo.GeologicalPeriod, + dataStack: DataStack + ) { + defer { + + self.isBusy = false + } + self.current = ( + period: period, + dataStack: dataStack, + dataSource: period.creatureType.dataSource(in: dataStack) + ) + self.currentPeriod = period + self.progress = nil + } + private func spawnCreatures( in dataStack: DataStack, period: Advanced.EvolutionDemo.GeologicalPeriod, - completion: @escaping () -> Void + completion: @escaping @MainActor @Sendable () -> Void ) { - + dataStack.perform( - asynchronous: { (transaction) in - + asynchronous: { transaction in + let creatureType = period.creatureType for dnaCode in try creatureType.count(in: transaction) ..< 10000 { - + let object = creatureType.create(in: transaction) object.dnaCode = Int64(dnaCode) object.mutate(in: transaction) } }, - completion: { _ in completion() } + completion: { _ in + + completion() + } ) } diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.ProgressView.swift index 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.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.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..eb81d407 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -10,7 +10,7 @@ import SwiftUI extension Modern.ColorsDemo { // MARK: - Modern.ColorsDemo.MainView - + struct MainView: View { // MARK: Internal @@ -20,7 +20,8 @@ extension Modern.ColorsDemo { _ listPublisher: ListPublisher, _ onPaletteTapped: @escaping (ObjectPublisher) -> Void ) -> ListView, - detailView: @escaping (ObjectPublisher) -> DetailView) { + detailView: @escaping (ObjectPublisher) -> DetailView + ) { self.listView = listView self.detailView = detailView @@ -30,42 +31,51 @@ extension Modern.ColorsDemo { // MARK: View var body: some View { - return VStack(spacing: 0) { - self.listView(self.$palettes, { self.selectedPalette = $0 }) - .navigationBarTitle( - Text("Colors (\(self.palettes.count) objects)") - ) - .frame(minHeight: 0, maxHeight: .infinity) - self.selectedPalette.map { + + VStack(spacing: 0) { + + self.listView( + self.$palettes, + { + self.selectedPalette = $0 + } + ) + .frame(minHeight: 0, maxHeight: .infinity) + + if let selectedPalette = self.selectedPalette { - self.detailView($0) - .edgesIgnoringSafeArea(.all) + self.detailView(selectedPalette) + .ignoresSafeArea() .frame(minHeight: 0, maxHeight: .infinity) } } - .navigationBarItems( - leading: HStack { + .navigationTitle("Colors (\(self.palettes.count) objects)") + .toolbar { + + ToolbarItemGroup(placement: .topBarLeading) { EditButton() - Button( - action: { self.clearColors() }, - label: { Text("Clear") } - ) - }, - trailing: HStack { - Button( - action: { self.changeFilter() }, - label: { Text(self.filter.rawValue) } - ) - Button( - action: { self.shuffleColors() }, - label: { Text("Shuffle") } - ) - Button( - action: { self.addColor() }, - label: { Text("Add") } - ) + Button("Clear") { + + self.clearColors() + } } - ) + + ToolbarItemGroup(placement: .topBarTrailing) { + + Button(self.filter.rawValue) { + + self.changeFilter() + } + Button("Shuffle") { + + self.shuffleColors() + } + Button("Add") { + + self.addColor() + } + } + } } @@ -99,7 +109,7 @@ extension Modern.ColorsDemo { Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + try transaction.deleteAll(From()) }, sourceIdentifier: TransactionSource.clear, @@ -108,10 +118,10 @@ extension Modern.ColorsDemo { } private func addColor() { - + Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + _ = transaction.create(Into()) }, sourceIdentifier: TransactionSource.add, @@ -120,12 +130,12 @@ extension Modern.ColorsDemo { } private func shuffleColors() { - + Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + for palette in try transaction.fetchAll(From()) { - + palette.setRandomHue() } }, @@ -136,42 +146,38 @@ extension Modern.ColorsDemo { } } -#if DEBUG -struct _Demo_Modern_ColorsDemo_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - let minimumSamples = 10 - try! Modern.ColorsDemo.dataStack.perform( - synchronous: { transaction in +// MARK: - Preview - let missing = minimumSamples - - (try transaction.fetchCount(From())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return } - ) - return Modern.ColorsDemo.MainView( - listView: { listPublisher, onPaletteTapped in - Modern.ColorsDemo.SwiftUI.ListView( - listPublisher: listPublisher, - onPaletteTapped: onPaletteTapped - ) - }, - detailView: { objectPublisher in - Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) + for _ in 0..()) + palette.setRandomHue() } - ) - } + } + ) + return Modern.ColorsDemo.MainView( + listView: { listPublisher, onPaletteTapped in + + Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: listPublisher, + onPaletteTapped: onPaletteTapped + ) + }, + detailView: { objectPublisher in + + Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) + } + ) } - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift index b83e1b8d..5e74d66f 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift @@ -2,7 +2,6 @@ // Demo // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. -import Combine import CoreStore import SwiftUI @@ -31,9 +30,9 @@ extension Modern.ColorsDemo.SwiftUI { @Binding private var brightness: Float - + init(_ palette: ObjectPublisher) { - + self._palette = .init(palette) self._hue = Binding( get: { palette.hue ?? 0 }, @@ -78,7 +77,7 @@ extension Modern.ColorsDemo.SwiftUI { } ) } - + // MARK: View @@ -87,12 +86,17 @@ extension Modern.ColorsDemo.SwiftUI { if let palette = self.palette { ZStack(alignment: .center) { + Color(palette.$color) + ZStack { + RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color.white) .shadow(color: Color(.sRGB, white: 0.5, opacity: 0.3), radius: 2, x: 1, y: 1) + VStack(alignment: .leading, spacing: 10) { + HStack { Text("H: \(Int(palette.$hue * 359))°") .frame(width: 80) @@ -102,6 +106,7 @@ extension Modern.ColorsDemo.SwiftUI { step: 1 / 359 ) } + HStack { Text("S: \(Int(palette.$saturation * 100))%") .frame(width: 80) @@ -111,6 +116,7 @@ extension Modern.ColorsDemo.SwiftUI { step: 1 / 100 ) } + HStack { Text("B: \(Int(palette.$brightness * 100))%") .frame(width: 80) @@ -131,30 +137,3 @@ extension Modern.ColorsDemo.SwiftUI { } } } - -#if DEBUG - -struct _Demo_Modern_ColorsDemo_SwiftUI_DetailView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - try! Modern.ColorsDemo.dataStack.perform( - synchronous: { transaction in - - guard (try transaction.fetchCount(From())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Modern.ColorsDemo.SwiftUI.DetailView( - Modern.ColorsDemo.palettesPublisher.snapshot.first! - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift index 850813fa..ed2e0442 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ItemView.swift @@ -34,41 +34,15 @@ extension Modern.ColorsDemo.SwiftUI { if let palette = self.palette { - Color(palette.$color).overlay( - Text(palette.$colorText) - .foregroundColor(palette.$brightness > 0.6 ? .black : .white) - .padding(), - alignment: .leading - ) - .animation(.default, value: palette) + Color(palette.$color) + .overlay( + Text(palette.$colorText) + .foregroundColor(palette.$brightness > 0.6 ? .black : .white) + .padding(), + alignment: .leading + ) + .animation(.default, value: palette) } } } } - -#if DEBUG - -struct _Demo_Modern_ColorsDemo_SwiftUI_ItemView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - try! Modern.ColorsDemo.dataStack.perform( - synchronous: { transaction in - - guard (try transaction.fetchCount(From())) <= 0 else { - return - } - let palette = transaction.create(Into()) - palette.setRandomHue() - } - ) - - return Modern.ColorsDemo.SwiftUI.ItemView( - Modern.ColorsDemo.palettesPublisher.snapshot.first! - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift index e11a4cfe..52149d01 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift @@ -10,7 +10,7 @@ import SwiftUI extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.ListView - + struct ListView: View { /** @@ -40,7 +40,7 @@ extension Modern.ColorsDemo.SwiftUI { ForEach(sectionIn: self.palettes) { section in - Section(header: Text(section.sectionID)) { + Section(section.sectionID) { ForEach(objectIn: section) { palette in @@ -64,8 +64,7 @@ extension Modern.ColorsDemo.SwiftUI { } } // .animation(.default) // breaks layout - .listStyle(PlainListStyle()) - .edgesIgnoringSafeArea([]) + .listStyle(.plain) } @@ -81,7 +80,7 @@ extension Modern.ColorsDemo.SwiftUI { ) Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - + transaction.delete(objectIDs: objectIDsToDelete) }, completion: { _ in } @@ -90,35 +89,29 @@ extension Modern.ColorsDemo.SwiftUI { } } -#if DEBUG -struct _Demo_Modern_ColorsDemo_SwiftUI_ListView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - let minimumSamples = 10 - try! Modern.ColorsDemo.dataStack.perform( - synchronous: { transaction in +// MARK: - Preview - let missing = minimumSamples - - (try transaction.fetchCount(From())) - guard missing > 0 else { - return - } - for _ in 0..()) - palette.setRandomHue() - } +#Preview { + + let minimumSamples = 10 + try! Modern.ColorsDemo.dataStack.perform( + synchronous: { transaction in + + let missing = minimumSamples + - (try transaction.fetchCount(From())) + guard missing > 0 else { + return } - ) - return Modern.ColorsDemo.SwiftUI.ListView( - listPublisher: Modern.ColorsDemo.palettesPublisher, - onPaletteTapped: { _ in } - ) - } + for _ in 0..()) + palette.setRandomHue() + } + } + ) + return Modern.ColorsDemo.SwiftUI.ListView( + listPublisher: Modern.ColorsDemo.palettesPublisher, + onPaletteTapped: { _ in } + ) } - -#endif diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailView.swift index 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..afb1ed10 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift @@ -49,7 +49,7 @@ extension Modern.ColorsDemo.UIKit { } } - deinit { + isolated deinit { self.palette.removeObserver(self) } @@ -88,14 +88,17 @@ extension Modern.ColorsDemo.UIKit { // MARK: ObjectObserver - func objectMonitor( + nonisolated func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: Modern.ColorsDemo.Palette, + didUpdateObject object: sending Modern.ColorsDemo.Palette, changedPersistentKeys: Set, sourceIdentifier: Any? ) { - self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + MainActor.assumeIsolated { + + self.reloadPaletteInfo(object, changedKeys: changedPersistentKeys) + } } @@ -245,6 +248,7 @@ extension Modern.ColorsDemo.UIKit { private let saturationSlider: UISlider = .init() private let brightnessSlider: UISlider = .init() + @MainActor @objc private dynamic func hueSliderValueDidChange(_ sender: UISlider) { @@ -259,6 +263,7 @@ extension Modern.ColorsDemo.UIKit { ) } + @MainActor @objc private dynamic func saturationSliderValueDidChange(_ sender: UISlider) { @@ -273,6 +278,7 @@ extension Modern.ColorsDemo.UIKit { ) } + @MainActor @objc private dynamic func brightnessSliderValueDidChange(_ sender: UISlider) { diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListView.swift index 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..e82b46f7 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift @@ -42,13 +42,13 @@ extension Modern.ColorsDemo.UIKit { switch transactionSource as? Modern.ColorsDemo.TransactionSource { case .add, - .delete, - .shuffle, - .clear: + .delete, + .shuffle, + .clear: dataSource.apply(listPublisher.snapshot, animatingDifferences: true) case nil, - .refetch: + .refetch: dataSource.apply(listPublisher.snapshot, animatingDifferences: false) } } @@ -57,22 +57,22 @@ extension Modern.ColorsDemo.UIKit { /** ⭐️ Sample 3: We can end monitoring updates anytime. `removeObserver()` was called here for illustration purposes only. `ListPublisher`s safely remove deallocated observers automatically. */ - deinit { + isolated deinit { self.listPublisher.removeObserver(self) } - + /** ⭐️ Sample 4: This is the custom `DiffableDataSource.TableViewAdapter` subclass we wrote that enabled swipe-to-delete gestures and section index titles on the `UITableView`. */ final class CustomDataSource: DiffableDataSource.TableViewAdapter { // MARK: UITableViewDataSource - + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - + switch editingStyle { - + case .delete: guard let itemID = self.itemID(for: indexPath) else { @@ -80,13 +80,13 @@ extension Modern.ColorsDemo.UIKit { } self.dataStack.perform( asynchronous: { (transaction) in - + transaction.delete(objectIDs: [itemID]) }, sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete, completion: { _ in } ) - + default: break } @@ -116,10 +116,10 @@ extension Modern.ColorsDemo.UIKit { super.init(style: .plain) } - + // MARK: UIViewController - + override func viewDidLoad() { super.viewDidLoad() @@ -128,7 +128,7 @@ extension Modern.ColorsDemo.UIKit { Modern.ColorsDemo.UIKit.ItemCell.self, forCellReuseIdentifier: Modern.ColorsDemo.UIKit.ItemCell.reuseIdentifier ) - + self.startObservingList() } diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.swift index 3becb11e..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 { 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..7d5dfff9 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift @@ -3,7 +3,6 @@ // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. import CoreLocation -import Combine import CoreStore import Foundation import MapKit @@ -14,7 +13,7 @@ import SwiftUI extension Modern.PlacemarksDemo { // MARK: - Modern.PlacemarksDemo.MainView - + struct MainView: View { /** @@ -38,7 +37,7 @@ extension Modern.PlacemarksDemo { - Important: `perform(synchronous:)` was used here for illustration purposes. In practice, `perform(asynchronous:completion:)` is the preferred transaction type as synchronous transactions are very likely to cause deadlocks. */ private func demoSynchronousTransaction() { - + _ = try? Modern.PlacemarksDemo.dataStack.perform( synchronous: { (transaction) in @@ -68,41 +67,12 @@ extension Modern.PlacemarksDemo { print("Commit failed: \(error as Any)") } } - + // MARK: Internal - + @ObjectState(Modern.PlacemarksDemo.placePublisher) var place: ObjectSnapshot? - init() { - - self.sinkCancellable = self.$place?.reactive.snapshot().sink( - receiveCompletion: { _ in - - // Deleted, do nothing - }, - receiveValue: { [self] (snapshot) in - - guard let snapshot = snapshot else { - - return - } - self.geocoder.geocode(place: snapshot) { (title, subtitle) in - - guard self.place == snapshot else { - - return - } - self.demoUnsafeTransaction( - title: title, - subtitle: subtitle, - for: snapshot - ) - } - } - ) - } - // MARK: View @@ -130,34 +100,41 @@ extension Modern.PlacemarksDemo { ) } } - .navigationBarTitle("Placemarks") - .navigationBarItems( - trailing: Button("Random") { + .task(id: self.place.map({ "\($0.$latitude),\($0.$longitude)" })) { + + guard let place = self.place else { + + return + } + let geocoded = await self.geocoder.geocode(place: place) + guard self.place?.objectID() == place.objectID() else { + + return + } + guard geocoded.title != nil || geocoded.subtitle != nil else { + + return + } + self.demoUnsafeTransaction( + title: geocoded.title, + subtitle: geocoded.subtitle, + for: place + ) + } + .navigationTitle("Placemarks") + .toolbar { + + Button("Random") { self.demoSynchronousTransaction() } - ) + } } // MARK: Private - private var sinkCancellable: AnyCancellable? - private let geocoder = Modern.PlacemarksDemo.Geocoder() + @State + private var geocoder = Modern.PlacemarksDemo.Geocoder() } } - - -#if DEBUG - -struct _Demo_Modern_PlacemarksDemo_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - Modern.PlacemarksDemo.MainView() - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.swift index 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.MainView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift index c84f5d32..a25b7d20 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -2,18 +2,17 @@ // Demo // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. -import Combine import CoreStore import SwiftUI // MARK: - Modern.PokedexDemo extension Modern.PokedexDemo { - + // MARK: - Modern.PokedexDemo.MainView - + struct MainView: View { - + // MARK: Internal init( @@ -22,16 +21,16 @@ extension Modern.PokedexDemo { self.listView = listView } - - + + // MARK: View - + var body: some View { ZStack { self.listView() - .frame(minHeight: 0, maxHeight: .infinity) - .edgesIgnoringSafeArea(.vertical) + .frame(minHeight: 0, maxHeight: .infinity) + .ignoresSafeArea(.container, edges: .vertical) if self.pokedexEntries.isEmpty { @@ -56,10 +55,10 @@ extension Modern.PokedexDemo { .padding() } } - .navigationBarTitle("Pokedex") + .navigationTitle("Pokedex") } - - + + // MARK: Private @ListState( @@ -68,28 +67,10 @@ extension Modern.PokedexDemo { in: Modern.PokedexDemo.dataStack ) private var pokedexEntries - - @ObservedObject - private var service: Modern.PokedexDemo.Service = .init() + + @State + private var service = Modern.PokedexDemo.Service() private let listView: () -> ListView } } - - -#if DEBUG - -@available(iOS 14.0, *) -struct _Demo_Modern_PokedexDemo_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - Modern.PokedexDemo.MainView( - listView: Modern.PokedexDemo.UIKit.ListView.init - ) - } -} - -#endif diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index a365b18d..37cd65d5 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -2,64 +2,48 @@ // Demo // Copyright © 2020 John Rommel Estropia, Inc. All rights reserved. -import Foundation -import Combine +import CoreData import CoreStore -import UIKit +import Foundation +import Observation // MARK: - Modern.PokedexDemo extension Modern.PokedexDemo { - + // MARK: - Modern.PokedexDemo.Service - - final class Service: ObservableObject { + + @MainActor + @Observable + final class Service { /** ⭐️ Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples */ - private static func importPokedexEntries( - from output: URLSession.DataTaskPublisher.Output - ) -> Future { + private static func importPokedexEntries(from data: Data) async throws { - return .init { promise in + do { - Modern.PokedexDemo.dataStack.perform( - asynchronous: { transaction -> Void in - - let json: Dictionary = try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) - ) - let results: [Dictionary] = try self.parseJSON( - json["results"] - ) - _ = try transaction.importUniqueObjects( - Into(), - sourceArray: results.enumerated().map { (index, json) in - (index: index, json: json) - } - ) - }, - success: { result in - - promise(.success(result)) - }, - failure: { error in - - switch error { - - case .userError(let error as Modern.PokedexDemo.Service.Error): - promise(.failure(error)) - - case .userError(let error): - promise(.failure(.otherError(error))) - - case let error: - promise(.failure(.saveError(error))) + try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in + + let json: Dictionary = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) + ) + let results: [Dictionary] = try self.parseJSON( + json["results"] + ) + _ = try transaction.importUniqueObjects( + Into(), + sourceArray: results.enumerated().map { index, json in + (index: index, json: json) } - } - ) + ) + } + } + catch { + + throw self.mapError(error) } } @@ -67,134 +51,93 @@ extension Modern.PokedexDemo { ⭐️ Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary` */ private static func importSpecies( - for details: ObjectSnapshot, - from output: URLSession.DataTaskPublisher.Output - ) -> Future, Modern.PokedexDemo.Service.Error> { + for detailsObjectID: NSManagedObjectID, + from data: Data + ) async throws -> ObjectSnapshot { - return .init { promise in + let speciesObjectID = try await Modern.PokedexDemo.dataStack.async.perform { transaction -> NSManagedObjectID in - Modern.PokedexDemo.dataStack.perform( - asynchronous: { transaction -> Modern.PokedexDemo.Species in - - let json: Dictionary = try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) - ) - guard - let species = try transaction.importUniqueObject( - Into(), - source: json - ) - else { - - throw Modern.PokedexDemo.Service.Error.unexpected - } - details.asEditable(in: transaction)?.species = species - return species - }, - success: { species in - - promise(.success(species.asSnapshot(in: Modern.PokedexDemo.dataStack)!)) - }, - failure: { error in - - switch error { - - case .userError(let error as Modern.PokedexDemo.Service.Error): - promise(.failure(error)) - - case .userError(let error): - promise(.failure(.otherError(error))) - - case let error: - promise(.failure(.saveError(error))) - } - } + let json: Dictionary = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) ) + guard + let species = try transaction.importUniqueObject( + Into(), + source: json + ) + else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + transaction + .edit(Into(), detailsObjectID)? + .species = species + return species.objectID() } + guard + let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesObjectID), + let snapshot = species.asSnapshot() + else { + + throw Modern.PokedexDemo.Service.Error.unexpected + } + return snapshot } /** ⭐️ Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s */ private static func importForms( - for details: ObjectSnapshot, - from outputs: [URLSession.DataTaskPublisher.Output] - ) -> Future { + for detailsObjectID: NSManagedObjectID, + from dataArray: [Data] + ) async throws { - return .init { promise in + do { - Modern.PokedexDemo.dataStack.perform( - asynchronous: { transaction -> Void in - - let forms = try transaction.importUniqueObjects( - Into(), - sourceArray: outputs.map { output in - - return try self.parseJSON( - try JSONSerialization.jsonObject(with: output.data, options: []) - ) - } - ) - guard !forms.isEmpty else { + try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in + + let forms = try transaction.importUniqueObjects( + Into(), + sourceArray: dataArray.map { data in - throw Modern.PokedexDemo.Service.Error.unexpected + try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) + ) as [String: Any] } - details.asEditable(in: transaction)?.forms = forms - }, - success: { - - promise(.success(())) - }, - failure: { error in + ) + guard !forms.isEmpty else { - switch error { - - case .userError(let error as Modern.PokedexDemo.Service.Error): - promise(.failure(error)) - - case .userError(let error): - promise(.failure(.otherError(error))) - - case let error: - promise(.failure(.saveError(error))) - } + throw Modern.PokedexDemo.Service.Error.unexpected } - ) + transaction + .edit(Into(), detailsObjectID)? + .forms = forms + } } - } - - - // MARK: Internal - - private(set) var isLoading: Bool = false { - - willSet { + catch { - self.objectWillChange.send() + throw self.mapError(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,153 +176,235 @@ extension Modern.PokedexDemo { ) } } - + func fetchPokedexEntries() { - - self.cancellable["pokedexEntries"] = self.pokedexEntries - .receive(on: DispatchQueue.main) - .handleEvents( - receiveSubscription: { [weak self] _ in - - guard let self = self else { - - return - } - self.lastError = nil - self.isLoading = true - } - ) - .sink( - receiveCompletion: { [weak self] completion in - - guard let self = self else { - - return - } - self.isLoading = false - switch completion { - - case .finished: - self.lastError = nil - - case .failure(let error): - print(error) - self.lastError = ( - error: error, - retry: { [weak self] in - - self?.fetchPokedexEntries() - } - ) - } - }, - receiveValue: {} - ) + + self.pokedexEntriesTask?.cancel() + self.pokedexEntriesTask = Task { [weak self] in + + await self?.runFetchPokedexEntries() + } } func fetchDetails(for pokedexEntry: ObjectSnapshot) { - self.fetchSpeciesIfNeeded(for: pokedexEntry) - } - - - // MARK: Private - - private var cancellable: Dictionary = [:] - - private lazy var pokedexEntries: AnyPublisher = URLSession.shared - .dataTaskPublisher( - for: URL(string: "https://pokeapi.co/api/v2/pokemon?limit=10000&offset=0")! - ) - .mapError({ .networkError($0) }) - .flatMap(Self.importPokedexEntries(from:)) - .eraseToAnyPublisher() - - private func fetchSpeciesIfNeeded(for pokedexEntry: ObjectSnapshot) { - guard let details = pokedexEntry.$details?.snapshot else { return } if let species = details.$species?.snapshot { - self.fetchFormsIfNeeded(for: species) + self.fetchFormsIfNeeded( + key: species.$id, + detailsObjectID: details.objectID(), + species: species + ) return } - self.cancellable["species.\(pokedexEntry.$id)"] = URLSession.shared - .dataTaskPublisher(for: pokedexEntry.$speciesURL) - .mapError({ .networkError($0) }) - .flatMap({ Self.importSpecies(for: details, from: $0) }) - .sink( - receiveCompletion: { completion in - - switch completion { - - case .finished: - break - - case .failure(let error): - print(error) - } - }, - receiveValue: { species in - - self.fetchFormsIfNeeded(for: species) - } + let key = pokedexEntry.$id + guard self.detailTasks[key] == nil else { + + return + } + let speciesURL = pokedexEntry.$speciesURL + let detailsObjectID = details.objectID() + self.detailTasks[key] = Task { [weak self] in + + guard let self else { + + return + } + defer { + + self.detailTasks.removeValue(forKey: key) + } + await self.fetchSpecies( + key: key, + detailsObjectID: detailsObjectID, + speciesURL: speciesURL ) + } } - 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, + detailsObjectID: NSManagedObjectID, + speciesURL: URL + ) async { + + do { + + let (data, _) = try await URLSession.shared.data(from: speciesURL) + try Task.checkCancellation() + + let species = try await Self.importSpecies( + for: detailsObjectID, + from: data ) - .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( + detailsObjectID: detailsObjectID, + 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, + detailsObjectID: NSManagedObjectID, + species: ObjectSnapshot + ) { + + guard species.$details?.snapshot?.$forms.isEmpty == true else { + + return + } + guard self.detailTasks[key] == nil else { + + return + } + + let formsURLs = species.$formsURLs + self.detailTasks[key] = Task { [weak self] in + + guard let self else { + + return + } + defer { + + self.detailTasks.removeValue(forKey: key) + } + await self.fetchForms( + detailsObjectID: detailsObjectID, + formsURLs: formsURLs ) + } } - - + + private func fetchForms( + detailsObjectID: NSManagedObjectID, + formsURLs: [URL] + ) async { + + do { + + var dataArray: [Data] = [] + dataArray.reserveCapacity(formsURLs.count) + + for url in formsURLs { + let (data, _) = try await URLSession.shared.data(from: url) + try Task.checkCancellation() + dataArray.append(data) + } + try await Self.importForms( + for: detailsObjectID, + from: dataArray + ) + } + catch is CancellationError { + + return + } + catch let error as Modern.PokedexDemo.Service.Error { + + print(error) + } + catch let error as URLError { + + print(Modern.PokedexDemo.Service.Error.networkError(error)) + } + catch { + + print(Modern.PokedexDemo.Service.Error.otherError(error)) + } + } + + // MARK: - Modern.PokedexDemo.Service.Error - + enum Error: Swift.Error { - + case networkError(URLError) case parseError(expected: Any.Type, actual: Any.Type, file: String) case saveError(CoreStoreError) diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.UIKit.ListView.swift index 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.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..4373f770 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -11,124 +11,54 @@ import SwiftUI extension Menu { // MARK: - Menu.MainView - + struct MainView: View { + @State + private var selection: Menu.Route? + + // MARK: View var body: some View { - NavigationView { - List { - Section(header: Text("Modern (CoreStoreObject subclasses)")) { - Menu.ItemView( - title: "Placemarks", - subtitle: "Making changes using Transactions", - destination: { - Modern.PlacemarksDemo.MainView() - } - ) - Menu.ItemView( - title: "Time Zones", - subtitle: "Fetching objects and Querying raw values", - destination: { - Modern.TimeZonesDemo.MainView() + + NavigationSplitView( + sidebar: { + + List(selection: self.$selection) { + + ForEach(Menu.Section.allCases, id: \.self) { section in + + Section(section.rawValue) { + + ForEach(section.routes) { route in + + Menu.ItemView( + title: route.title, + subtitle: route.subtitle, + isEnabled: route.isEnabled + ) + .tag(route as Menu.Route?) + .disabled(!route.isEnabled) + } } - ) - Menu.ItemView( - title: "Colors (UIKit)", - subtitle: "Observing list changes and single-object changes using DiffableDataSources", - destination: { - Modern.ColorsDemo.MainView( - listView: { listPublisher, onPaletteTapped in - Modern.ColorsDemo.UIKit.ListView( - listPublisher: listPublisher, - onPaletteTapped: onPaletteTapped - ) - .edgesIgnoringSafeArea(.all) - }, - detailView: { objectPublisher in - Modern.ColorsDemo.UIKit.DetailView(objectPublisher) - } - ) - } - ) - Menu.ItemView( - title: "Colors (SwiftUI)", - subtitle: "Observing list changes and single-object changes using SwiftUI bindings", - destination: { - Modern.ColorsDemo.MainView( - listView: { listPublisher, onPaletteTapped in - Modern.ColorsDemo.SwiftUI.ListView( - listPublisher: listPublisher, - onPaletteTapped: onPaletteTapped - ) - }, - detailView: { objectPublisher in - Modern.ColorsDemo.SwiftUI.DetailView(objectPublisher) - } - ) - } - ) - Menu.ItemView( - title: "Pokedex API", - subtitle: "Importing JSON data from external source", - destination: { - Modern.PokedexDemo.MainView( - listView: Modern.PokedexDemo.UIKit.ListView.init - ) - } - ) + } } - Section(header: Text("Classic (NSManagedObject subclasses)")) { - Menu.ItemView( - title: "Colors", - subtitle: "Observing list changes and single-object changes using ListMonitor", - destination: { - Classic.ColorsDemo.MainView() - } - ) + .navigationTitle("CoreStore Demos") + .listStyle(.sidebar) + }, + detail: { + + if let selection = self.selection { + + selection.destination } - Section(header: Text("Advanced")) { - Menu.ItemView( - title: "Accounts", - subtitle: "Switching between multiple persistent stores", - destination: { EmptyView() } - ) - .disabled(true) - Menu.ItemView( - title: "Evolution", - subtitle: "Migrating and reverse-migrating stores", - destination: { - Advanced.EvolutionDemo.MainView() - } - ) - Menu.ItemView( - title: "Logger", - subtitle: "Implementing a custom logger", - destination: { EmptyView() } - ) - .disabled(true) + else { + + Menu.PlaceholderView() } } - .listStyle(GroupedListStyle()) - .navigationBarTitle("CoreStore Demos") - Menu.PlaceholderView() - } - .navigationViewStyle(DoubleColumnNavigationViewStyle()) + ) } } } - -#if DEBUG - -struct _Demo_Menu_MainView_Preview: PreviewProvider { - - // MARK: PreviewProvider - - static var previews: some View { - - Menu.MainView() - } -} - -#endif diff --git a/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift b/Demo/Sources/Helpers/Menu/Menu.PlaceholderView.swift index 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/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/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/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index ce012277..4e5546c1 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -32,7 +32,8 @@ import CoreData /** The `AsynchronousDataTransaction` provides an interface for `DynamicObject` creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from `DataStack.perform(asynchronous:...)`. */ -public final class AsynchronousDataTransaction: BaseDataTransaction { +@_nonSendable +public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction { /** Cancels a transaction by throwing `CoreStoreError.userCancelled`. @@ -78,6 +79,23 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { return super.create(into) } + /** + Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. + + - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ persistentID: DynamicObjectID? + ) -> O? { + + Internals.assert( + !self.isCommitted, + "Attempted to update an entity for \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))." + ) + return super.edit(persistentID) + } + /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. @@ -188,7 +206,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction { } internal func autoCommit( - _ completion: @escaping @MainActor ( + _ completion: @escaping @MainActor @Sendable ( _ hasChanges: Bool, _ error: CoreStoreError? ) -> Void diff --git a/Sources/BaseDataTransaction+Querying.swift b/Sources/BaseDataTransaction+Querying.swift index c74eaa06..78a217a9 100644 --- a/Sources/BaseDataTransaction+Querying.swift +++ b/Sources/BaseDataTransaction+Querying.swift @@ -110,6 +110,19 @@ extension BaseDataTransaction: FetchableSource, QueryableSource { return self.context.fetchExisting(object) } + /** + Fetches the `DynamicObject` instance in the transaction's context from an `NSManagedObjectID`. + + - parameter persistentID: the `DynamicObjectID` for the object + - returns: the `DynamicObject` instance if the object exists in the transaction, or `nil` if not found. + */ + public func fetchExisting( + _ persistentID: DynamicObjectID + ) -> O? { + + return self.context.fetchExisting(persistentID.managedObjectID) + } + /** Fetches the `DynamicObject` instance in the transaction's context from an `NSManagedObjectID`. @@ -136,6 +149,19 @@ extension BaseDataTransaction: FetchableSource, QueryableSource { return self.context.fetchExisting(objects) } + /** + Fetches the `DynamicObject` instances in the transaction's context from a list of `DynamicObjectID`. + + - parameter objectIDs: the `DynamicObjectID` array for the objects + - returns: the `DynamicObject` array for objects that exists in the transaction + */ + public func fetchExisting( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return self.context.fetchExisting(objectIDs) + } + /** Fetches the `DynamicObject` instances in the transaction's context from a list of `NSManagedObjectID`. diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index df88ee7d..ee65c719 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -115,6 +115,27 @@ public /*abstract*/ class BaseDataTransaction { } } + /** + Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. + + - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public func edit( + _ persistentID: DynamicObjectID? + ) -> O? { + + Internals.assert( + self.isRunningInAllowedQueue(), + "Attempted to update an entity for \(Internals.typeName(persistentID)) outside its designated queue." + ) + guard let persistentID = persistentID else { + + return nil + } + return self.context.fetchExisting(persistentID.managedObjectID) + } + /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. diff --git a/Sources/CoreStore+CustomDebugStringConvertible.swift b/Sources/CoreStore+CustomDebugStringConvertible.swift index 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/CoreStoreDefaults.swift b/Sources/CoreStoreDefaults.swift index f66dec44..df9d7f93 100644 --- a/Sources/CoreStoreDefaults.swift +++ b/Sources/CoreStoreDefaults.swift @@ -24,7 +24,6 @@ // import Foundation -import os // MARK: - CoreStoreDefaults @@ -94,6 +93,6 @@ public enum CoreStoreDefaults { // MARK: Private - private static let defaultStackInstance: OSAllocatedUnfairLock = .init(initialState: nil) - private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil) + private static let defaultStackInstance: Internals.Mutex = .init(nil) + private static let loggerInstance: Internals.Mutex<(any CoreStoreLogger)?> = .init(nil) } diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index 4f463e03..136b9200 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -32,7 +32,7 @@ import Foundation /** All errors thrown from CoreStore are expressed in `CoreStoreError` enum values. */ -public enum CoreStoreError: Error, CustomNSError, Hashable, @unchecked Sendable { +public enum CoreStoreError: Error, CustomNSError, Hashable, Sendable { /** A failure occured because of an unknown error. diff --git a/Sources/CoreStoreObject.swift b/Sources/CoreStoreObject.swift index e848c85d..2307beb2 100644 --- a/Sources/CoreStoreObject.swift +++ b/Sources/CoreStoreObject.swift @@ -65,6 +65,7 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { Do not call this directly. This is exposed as public only as a required initializer. - Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations. */ + @_spi(Internals) public required init(rawObject: NSManagedObject) { self.isMeta = false @@ -84,6 +85,7 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { Do not call this directly. This is exposed as public only as a required initializer. - Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations. */ + @_spi(Internals) public required init(asMeta: Void) { self.isMeta = true diff --git a/Sources/CoreStoreSchema.swift b/Sources/CoreStoreSchema.swift index 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..3fdd5994 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -32,7 +32,7 @@ import Foundation /** A `SchemaMappingProvider` that accepts custom mappings for some entities. Mappings of entities with no `CustomMapping` provided will be automatically calculated if possible. */ -public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { +public final class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { /** The source model version for the mapping. @@ -78,7 +78,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { /** Provides the type of mapping for an entity. Mappings of entities with no `CustomMapping` provided will be automatically calculated if possible. Any conflicts or ambiguity will raise an assertion. */ - public enum CustomMapping: Hashable { + public enum CustomMapping: Hashable, Sendable { /** The `sourceEntity` is meant to be removed from the source `DynamicSchema` and should not be migrated to the destination `DynamicSchema`. @@ -105,7 +105,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { - parameter sourceObject: a proxy object representing the source entity. The properties can be accessed via keyPath. - parameter createDestinationObject: the closure to create the object for the destination entity. The `CustomMapping.inferredTransformation` method can be used directly as the `transformer` if the changes can be inferred (i.e. lightweight). The object is created lazily and executing the closure multiple times will return the same instance. The destination object's properties can be accessed and updated via keyPath. */ - public typealias Transformer = ( + public typealias Transformer = @Sendable ( _ sourceObject: UnsafeSourceObject, _ createDestinationObject: () -> UnsafeDestinationObject ) throws(any Swift.Error) -> Void @@ -737,7 +737,10 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { .transformEntity( sourceEntity: sourceEntityName, destinationEntity: destinationEntityName, - transformer: CustomMapping.inferredTransformation + transformer: { + + return try CustomMapping.inferredTransformation($0, $1) + } ) ) } diff --git a/Sources/DataStack+DataSources.swift b/Sources/DataStack+DataSources.swift index 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 14e50050..c7251f0d 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -24,8 +24,7 @@ // import Foundation -@preconcurrency import CoreData -import os +import CoreData // MARK: - DataStack @@ -50,7 +49,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping @MainActor (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) { self.coordinator.performAsynchronously { @@ -111,7 +110,7 @@ extension DataStack { */ public func addStorage( _ storage: T, - completion: @escaping @MainActor (SetupResult) -> Void + completion: @escaping @MainActor @Sendable (SetupResult) -> Void ) -> Progress? { let fileURL = storage.fileURL @@ -274,7 +273,7 @@ extension DataStack { */ public func upgradeStorageIfNeeded( _ storage: T, - completion: @escaping @MainActor (MigrationResult) -> Void + completion: @escaping @MainActor @Sendable (MigrationResult) -> Void ) throws(CoreStoreError) -> Progress? { return try self.coordinator.performSynchronously { @@ -386,7 +385,7 @@ extension DataStack { private func upgradeStorageIfNeeded( _ storage: T, metadata: [String: Any], - completion: @escaping @MainActor (MigrationResult) -> Void + completion: @escaping @MainActor @Sendable (MigrationResult) -> Void ) -> Progress? { guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else { @@ -433,8 +432,8 @@ extension DataStack { } let migrationTypes = migrationSteps.map { $0.migrationType } - let migrationState: OSAllocatedUnfairLock<(migrationResult: MigrationResult?, cancelled: Bool)> = .init( - initialState: ( + let migrationState: Internals.Mutex<(migrationResult: MigrationResult?, cancelled: Bool)> = .init( + ( migrationResult: nil, cancelled: false ) @@ -451,6 +450,9 @@ extension DataStack { let childProgress = Progress(parent: progress, userInfo: nil) childProgress.totalUnitCount = 100 + nonisolated(unsafe) let sourceModel = sourceModel + nonisolated(unsafe) let destinationModel = destinationModel + nonisolated(unsafe) let mappingModel = mappingModel operations.append( BlockOperation { [weak self] in @@ -508,13 +510,11 @@ extension DataStack { operations.forEach { migrationOperation.addDependency($0) } migrationOperation.addExecutionBlock { () -> Void in + let migrationResult = migrationState.withLock { $0.migrationResult } DispatchQueue.main.async { progress.setProgressHandler(nil) - completion( - migrationState.withLock { $0.migrationResult } - ?? .success(migrationTypes) - ) + completion(migrationResult ?? .success(migrationTypes)) return } } @@ -620,7 +620,7 @@ extension DataStack { let estimatedTime: TimeInterval = 60 * 3 // 3 mins let interval: TimeInterval = 1 let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount) - let fakeProgress: OSAllocatedUnfairLock = .init(initialState: 0) + let fakeProgress: Internals.Mutex = .init(0) @Sendable func recursiveCheck() { @@ -656,9 +656,11 @@ extension DataStack { ) ) } - timerQueue.sync { - - fakeProgress.withLock({ $0 = 1.0 }) + withoutActuallyEscaping(timerQueue.sync) { escapingClosure in + escapingClosure { + + fakeProgress.withLock({ $0 = 1.0 }) + } } _ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel) progress.completedUnitCount = progress.totalUnitCount diff --git a/Sources/DataStack+Observing.swift b/Sources/DataStack+Observing.swift index 2ec7f30c..8dc9572b 100644 --- a/Sources/DataStack+Observing.swift +++ b/Sources/DataStack+Observing.swift @@ -37,6 +37,7 @@ extension DataStack { - parameter object: the `DynamicObject` to observe changes from - returns: an `ObjectMonitor` that monitors changes to `object` */ + @MainActor public func monitorObject( _ object: O ) -> ObjectMonitor { @@ -55,6 +56,7 @@ extension DataStack { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - returns: a `ListMonitor` instance that monitors changes to the list */ + @MainActor public func monitorList( _ from: From, _ fetchClauses: FetchClause... @@ -70,6 +72,7 @@ extension DataStack { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - returns: a `ListMonitor` instance that monitors changes to the list */ + @MainActor public func monitorList( _ from: From, _ fetchClauses: [FetchClause] @@ -107,6 +110,7 @@ extension DataStack { - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses - returns: a `ListMonitor` for a list of `DynamicObject`s that satisfy the specified `FetchChainableBuilderType` */ + @MainActor public func monitorList( _ clauseChain: B ) -> ListMonitor { @@ -124,6 +128,7 @@ extension DataStack { - parameter from: a `From` clause indicating the entity type - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ + @MainActor public func monitorList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -144,6 +149,7 @@ extension DataStack { - parameter from: a `From` clause indicating the entity type - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ + @MainActor public func monitorList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -187,6 +193,7 @@ extension DataStack { - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses */ + @MainActor public func monitorList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B @@ -207,6 +214,7 @@ extension DataStack { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - returns: a `ListMonitor` instance that monitors changes to the list */ + @MainActor public func monitorSectionedList( _ from: From, _ sectionBy: SectionBy, @@ -228,6 +236,7 @@ extension DataStack { - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - returns: a `ListMonitor` instance that monitors changes to the list */ + @MainActor public func monitorSectionedList( _ from: From, _ sectionBy: SectionBy, @@ -268,6 +277,7 @@ extension DataStack { - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses - returns: a `ListMonitor` for a list of `DynamicObject`s that satisfy the specified `SectionMonitorBuilderType` */ + @MainActor public func monitorSectionedList( _ clauseChain: B ) -> ListMonitor { @@ -287,6 +297,7 @@ extension DataStack { - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ + @MainActor public func monitorSectionedList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -310,6 +321,7 @@ extension DataStack { - parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. */ + @MainActor public func monitorSectionedList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ from: From, @@ -355,6 +367,7 @@ extension DataStack { - parameter createAsynchronously: the closure that receives the created `ListMonitor` instance - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses */ + @MainActor public func monitorSectionedList( createAsynchronously: @escaping @Sendable (ListMonitor) -> Void, _ clauseChain: B diff --git a/Sources/DataStack+Querying.swift b/Sources/DataStack+Querying.swift index 6c48c153..f1cbb599 100644 --- a/Sources/DataStack+Querying.swift +++ b/Sources/DataStack+Querying.swift @@ -46,6 +46,19 @@ extension DataStack: FetchableSource, QueryableSource { return self.mainContext.fetchExisting(object) } + /** + Fetches the `DynamicObject` instance in the `DataStack`'s context from an `NSManagedObjectID`. + + - parameter persistentID: the `DynamicObjectID` for the object + - returns: the `DynamicObject` instance if the object exists in the `DataStack`, or `nil` if not found. + */ + public func fetchExisting( + _ persistentID: DynamicObjectID + ) -> O? { + + return self.mainContext.fetchExisting(persistentID.managedObjectID) + } + /** Fetches the `DynamicObject` instance in the `DataStack`'s context from an `NSManagedObjectID`. @@ -72,6 +85,19 @@ extension DataStack: FetchableSource, QueryableSource { return self.mainContext.fetchExisting(objects) } + /** + Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `DynamicObjectID`. + + - parameter objectIDs: the `DynamicObjectID` array for the objects + - returns: the `DynamicObject` array for objects that exists in the `DataStack` + */ + public func fetchExisting( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return self.mainContext.fetchExisting(objectIDs) + } + /** Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `NSManagedObjectID`. diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index 2994b440..68946fd9 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -43,7 +43,7 @@ extension DataStack { _ transaction: AsynchronousDataTransaction ) throws(any Swift.Error) -> T, sourceIdentifier: (any Sendable)? = nil, - completion: @escaping @Sendable (AsynchronousDataTransaction.Result) -> Void + completion: @escaping @MainActor @Sendable (AsynchronousDataTransaction.Result) -> Void ) { self.perform( @@ -67,8 +67,8 @@ extension DataStack { _ transaction: AsynchronousDataTransaction ) throws(any Swift.Error) -> T, sourceIdentifier: (any Sendable)? = nil, - success: @escaping @Sendable (sending T) -> Void, - failure: @escaping @Sendable (CoreStoreError) -> Void + success: @escaping @MainActor @Sendable (sending T) -> Void, + failure: @escaping @MainActor @Sendable (CoreStoreError) -> Void ) { nonisolated(unsafe) let transaction = AsynchronousDataTransaction( @@ -186,6 +186,7 @@ extension DataStack { /** Refreshes all registered objects `NSManagedObject`s or `CoreStoreObject`s in the `DataStack`. */ + @MainActor public func refreshAndMergeAllObjects() { Internals.assert( diff --git a/Sources/DataStack.AddStoragePublisher.swift b/Sources/DataStack.AddStoragePublisher.swift index 62caaac5..b12d006e 100644 --- a/Sources/DataStack.AddStoragePublisher.swift +++ b/Sources/DataStack.AddStoragePublisher.swift @@ -90,7 +90,7 @@ extension DataStack { // MARK: - AddStorageSubscription - fileprivate final class AddStorageSubscription: Subscription, @unchecked Sendable + fileprivate final class AddStorageSubscription: Subscription where S.Input == Output, S.Failure == CoreStoreError { // MARK: FilePrivate @@ -116,14 +116,15 @@ extension DataStack { return } nonisolated(unsafe) var progress: Progress? = nil + nonisolated(unsafe) weak let weakSelf = self as Optional progress = self.dataStack.addStorage( self.storage, - completion: { [weak self] result in + completion: { result in progress?.setProgressHandler(nil) guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { @@ -151,12 +152,12 @@ extension DataStack { ) if let progress = progress { - Internals.mainActorImmediate { @MainActor [weak self] in + Internals.mainActorImmediate { @MainActor in progress.setProgressHandler { progress in guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index be5a3f73..5c5c2379 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -32,7 +32,7 @@ import CoreData /** The `DataStack` encapsulates the data model for the Core Data stack. Each `DataStack` can have multiple data stores, usually specified as a "Configuration" in the model editor. Behind the scenes, the DataStack manages its own `NSPersistentStoreCoordinator`, a root `NSManagedObjectContext` for disk saves, and a shared `NSManagedObjectContext` designed as a read-only model interface for `NSManagedObjects`. */ -public final class DataStack: Equatable, @unchecked Sendable { +public final class DataStack: Equatable, Sendable { /** The resolved application name, used by the `DataStack` as the default Xcode model name (.xcdatamodel filename) if not explicitly provided. @@ -398,7 +398,7 @@ public final class DataStack: Equatable, @unchecked Sendable { - parameter completion: the closure to execute after all persistent stores are removed */ public func unsafeRemoveAllPersistentStores( - completion: @escaping @MainActor () -> Void = {} + completion: @escaping @MainActor @Sendable () -> Void = {} ) { let coordinator = self.coordinator @@ -466,7 +466,6 @@ public final class DataStack: Equatable, @unchecked Sendable { internal let mainContext: NSManagedObjectContext internal let schemaHistory: SchemaHistory internal let childTransactionQueue = DispatchQueue.serial("com.coreStore.dataStack.childTransactionQueue", qos: .utility) - internal let storeMetadataLock: NSRecursiveLock = .init() internal let migrationQueue: OperationQueue = Internals.with { let migrationQueue = OperationQueue() @@ -487,15 +486,14 @@ public final class DataStack: Equatable, @unchecked Sendable { } internal func persistentStores( - for entityIdentifier: Internals.EntityIdentifier + for entityIdentifier: sending Internals.EntityIdentifier ) -> [NSPersistentStore]? { - self.storeMetadataLock.lock() - defer { - self.storeMetadataLock.unlock() + return self.storeMetadataLock.withLockUnchecked { metadata in + + return metadata.finalConfigurationsByEntityIdentifier[entityIdentifier]? + .map({ metadata.persistentStoresByFinalConfiguration[$0]! }) ?? [] } - return self.finalConfigurationsByEntityIdentifier[entityIdentifier]? - .map({ self.persistentStoresByFinalConfiguration[$0]! }) ?? [] } internal func persistentStore( @@ -504,33 +502,32 @@ public final class DataStack: Equatable, @unchecked Sendable { inferStoreIfPossible: Bool ) -> (store: NSPersistentStore?, isAmbiguous: Bool) { - self.storeMetadataLock.lock() - defer { - self.storeMetadataLock.unlock() - } - let configurationsForEntity = self.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? [] - if let configuration = configuration { + return self.storeMetadataLock.withLockUnchecked { metadata in + + let configurationsForEntity = metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] ?? [] + if let configuration = configuration { - if configurationsForEntity.contains(configuration) { + if configurationsForEntity.contains(configuration) { - return (store: self.persistentStoresByFinalConfiguration[configuration], isAmbiguous: false) - } - 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) + } } } @@ -550,26 +547,24 @@ public final class DataStack: Equatable, @unchecked Sendable { do { - self.storeMetadataLock.lock() - defer { - self.storeMetadataLock.unlock() - } - - let configurationName = persistentStore.configurationName - self.persistentStoresByFinalConfiguration[configurationName] = persistentStore - for entityDescription in (self.coordinator.managedObjectModel.entities(forConfigurationName: configurationName) ?? []) { + self.storeMetadataLock.withLock { metadata in - let managedObjectClassName = entityDescription.managedObjectClassName! - Internals.assert( - NSClassFromString(managedObjectClassName) != nil, - "The class \(Internals.typeName(managedObjectClassName)) for the entity \(Internals.typeName(entityDescription.name)) does not exist. Check if the subclass type and module name are properly configured." - ) - let entityIdentifier = Internals.EntityIdentifier(entityDescription) - if self.finalConfigurationsByEntityIdentifier[entityIdentifier] == nil { + let configurationName = persistentStore.configurationName + metadata.persistentStoresByFinalConfiguration[configurationName] = persistentStore + for entityDescription in (self.coordinator.managedObjectModel.entities(forConfigurationName: configurationName) ?? []) { - self.finalConfigurationsByEntityIdentifier[entityIdentifier] = [] + let managedObjectClassName = entityDescription.managedObjectClassName! + Internals.assert( + NSClassFromString(managedObjectClassName) != nil, + "The class \(Internals.typeName(managedObjectClassName)) for the entity \(Internals.typeName(entityDescription.name)) does not exist. Check if the subclass type and module name are properly configured." + ) + let entityIdentifier = Internals.EntityIdentifier(entityDescription) + if metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] == nil { + + metadata.finalConfigurationsByEntityIdentifier[entityIdentifier] = [] + } + metadata.finalConfigurationsByEntityIdentifier[entityIdentifier]?.insert(configurationName) } - self.finalConfigurationsByEntityIdentifier[entityIdentifier]?.insert(configurationName) } } storage.cs_didAddToDataStack(self) @@ -586,8 +581,12 @@ public final class DataStack: Equatable, @unchecked Sendable { // MARK: Private - private var persistentStoresByFinalConfiguration = [String: NSPersistentStore]() - private var finalConfigurationsByEntityIdentifier = [Internals.EntityIdentifier: Set]() + private let storeMetadataLock: Internals.Mutex< + ( + persistentStoresByFinalConfiguration: [String: NSPersistentStore], + finalConfigurationsByEntityIdentifier: [Internals.EntityIdentifier: Set] + ) + > = .init(([:], [:])) deinit { diff --git a/Sources/DiffableDataSource.BaseAdapter.swift b/Sources/DiffableDataSource.BaseAdapter.swift index c4ced632..f87b68aa 100644 --- a/Sources/DiffableDataSource.BaseAdapter.swift +++ b/Sources/DiffableDataSource.BaseAdapter.swift @@ -218,6 +218,7 @@ extension DiffableDataSource { public func itemID(for indexPath: IndexPath) -> O.ObjectID? { return self.dispatcher.itemIdentifier(for: indexPath) + .map(O.ObjectID.init(managedObjectID:)) } /** @@ -228,7 +229,7 @@ extension DiffableDataSource { */ public func indexPath(for itemID: O.ObjectID) -> IndexPath? { - return self.dispatcher.indexPath(for: itemID) + return self.dispatcher.indexPath(for: itemID.managedObjectID) } /** @@ -254,6 +255,11 @@ extension DiffableDataSource { // MARK: Internal internal let dispatcher: Internals.DiffableDataUIDispatcher + + internal func itemID(for indexPath: IndexPath) -> NSManagedObjectID? { + + return self.dispatcher.itemIdentifier(for: indexPath) + } } } diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-UIKit.swift index 73014d1b..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") } diff --git a/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift b/Sources/DiffableDataSource.TableViewAdapter-UIKit.swift index a3f39367..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 @MainActor (UITableView, IndexPath, O) -> UITableViewCell? + cellProvider: @escaping @MainActor @Sendable (UITableView, IndexPath, O) -> UITableViewCell? ) { self.cellProvider = cellProvider @@ -150,7 +150,7 @@ extension DiffableDataSource { cellForRowAt indexPath: IndexPath ) -> UITableViewCell { - guard let objectID = self.itemID(for: indexPath) else { + guard let objectID: NSManagedObjectID = self.itemID(for: indexPath) else { Internals.abort("Object at \(Internals.typeName(IndexPath.self)) \(indexPath) already removed from list") } diff --git a/Sources/DiffableDataSource.Target.swift b/Sources/DiffableDataSource.Target.swift index 961d5cab..a4717ab4 100644 --- a/Sources/DiffableDataSource.Target.swift +++ b/Sources/DiffableDataSource.Target.swift @@ -220,7 +220,10 @@ extension DiffableDataSource.Target { } }, animated: animated, - completion: group.leave + completion: { + + group.leave() + } ) } } diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index d0e42a22..fb95c462 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -32,16 +32,19 @@ import CoreData /** All CoreStore's utilities are designed around `DynamicObject` instances. `NSManagedObject` and `CoreStoreObject` instances all conform to `DynamicObject`. */ -public protocol DynamicObject: AnyObject { +@_nonSendable +public nonisolated protocol DynamicObject: AnyObject, SendableMetatype { /** The object ID for this instance */ - typealias ObjectID = NSManagedObjectID + typealias ObjectID = DynamicObjectID + /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) static func cs_forceCreate( entityDescription: NSEntityDescription, into context: NSManagedObjectContext, @@ -51,14 +54,16 @@ public protocol DynamicObject: AnyObject { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) static func cs_snapshotDictionary( - id: ObjectID, + managedObjectID: NSManagedObjectID, context: NSManagedObjectContext ) -> [String: Any]? /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) static func cs_fromRaw( object: NSManagedObject ) -> Self @@ -66,6 +71,7 @@ public protocol DynamicObject: AnyObject { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) static func cs_matches( object: NSManagedObject ) -> Bool @@ -73,16 +79,26 @@ public protocol DynamicObject: AnyObject { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) func cs_toRaw() -> NSManagedObject /** Used internally by CoreStore. Do not call directly. */ - func cs_id() -> ObjectID + @_spi(Internals) + func cs_id() -> NSManagedObjectID } extension DynamicObject { + // MARK: Public + + public func persistentID() -> DynamicObjectID { + + return .init(managedObjectID: self.cs_id()) + } + + // MARK: Internal internal func runtimeType() -> Self.Type { @@ -99,6 +115,7 @@ extension NSManagedObject: DynamicObject { // MARK: DynamicObject + @_spi(Internals) public class func cs_forceCreate( entityDescription: NSEntityDescription, into context: NSManagedObjectContext, @@ -112,13 +129,14 @@ extension NSManagedObject: DynamicObject { } return object } - + + @_spi(Internals) public class func cs_snapshotDictionary( - id: ObjectID, + managedObjectID: NSManagedObjectID, context: NSManagedObjectContext ) -> [String: Any]? { - guard let object = context.fetchExisting(id) as NSManagedObject? else { + guard let object = context.fetchExisting(managedObjectID) as NSManagedObject? else { return nil } @@ -131,6 +149,7 @@ extension NSManagedObject: DynamicObject { return dictionary } + @_spi(Internals) public class func cs_fromRaw(object: NSManagedObject) -> Self { #if swift(>=5.9) @@ -143,6 +162,7 @@ extension NSManagedObject: DynamicObject { #endif } + @_spi(Internals) public static func cs_matches( object: NSManagedObject ) -> Bool { @@ -150,12 +170,14 @@ extension NSManagedObject: DynamicObject { return object.isKind(of: self) } + @_spi(Internals) public func cs_toRaw() -> NSManagedObject { return self } - public func cs_id() -> ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.objectID } @@ -168,6 +190,7 @@ extension CoreStoreObject { // MARK: DynamicObject + @_spi(Internals) public class func cs_forceCreate( entityDescription: NSEntityDescription, into context: NSManagedObjectContext, @@ -182,9 +205,10 @@ extension CoreStoreObject { } return self.cs_fromRaw(object: object) } - + + @_spi(Internals) public class func cs_snapshotDictionary( - id: ObjectID, + managedObjectID: NSManagedObjectID, context: NSManagedObjectContext ) -> [String: Any]? { @@ -240,7 +264,7 @@ extension CoreStoreObject { } } } - guard let object = context.fetchExisting(id) as CoreStoreObject? else { + guard let object = context.fetchExisting(managedObjectID) as CoreStoreObject? else { return nil } @@ -253,7 +277,7 @@ extension CoreStoreObject { else { guard - let object = context.fetchExisting(id) as CoreStoreObject?, + let object = context.fetchExisting(managedObjectID) as CoreStoreObject?, let rawObject = object.rawObject, !rawObject.isDeleted else { @@ -292,6 +316,7 @@ extension CoreStoreObject { return values } + @_spi(Internals) public class func cs_fromRaw(object: NSManagedObject) -> Self { if let coreStoreObject = object.coreStoreObject { @@ -310,6 +335,7 @@ extension CoreStoreObject { return coreStoreObject } + @_spi(Internals) public static func cs_matches( object: NSManagedObject ) -> Bool { @@ -321,12 +347,14 @@ extension CoreStoreObject { return (self as AnyClass).isSubclass(of: type as AnyClass) } + @_spi(Internals) public func cs_toRaw() -> NSManagedObject { return self.rawObject! } - public func cs_id() -> ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.rawObject!.objectID } diff --git a/Sources/DynamicObjectID.swift b/Sources/DynamicObjectID.swift new file mode 100644 index 00000000..a2fb43f0 --- /dev/null +++ b/Sources/DynamicObjectID.swift @@ -0,0 +1,137 @@ +// +// DynamicObject.swift +// CoreStore +// +// Copyright © 2026 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import CoreData + + +// MARK: - DynamicObjectID + +public struct DynamicObjectID: Hashable, ObjectRepresentation, Sendable { + + /** + The associated `NSManagedObject` or `CoreStoreObject` entity class + */ + public let entityClass: O.Type + + public init( + managedObjectID: NSManagedObjectID + ) { + + self.init( + entityClass: O.self, + managedObjectID: managedObjectID + ) + } + + public init( + entityClass: O.Type, + managedObjectID: NSManagedObjectID + ) { + + Internals.assert( + Internals.EntityIdentifier(entityClass) == Internals.EntityIdentifier(managedObjectID.entity), + "The \(Internals.typeName(managedObjectID)) does not belong to the entity \(Internals.typeName(entityClass))." + ) + + self.entityClass = entityClass + self.managedObjectID = managedObjectID + } + + + // MARK: Equatable + + public static func ==(lhs: Self, rhs: Self) -> Bool { + + return lhs.managedObjectID == rhs.managedObjectID + } + + + // MARK: Hashable + + public func hash(into hasher: inout Hasher) { + + hasher.combine(self.managedObjectID) + } + + + // MARK: AnyObjectRepresentation + + @_spi(Internals) + public func cs_dataStack() -> DataStack? { + + return nil + } + + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { + + return self.managedObjectID + } + + + // MARK: ObjectRepresentation + + public typealias ObjectType = O + + public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { + + let context = dataStack.unsafeContext() + return context.objectPublisher(managedObjectID: self.managedObjectID) + } + + public func asReadOnly(in dataStack: DataStack) -> ObjectType? { + + let context = dataStack.unsafeContext() + return context.fetchExisting(self.managedObjectID) + } + + public func asEditable(in transaction: BaseDataTransaction) -> ObjectType? { + + let context = transaction.unsafeContext() + return context.fetchExisting(self.managedObjectID) + } + + public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { + + let context = dataStack.unsafeContext() + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) + } + + public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { + + let context = transaction.unsafeContext() + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) + } + + + // MARK: Internal + + internal let managedObjectID: NSManagedObjectID + + internal var entityIdentifier: Internals.EntityIdentifier { + + return Internals.EntityIdentifier(self.managedObjectID.entity) + } +} diff --git a/Sources/DynamicSchema.swift b/Sources/DynamicSchema.swift index 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/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/InMemoryStore.swift b/Sources/InMemoryStore.swift index 8794d729..7297d1dd 100644 --- a/Sources/InMemoryStore.swift +++ b/Sources/InMemoryStore.swift @@ -31,7 +31,7 @@ import CoreData /** A storage interface that is backed only in memory. */ -public final class InMemoryStore: StorageInterface, @unchecked Sendable { +public final class InMemoryStore: StorageInterface { /** Initializes an `InMemoryStore` for the specified configuration @@ -74,6 +74,7 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didAddToDataStack(_ dataStack: DataStack) { self.dataStack = dataStack @@ -82,6 +83,7 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { self.dataStack = nil @@ -90,5 +92,5 @@ public final class InMemoryStore: StorageInterface, @unchecked Sendable { // MARK: Private - private weak var dataStack: DataStack? + private nonisolated(unsafe) weak var dataStack: DataStack? } diff --git a/Sources/Internals.Closure.swift b/Sources/Internals.Closure.swift index 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.DiffableDataUIDispatcher.swift b/Sources/Internals.DiffableDataUIDispatcher.swift index f9777a53..b6d783cb 100644 --- a/Sources/Internals.DiffableDataUIDispatcher.swift +++ b/Sources/Internals.DiffableDataUIDispatcher.swift @@ -32,8 +32,6 @@ import QuartzCore #endif -import os - // MARK: - Internals @@ -99,7 +97,7 @@ extension Internals { return } - let performDiffingUpdates: @MainActor () -> Void = { + let performDiffingUpdates: @MainActor @Sendable () -> Void = { let changeset = StagedChangeset(source: self.sections, target: newSections) performUpdates( @@ -148,7 +146,7 @@ extension Internals { return self.sections[section].differenceIdentifier } - func itemIdentifier(for indexPath: IndexPath) -> O.ObjectID? { + func itemIdentifier(for indexPath: IndexPath) -> NSManagedObjectID? { guard self.sections.indices.contains(indexPath.section) else { @@ -162,9 +160,9 @@ extension Internals { return items[indexPath.item].differenceIdentifier } - func indexPath(for itemIdentifier: O.ObjectID) -> IndexPath? { + func indexPath(for itemIdentifier: NSManagedObjectID) -> IndexPath? { - let indexPathMap: [O.ObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in + let indexPathMap: [NSManagedObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in for (itemIndex, item) in section.element.elements.enumerated() { @@ -243,7 +241,7 @@ extension Internals { fileprivate init() {} - fileprivate func dispatch(_ action: @escaping @MainActor () -> Void) { + fileprivate func dispatch(_ action: @escaping @MainActor @Sendable () -> Void) { let count = self.executingCount.incrementAndGet() if Thread.isMainThread && count == 1 { @@ -300,7 +298,7 @@ extension Internals { // MARK: Private - private let value: OSAllocatedUnfairLock = .init(initialState: 0) + private let value: Internals.Mutex = .init(0) } } diff --git a/Sources/Internals.EntityIdentifier.swift b/Sources/Internals.EntityIdentifier.swift index 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.Mutext.swift b/Sources/Internals.Mutext.swift new file mode 100644 index 00000000..d322129b --- /dev/null +++ b/Sources/Internals.Mutext.swift @@ -0,0 +1,115 @@ +// +// Internals.Mutex.swift +// CoreStore +// +// Copyright © 2026 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import Synchronization +import os + + +// MARK: - Internals + +extension Internals { + + // MARK: - Mutex + + internal struct Mutex: ~Copyable, @unchecked Sendable { + + // MARK: Internal + + init(_ initialValue: consuming sending Value) { + + self.storage = .init(initialValue) + } + + borrowing func withLock( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result + where E: Error, Result: ~Copyable { + + let storage = self.storage + storage.lock() + defer { + + storage.unlock() + } + return try body(&storage.value) + } + + borrowing func withLockUnchecked( + _ body: (inout sending Value) throws(E) -> Result + ) throws(E) -> sending Result + where E: Error { + + let storage = self.storage + storage.lock() + defer { + + storage.unlock() + } + return try body(&storage.value) + } + + + // MARK: Private + + private let storage: Storage + + + // MARK: - Storage + + fileprivate final class Storage { + + // MARK: FilePrivate + + var value: Value + + init(_ initialValue: consuming sending Value) { + + self.unfairLock = .allocate(capacity: 1) + self.unfairLock.initialize(to: os_unfair_lock()) + self.value = initialValue + } + + deinit { + + self.unfairLock.deinitialize(count: 1) + self.unfairLock.deallocate() + } + + func lock() { + os_unfair_lock_lock(self.unfairLock) + } + + func unlock() { + os_unfair_lock_unlock(self.unfairLock) + } + + + // MARK: Private + + private let unfairLock: os_unfair_lock_t + } + } +} diff --git a/Sources/Internals.NotificationObserver.swift b/Sources/Internals.NotificationObserver.swift index e6f36700..72cac2f9 100644 --- a/Sources/Internals.NotificationObserver.swift +++ b/Sources/Internals.NotificationObserver.swift @@ -52,6 +52,27 @@ extension Internals { using: closure ) } + + init( + notificationName: Notification.Name, + object: Any?, + closure: @escaping @MainActor (_ note: Notification) -> Void + ) { + + self.observer = NotificationCenter.default.addObserver( + forName: notificationName, + object: object, + queue: .main, + using: { note in + + nonisolated(unsafe) let note = note + MainActor.assumeIsolated { + + closure(note) + } + } + ) + } deinit { diff --git a/Sources/Internals.SharedNotificationObserver.swift b/Sources/Internals.SharedNotificationObserver.swift index 2a56c0ad..b70e7143 100644 --- a/Sources/Internals.SharedNotificationObserver.swift +++ b/Sources/Internals.SharedNotificationObserver.swift @@ -66,7 +66,7 @@ extension Internals { internal func addObserver( _ observer: U, - closure: @escaping (T) -> Void + closure: @escaping @Sendable (T) -> Void ) { self.observers.setObject( diff --git a/Sources/Internals.swift b/Sources/Internals.swift index a281d70a..eeab9851 100644 --- a/Sources/Internals.swift +++ b/Sources/Internals.swift @@ -139,15 +139,6 @@ internal enum Internals { return try ObjectiveC.autoreleasepool(invoking: closure) } - @inline(__always) - internal static func autoreleasepool( - _ closure: () throws(E) -> T - ) throws(E) -> T { - - return try ObjectiveC.autoreleasepool(invoking: closure) - } - - @inline(__always) internal static func withCheckedThrowingContinuation( function: String = #function, @@ -160,6 +151,17 @@ internal enum Internals { ) } + +#if compiler(>=27) + + @inline(__always) + internal static func autoreleasepool( + _ closure: () throws(E) -> T + ) throws(E) -> T { + + return try ObjectiveC.autoreleasepool(invoking: closure) + } + @inline(__always) internal static func withCheckedThrowingContinuation( function: String = #function, @@ -172,10 +174,12 @@ internal enum Internals { ) } +#endif + @inline(__always) internal static func mainActorImmediate( - _ body: @escaping @MainActor () -> Void + _ body: @escaping @MainActor @Sendable () -> Void ) { if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) { diff --git a/Sources/Into.swift b/Sources/Into.swift index eba923dd..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, @unchecked Sendable { +public struct Into: Hashable, Sendable { /** The associated `NSManagedObject` or `CoreStoreObject` entity class diff --git a/Sources/KeyPath+Querying.swift b/Sources/KeyPath+Querying.swift index 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 e5053be3..828b5f69 100644 --- a/Sources/ListMonitor.swift +++ b/Sources/ListMonitor.swift @@ -66,7 +66,7 @@ import CoreData ``` In the example above, both `person1` and `person2` will contain the object at section=2, index=3. */ -public final class ListMonitor: Hashable, @unchecked Sendable { +public final class ListMonitor: Hashable, Sendable { // MARK: Public (Accessors) @@ -386,8 +386,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -395,28 +397,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -433,8 +435,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObjectObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -442,28 +446,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -475,7 +479,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertObject: object, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteObject: { (observer, monitor, object, fromIndexPath) in @@ -484,7 +488,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteObject: object, fromIndexPath: fromIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didUpdateObject: { (observer, monitor, object, atIndexPath) in @@ -493,7 +497,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didUpdateObject: object, atIndexPath: atIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didMoveObject: { (observer, monitor, object, fromIndexPath, toIndexPath) in @@ -503,7 +507,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable didMoveObject: object, fromIndexPath: fromIndexPath, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -520,8 +524,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListSectionObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ListEntityType == O { + let managedObjectContext = self.managedObjectContext self.unregisterObserver(observer) self.registerObserver( observer, @@ -529,28 +535,28 @@ public final class ListMonitor: Hashable, @unchecked Sendable observer.listMonitorWillChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didChange: { (observer, monitor) in observer.listMonitorDidChange( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, willRefetch: { (observer, monitor) in observer.listMonitorWillRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didRefetch: { (observer, monitor) in observer.listMonitorDidRefetch( monitor, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -562,7 +568,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertObject: object, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteObject: { (observer, monitor, object, fromIndexPath) in @@ -571,7 +577,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteObject: object, fromIndexPath: fromIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didUpdateObject: { (observer, monitor, object, atIndexPath) in @@ -580,7 +586,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didUpdateObject: object, atIndexPath: atIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didMoveObject: { (observer, monitor, object, fromIndexPath, toIndexPath) in @@ -590,7 +596,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable didMoveObject: object, fromIndexPath: fromIndexPath, toIndexPath: toIndexPath, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -602,7 +608,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didInsertSection: sectionInfo, toSectionIndex: toIndex, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) }, didDeleteSection: { (observer, monitor, sectionInfo, fromIndex) in @@ -611,7 +617,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable monitor, didDeleteSection: sectionInfo, fromSectionIndex: fromIndex, - sourceIdentifier: monitor.fetchedResultsController.managedObjectContext.saveMetadata?.sourceIdentifier + sourceIdentifier: managedObjectContext.saveMetadata?.sourceIdentifier ) } ) @@ -624,6 +630,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter observer: a `ListObserver` to unregister notifications to */ + @MainActor public func removeObserver(_ observer: U) where U.ListEntityType == O { self.unregisterObserver(observer) @@ -635,7 +642,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable /** Returns `true` if a call to `refetch(...)` was made to the `ListMonitor` and is currently waiting for the fetching to complete. Returns `false` otherwise. */ - public private(set) var isPendingRefetch = false + public nonisolated(unsafe) private(set) var isPendingRefetch = false /** Asks the `ListMonitor` to refetch its objects using the specified series of `FetchClause`s. Note that this method does not execute the fetch immediately; the actual fetching will happen after the `NSFetchedResultsController`'s last `controllerDidChangeContent(_:)` notification completes. @@ -646,6 +653,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - Important: Starting CoreStore 4.0, all `FetchClause`s required by the `ListMonitor` should be provided in the arguments list of `refetch(...)`. */ + @MainActor public func refetch( _ fetchClauses: FetchClause..., sourceIdentifier: (any Sendable)? = nil @@ -666,6 +674,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable - parameter sourceIdentifier: an optional value that identifies the source of this transaction. This identifier will be passed to the change notifications and callers can use it for custom handling that depends on the source. - Important: Starting CoreStore 4.0, all `FetchClause`s required by the `ListMonitor` should be provided in the arguments list of `refetch(...)`. */ + @MainActor public func refetch( _ fetchClauses: [FetchClause], sourceIdentifier: (any Sendable)? = nil @@ -892,6 +901,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, willChange: @escaping @Sendable ( @@ -969,6 +979,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, didInsertObject: @escaping @Sendable ( @@ -1056,6 +1067,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func registerObserver( _ observer: U, didInsertSection: @escaping @Sendable ( @@ -1104,6 +1116,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable ) } + @MainActor internal func unregisterObserver(_ observer: AnyObject) { Internals.assert( @@ -1125,6 +1138,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable Internals.setAssociatedRetainedObject(nilValue, forKey: &self.didDeleteSectionKey, inObject: observer) } + @MainActor internal func refetch( _ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, sourceIdentifier: (any Sendable)? @@ -1154,7 +1168,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable } let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( - context: self.fetchedResultsController.managedObjectContext, + context: self.managedObjectContext, from: self.from, sectionBy: self.sectionBy, applyFetchClauses: self.applyFetchClauses @@ -1216,32 +1230,34 @@ public final class ListMonitor: Hashable, @unchecked Sendable // MARK: Private - fileprivate var fetchedResultsController: Internals.CoreStoreFetchedResultsController - fileprivate let taskGroup = DispatchGroup() - internal let sectionByIndexTransformer: (_ sectionName: KeyPathString?) -> String? - + private let managedObjectContext: NSManagedObjectContext + private let taskGroup = DispatchGroup() + private let sectionByIndexTransformer: @Sendable (_ sectionName: KeyPathString?) -> String? private let isSectioned: Bool + private 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 nonisolated(unsafe) var observerForWillChangePersistentStore: Internals.NotificationObserver! + private nonisolated(unsafe) var observerForDidChangePersistentStore: Internals.NotificationObserver! - private var isPersistentStoreChanging: Bool = false { + private nonisolated(unsafe) var fetchedResultsController: Internals.CoreStoreFetchedResultsController + private nonisolated(unsafe) var fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate + + private nonisolated(unsafe) var isPersistentStoreChanging: Bool = false { didSet { @@ -1302,18 +1318,21 @@ public final class ListMonitor: Hashable, @unchecked Sendable from: From, sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, - createAsynchronously: (@Sendable (ListMonitor) -> Void)? + createAsynchronously: (@MainActor (ListMonitor) -> Void)? ) { + self.managedObjectContext = context self.isSectioned = (sectionBy != nil) self.from = from self.sectionBy = sectionBy - (self.fetchedResultsController, self.fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( + let (fetchedResultsController, fetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( context: context, from: from, sectionBy: sectionBy, applyFetchClauses: applyFetchClauses ) + self.fetchedResultsController = fetchedResultsController + self.fetchedResultsControllerDelegate = fetchedResultsControllerDelegate if let sectionIndexTransformer = sectionBy?.sectionIndexTransformer { @@ -1325,7 +1344,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable } self.transactionQueue = transactionQueue self.applyFetchClauses = applyFetchClauses - self.fetchedResultsControllerDelegate.handler = self + fetchedResultsControllerDelegate.handler = self guard let coordinator = context.parentStack?.coordinator else { @@ -1335,8 +1354,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable self.observerForWillChangePersistentStore = Internals.NotificationObserver( notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresWillChange, object: coordinator, - queue: OperationQueue.main, - closure: { [weak self] (note) -> Void in + closure: { @MainActor [weak self] (note) -> Void in guard let self = self else { @@ -1357,8 +1375,7 @@ public final class ListMonitor: Hashable, @unchecked Sendable self.observerForDidChangePersistentStore = Internals.NotificationObserver( notificationName: NSNotification.Name.NSPersistentStoreCoordinatorStoresDidChange, object: coordinator, - queue: OperationQueue.main, - closure: { [weak self] (note) -> Void in + closure: { @MainActor [weak self] (note) -> Void in guard let self = self else { @@ -1389,7 +1406,10 @@ public final class ListMonitor: Hashable, @unchecked Sendable try! self.fetchedResultsController.performFetchFromSpecifiedStores() self.taskGroup.notify(queue: .main) { - createAsynchronously(self) + MainActor.assumeIsolated { + + createAsynchronously(self) + } } } } diff --git a/Sources/ListPublisher.SnapshotPublisher.swift b/Sources/ListPublisher.SnapshotPublisher.swift index 36285d17..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,7 @@ extension ListPublisher { // MARK: - ListSnapshotSubscription - fileprivate final class ListSnapshotSubscription: Subscription, @unchecked Sendable + fileprivate final class ListSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -115,15 +115,17 @@ extension ListPublisher { return } - Internals.mainActorImmediate { [self] in + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { @@ -142,9 +144,10 @@ extension ListPublisher { self.subscriber = nil + nonisolated(unsafe) let strongSelf = self Internals.mainActorImmediate { - self.publisher.removeObserver(self) + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index 5657f343..1520847d 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -98,7 +98,7 @@ public final class ListPublisher: Hashable { public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ListPublisher) -> Void + _ callback: @escaping @Sendable (ListPublisher) -> Void ) { Internals.assert( @@ -134,7 +134,7 @@ public final class ListPublisher: Hashable { _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ listPublisher: ListPublisher, _ sourceIdentifier: Any? ) -> Void @@ -234,6 +234,7 @@ public final class ListPublisher: Hashable { /** Used internally by CoreStore. Do not call directly. */ + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack diff --git a/Sources/ListSnapshot.SectionInfo.swift b/Sources/ListSnapshot.SectionInfo.swift index 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 3c9c9866..9cc3ba26 100644 --- a/Sources/ListState.swift +++ b/Sources/ListState.swift @@ -35,7 +35,7 @@ import SwiftUI A property wrapper type that can read `ListPublisher` changes. */ @propertyWrapper -public struct ListState: DynamicProperty { +public struct ListState: DynamicProperty { // MARK: Public @@ -67,7 +67,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ listPublisher: ListPublisher + _ listPublisher: ListPublisher ) { self._observer = .init(wrappedValue: .init(listPublisher: listPublisher)) @@ -103,7 +103,7 @@ public struct ListState: DynamicProperty { public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -145,7 +145,7 @@ public struct ListState: DynamicProperty { public init( _ clauseChain: B, in dataStack: DataStack - ) where B.ObjectType == Object { + ) where B.ObjectType == O { self.init(dataStack.publishList(clauseChain)) } @@ -179,7 +179,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, + _ from: From, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -218,7 +218,7 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, + _ from: From, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -263,8 +263,8 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: FetchClause..., in dataStack: DataStack ) { @@ -311,8 +311,8 @@ public struct ListState: DynamicProperty { */ @MainActor public init( - _ from: From, - _ sectionBy: SectionBy, + _ from: From, + _ sectionBy: SectionBy, _ fetchClauses: [FetchClause], in dataStack: DataStack ) { @@ -324,13 +324,13 @@ public struct ListState: DynamicProperty { // MARK: @propertyWrapper @MainActor - public var wrappedValue: ListSnapshot { + public var wrappedValue: ListSnapshot { return self.observer.items } @MainActor - public var projectedValue: ListPublisher { + public var projectedValue: ListPublisher { return self.observer.listPublisher } @@ -355,31 +355,28 @@ public struct ListState: DynamicProperty { @MainActor private final class Observer: Observation.Observable { - private let registrar = ObservationRegistrar() - private var _items: ListSnapshot - - let listPublisher: ListPublisher + let listPublisher: ListPublisher - var items: ListSnapshot { + nonisolated var items: ListSnapshot { get { self.registrar.access(self, keyPath: \.items) - return self._items + return self.current.withLock({ $0 }) } set { self.registrar.withMutation(of: self, keyPath: \.items) { - self._items = newValue + self.current.withLock({ $0 = newValue }) } } } - init(listPublisher: ListPublisher) { + init(listPublisher: ListPublisher) { self.listPublisher = listPublisher - self._items = listPublisher.snapshot + self.current = .init(listPublisher.snapshot) listPublisher.addObserver(self) { [weak self] (listPublisher) in @@ -395,6 +392,12 @@ public struct ListState: DynamicProperty { self.listPublisher.removeObserver(self) } + + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex> } } diff --git a/Sources/MigrationChain.swift b/Sources/MigrationChain.swift index 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/NSManagedObjectContext+CoreStore.swift b/Sources/NSManagedObjectContext+CoreStore.swift index b2d2517f..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 } } diff --git a/Sources/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift index 601ccb7b..eac7a16f 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -100,6 +100,14 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { return objects.compactMap({ self.fetchExisting($0.cs_id()) }) } + @nonobjc + public func fetchExisting( + _ objectIDs: S + ) -> [O] where S.Iterator.Element == DynamicObjectID { + + return objectIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) + } + @nonobjc public func fetchExisting( _ objectIDs: S diff --git a/Sources/NSManagedObjectContext+Transaction.swift b/Sources/NSManagedObjectContext+Transaction.swift index ab24259a..ed9f77a0 100644 --- a/Sources/NSManagedObjectContext+Transaction.swift +++ b/Sources/NSManagedObjectContext+Transaction.swift @@ -187,7 +187,7 @@ extension NSManagedObjectContext { @nonobjc internal func saveAsynchronously( sourceIdentifier: (any Sendable)?, - completion: @escaping @MainActor (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } + completion: @escaping @MainActor @Sendable (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } ) { self.perform { diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index 83a7e734..cb7c319b 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -39,7 +39,7 @@ import CoreData Observers registered via `addObserver(_:)` are not retained. `ObjectMonitor` only keeps a `weak` reference to all observers, thus keeping itself free from retain-cycles. */ -public final class ObjectMonitor: Hashable, ObjectRepresentation, @unchecked Sendable { +public final class ObjectMonitor: Hashable, ObjectRepresentation, Sendable { /** Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. @@ -71,6 +71,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ + @MainActor public func addObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) @@ -111,6 +112,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to unregister notifications to */ + @MainActor public func removeObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) @@ -165,11 +167,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.id } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -182,7 +186,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { - return dataStack.unsafeContext().objectPublisher(objectID: self.id) + return dataStack.unsafeContext().objectPublisher(managedObjectID: self.id) } public func asReadOnly(in dataStack: DataStack) -> O? { @@ -198,20 +202,20 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.id, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.id, context: context) } // MARK: Internal internal init( - objectID: O.ObjectID, + objectID: NSManagedObjectID, context: NSManagedObjectContext ) { @@ -240,9 +244,13 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController try! fetchedResultsController.performFetchFromSpecifiedStores() - self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] + self.lastCommittedAttributes.withLock { + + $0 = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] + } } + @MainActor internal func registerObserver( _ observer: U, willChangeObject: @escaping @Sendable ( @@ -303,25 +311,29 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati return } - - let previousCommitedAttributes = self.lastCommittedAttributes - let currentCommitedAttributes = object.cs_toRaw().committedValues(forKeys: nil) as! [String: NSObject] - - var changedKeys = Set() - for key in currentCommitedAttributes.keys { + let changedKeys = self.lastCommittedAttributes.withLock { - if previousCommitedAttributes[key] != currentCommitedAttributes[key] { + let previousCommitedAttributes = $0 + let currentCommitedAttributes = object.cs_toRaw().committedValues(forKeys: nil) as! [String: NSObject] + + var changedKeys = Set() + for key in currentCommitedAttributes.keys { - changedKeys.insert(key) + if previousCommitedAttributes[key] != currentCommitedAttributes[key] { + + changedKeys.insert(key) + } } + + $0 = currentCommitedAttributes + return changedKeys } - - self.lastCommittedAttributes = currentCommitedAttributes didUpdateObject(observer, monitor, object, changedKeys) } ) } + @MainActor internal func unregisterObserver(_ observer: AnyObject) { Internals.assert( @@ -343,14 +355,14 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: Private - private let id: O.ObjectID + private let id: NSManagedObjectID private let fetchedResultsController: Internals.CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate - private var lastCommittedAttributes = [String: NSObject]() + private let lastCommittedAttributes: Internals.Mutex<[String: NSObject]> = .init([:]) - private var willChangeObjectKey: Void? - private var didDeleteObjectKey: Void? - private var didUpdateObjectKey: Void? + private nonisolated(unsafe) var willChangeObjectKey: Void? + private nonisolated(unsafe) var didDeleteObjectKey: Void? + private nonisolated(unsafe) var didUpdateObjectKey: Void? private var context: NSManagedObjectContext { diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index 3149e29f..cfe5aab9 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -36,12 +36,12 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ObjectObserver: AnyObject & Sendable { +public protocol ObjectObserver: AnyObject, Sendable { /** The `DynamicObject` type for the observed object */ - associatedtype ObjectEntityType: DynamicObject & Sendable + associatedtype ObjectEntityType: DynamicObject /** Handles processing just before a change to the observed `object` occurs. (Optional) diff --git a/Sources/ObjectPublisher.SnapshotPublisher.swift b/Sources/ObjectPublisher.SnapshotPublisher.swift index a610caa6..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,7 @@ extension ObjectPublisher { // MARK: - ObjectSnapshotSubscription - fileprivate final class ObjectSnapshotSubscription: Subscription, @unchecked Sendable + fileprivate final class ObjectSnapshotSubscription: Subscription where S.Input == Output, S.Failure == Never { // MARK: FilePrivate @@ -115,15 +115,17 @@ extension ObjectPublisher { return } - Internals.mainActorImmediate { [self] in + nonisolated(unsafe) let strongSelf = self + Internals.mainActorImmediate { - self.publisher.addObserver( - self, - notifyInitial: self.emitInitialValue, - { [weak self] (publisher) in + nonisolated(unsafe) weak let weakSelf = strongSelf as Optional + strongSelf.publisher.addObserver( + strongSelf, + notifyInitial: strongSelf.emitInitialValue, + { (publisher) in guard - let self = self, + let self = weakSelf, let subscriber = self.subscriber else { @@ -142,9 +144,10 @@ extension ObjectPublisher { self.subscriber = nil + nonisolated(unsafe) let strongSelf = self Internals.mainActorImmediate { - self.publisher.removeObserver(self) + strongSelf.publisher.removeObserver(strongSelf) } } diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 6a6388ec..12d2746d 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -57,7 +57,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash /** The actual `DynamicObject` instance. Becomes `nil` if the object has been deleted. */ - public private(set) lazy var object: O? = self.context.fetchExisting(self.id) + public private(set) lazy var object: O? = self.context.fetchExisting(self.managedObjectID) @@ -80,7 +80,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public func addObserver( _ observer: T, notifyInitial: Bool = false, - _ callback: @escaping (ObjectPublisher) -> Void + _ callback: @escaping @Sendable (ObjectPublisher) -> Void ) { Internals.assert( @@ -118,7 +118,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash _ observer: T, notifyInitial: Bool = false, initialSourceIdentifier: Any? = nil, - _ callback: @escaping ( + _ callback: @escaping @Sendable ( _ objectPublisher: ObjectPublisher, _ sourceIdentifier: Any? ) -> Void @@ -160,11 +160,13 @@ public final class ObjectPublisher: ObjectRepresentation, Hash // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { - return self.id + return self.managedObjectID } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -182,17 +184,17 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self } - return context.objectPublisher(objectID: self.id) + return context.objectPublisher(managedObjectID: self.managedObjectID) } public func asReadOnly(in dataStack: DataStack) -> O? { - return dataStack.unsafeContext().fetchExisting(self.id) + return dataStack.unsafeContext().fetchExisting(self.managedObjectID) } public func asEditable(in transaction: BaseDataTransaction) -> O? { - return transaction.unsafeContext().fetchExisting(self.id) + return transaction.unsafeContext().fetchExisting(self.managedObjectID) } public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { @@ -202,7 +204,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self.lazySnapshot } - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { @@ -212,7 +214,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash return self.lazySnapshot } - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } @@ -220,7 +222,7 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public static func == (_ lhs: ObjectPublisher, _ rhs: ObjectPublisher) -> Bool { - return lhs.id == rhs.id + return lhs.managedObjectID == rhs.managedObjectID && lhs.context == rhs.context } @@ -229,24 +231,24 @@ public final class ObjectPublisher: ObjectRepresentation, Hash public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(self.managedObjectID) hasher.combine(self.context) } // MARK: Internal - internal var cs_objectID: O.ObjectID { - - return self.objectID() - } + internal let managedObjectID: NSManagedObjectID - internal static func createUncached(objectID: O.ObjectID, context: NSManagedObjectContext) -> ObjectPublisher { + internal static func createUncached( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext + ) -> ObjectPublisher { return self.init( - objectID: objectID, + managedObjectID: managedObjectID, context: context, - initializer: ObjectSnapshot.init(objectID:context:) + initializer: ObjectSnapshot.init(managedObjectID:context:) ) } @@ -261,43 +263,52 @@ public final class ObjectPublisher: ObjectRepresentation, Hash fileprivate typealias ObserverClosureType = Internals.Closure<(objectPublisher: ObjectPublisher, sourceIdentifier: Any?), Void> - fileprivate init(objectID: O.ObjectID, context: NSManagedObjectContext, initializer: @escaping (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot?) { + fileprivate init( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext, + initializer: @escaping @Sendable (NSManagedObjectID, NSManagedObjectContext) -> ObjectSnapshot? + ) { - self.id = objectID + self.managedObjectID = managedObjectID self.context = context self.$lazySnapshot.initialize { [weak self] in guard let self = self else { - return initializer(objectID, context) - } - 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/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index 0ad37874..c30114f1 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 } @@ -98,7 +100,7 @@ extension DynamicObject where Self: ObjectRepresentation { return self.cs_toRaw() .managedObjectContext - .map({ $0.objectPublisher(objectID: self.cs_id()) }) + .map({ $0.objectPublisher(managedObjectID: self.cs_id()) }) } /** @@ -108,17 +110,19 @@ extension DynamicObject where Self: ObjectRepresentation { return self.cs_toRaw() .managedObjectContext - .flatMap({ ObjectSnapshot(objectID: self.cs_id(), context: $0) }) + .flatMap({ ObjectSnapshot(managedObjectID: self.cs_id(), context: $0) }) } // MARK: AnyObjectRepresentation - public func objectID() -> Self.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { return self.cs_id() } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.cs_toRaw().managedObjectContext?.parentStack @@ -130,7 +134,7 @@ extension DynamicObject where Self: ObjectRepresentation { public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() - return context.objectPublisher(objectID: self.cs_id()) + return context.objectPublisher(managedObjectID: self.cs_id()) } public func asReadOnly(in dataStack: DataStack) -> Self? { @@ -156,12 +160,12 @@ extension DynamicObject where Self: ObjectRepresentation { public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.cs_id(), context: context) + return ObjectSnapshot(managedObjectID: self.cs_id(), context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.cs_id(), context: context) + return ObjectSnapshot(managedObjectID: self.cs_id(), context: context) } } diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index b4b93ad0..f3c7a969 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -40,7 +40,7 @@ import AppKit The `ObjectSnapshot` is a full copy of a `DynamicObject`'s properties at a given point in time. This is useful especially when keeping thread-safe state values, in ViewModels for example. Since this is a value type, any changes in this `struct` does not affect the actual object. */ @dynamicMemberLookup -public struct ObjectSnapshot: ObjectRepresentation, Hashable, @unchecked Sendable { +public struct ObjectSnapshot: ObjectRepresentation, Hashable, Sendable { // MARK: Public @@ -52,11 +52,13 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, // MARK: AnyObjectRepresentation - public func objectID() -> O.ObjectID { + @_spi(Internals) + public func cs_id() -> NSManagedObjectID { - return self.id + return self.managedObjectID } + @_spi(Internals) public func cs_dataStack() -> DataStack? { return self.context.parentStack @@ -70,29 +72,29 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() - return context.objectPublisher(objectID: self.id) + return context.objectPublisher(managedObjectID: self.managedObjectID) } public func asReadOnly(in dataStack: DataStack) -> O? { - return dataStack.unsafeContext().fetchExisting(self.id) + return dataStack.unsafeContext().fetchExisting(self.managedObjectID) } public func asEditable(in transaction: BaseDataTransaction) -> O? { - return transaction.unsafeContext().fetchExisting(self.id) + return transaction.unsafeContext().fetchExisting(self.managedObjectID) } public func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot? { let context = dataStack.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(objectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } @@ -100,7 +102,7 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public static func == (_ lhs: Self, _ rhs: Self) -> Bool { - return lhs.id == rhs.id + return lhs.managedObjectID == rhs.managedObjectID && (lhs.generation == rhs.generation || lhs.valuesRef == rhs.valuesRef) } @@ -109,34 +111,34 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, public func hash(into hasher: inout Hasher) { - hasher.combine(self.id) + hasher.combine(self.managedObjectID) hasher.combine(self.valuesRef) } // MARK: Internal + + internal let managedObjectID: NSManagedObjectID - internal init?(objectID: O.ObjectID, context: NSManagedObjectContext) { + internal init?( + managedObjectID: NSManagedObjectID, + context: NSManagedObjectContext + ) { - guard let values = O.cs_snapshotDictionary(id: objectID, context: context) else { + guard let values = O.cs_snapshotDictionary(managedObjectID: managedObjectID, context: context) else { return nil } - self.id = objectID + self.managedObjectID = managedObjectID self.context = context self.values = values self.generation = .init() } - - internal var cs_objectID: O.ObjectID { - - return self.objectID() - } // MARK: FilePrivate - fileprivate var values: [String: Any] { + fileprivate nonisolated(unsafe) var values: [String: Any] { didSet { @@ -147,7 +149,6 @@ public struct ObjectSnapshot: ObjectRepresentation, Hashable, // MARK: Private - private let id: O.ObjectID private let context: NSManagedObjectContext private var generation: UUID @@ -344,16 +345,16 @@ extension ObjectSnapshot where O: CoreStoreObject { get { let key = String(keyPath: member) - guard let id = self.values[key] as? D.ObjectID else { + guard let id = self.values[key] as? NSManagedObjectID else { return nil } - return self.context.objectPublisher(objectID: id) + return self.context.objectPublisher(managedObjectID: id) } set { let key = String(keyPath: member) - self.values[key] = newValue?.objectID() + self.values[key] = newValue?.cs_id() } } @@ -365,13 +366,13 @@ extension ObjectSnapshot where O: CoreStoreObject { let key = String(keyPath: member) let context = self.context - let ids = self.values[key] as! [D.ObjectID] - return ids.map(context.objectPublisher(objectID:)) + let ids = self.values[key] as! [NSManagedObjectID] + return ids.map(context.objectPublisher(managedObjectID:)) } set { let key = String(keyPath: member) - self.values[key] = newValue.map({ $0.objectID() }) + self.values[key] = newValue.map({ $0.cs_id() }) } } @@ -383,13 +384,13 @@ extension ObjectSnapshot where O: CoreStoreObject { let key = String(keyPath: member) let context = self.context - let ids = self.values[key] as! Set - return Set(ids.map(context.objectPublisher(objectID:))) + let ids = self.values[key] as! Set + return Set(ids.map(context.objectPublisher(managedObjectID:))) } set { let key = String(keyPath: member) - self.values[key] = Set(newValue.map({ $0.objectID() })) + self.values[key] = Set(newValue.map({ $0.cs_id() })) } } } diff --git a/Sources/ObjectState.swift b/Sources/ObjectState.swift index efa336f4..bf6156db 100644 --- a/Sources/ObjectState.swift +++ b/Sources/ObjectState.swift @@ -103,23 +103,20 @@ public struct ObjectState: DynamicProperty { @MainActor private final class Observer: Observation.Observable { - private let registrar = ObservationRegistrar() - private var _item: ObjectSnapshot? - let objectPublisher: ObjectPublisher? - var item: ObjectSnapshot? { + nonisolated var item: ObjectSnapshot? { get { self.registrar.access(self, keyPath: \.item) - return self._item + return self.current.withLock({ $0 }) } set { self.registrar.withMutation(of: self, keyPath: \.item) { - self._item = newValue + self.current.withLock({ $0 = newValue }) } } } @@ -132,12 +129,12 @@ public struct ObjectState: DynamicProperty { else { self.objectPublisher = nil - self._item = nil + self.current = .init(nil) return } self.objectPublisher = objectPublisher - self._item = objectPublisher.snapshot + self.current = .init(objectPublisher.snapshot) objectPublisher.addObserver(self) { [weak self] (objectPublisher) in @@ -153,6 +150,12 @@ public struct ObjectState: DynamicProperty { self.objectPublisher?.removeObserver(self) } + + + // MARK: Private + + private let registrar = ObservationRegistrar() + private let current: Internals.Mutex?> } } diff --git a/Sources/OrderBy.swift b/Sources/OrderBy.swift index 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/Progress+Convenience.swift b/Sources/Progress+Convenience.swift index 55992513..2a46d64b 100644 --- a/Sources/Progress+Convenience.swift +++ b/Sources/Progress+Convenience.swift @@ -37,7 +37,7 @@ extension Progress { */ @nonobjc @MainActor - public func setProgressHandler(_ closure: (@MainActor (_ progress: Progress) -> Void)?) { + public func setProgressHandler(_ closure: (@MainActor @Sendable (_ progress: Progress) -> Void)?) { self.progressObserver.progressHandler = closure } @@ -81,12 +81,12 @@ extension Progress { // MARK: - ProgressObserver @objc -private final class ProgressObserver: NSObject, @unchecked Sendable { +private final class ProgressObserver: NSObject, Sendable { private unowned let progress: Progress @MainActor - fileprivate var progressHandler: (@MainActor (_ progress: Progress) -> Void)? { + fileprivate var progressHandler: (@MainActor @Sendable (_ progress: Progress) -> Void)? { didSet { @@ -127,7 +127,12 @@ private final class ProgressObserver: NSObject, @unchecked Sendable { } } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) { guard let progress = object as? Progress, progress == self.progress, diff --git a/Sources/Relationship.ToManyOrdered.swift b/Sources/Relationship.ToManyOrdered.swift index 26bb8800..10bfeee9 100644 --- a/Sources/Relationship.ToManyOrdered.swift +++ b/Sources/Relationship.ToManyOrdered.swift @@ -196,7 +196,7 @@ extension RelationshipContainer { internal var valueForSnapshot: Any? { - return self.value.map({ $0.objectID() }) + return self.value.map({ $0.cs_id() }) } private init(keyPath: String, minCount: Int, maxCount: Int, inverseKeyPath: @escaping () -> String?, deleteRule: DeleteRule, versionHashModifier: @autoclosure @escaping () -> String?, renamingIdentifier: @autoclosure @escaping () -> String?, affectedByKeyPaths: @autoclosure @escaping () -> Set) { diff --git a/Sources/Relationship.ToManyUnordered.swift b/Sources/Relationship.ToManyUnordered.swift index f07ca917..fe3818fc 100644 --- a/Sources/Relationship.ToManyUnordered.swift +++ b/Sources/Relationship.ToManyUnordered.swift @@ -196,7 +196,7 @@ extension RelationshipContainer { internal var valueForSnapshot: Any? { - return Set(self.value.map({ $0.objectID() })) + return Set(self.value.map({ $0.cs_id() })) } private init(keyPath: KeyPathString, inverseKeyPath: @escaping () -> KeyPathString?, deleteRule: DeleteRule, minCount: Int, maxCount: Int, versionHashModifier: @autoclosure @escaping () -> String?, renamingIdentifier: @autoclosure @escaping () -> String?, affectedByKeyPaths: @autoclosure @escaping () -> Set) { diff --git a/Sources/Relationship.ToOne.swift b/Sources/Relationship.ToOne.swift index 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 3f762b89..ccb339f3 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -33,7 +33,7 @@ import CoreData - Warning: The default SQLite file location for the `LegacySQLiteStore` and `SQLiteStore` are different. If the app was depending on CoreStore's default directories prior to 2.0.0, make sure to use the `SQLiteStore.legacy(...)` factory methods to create the `SQLiteStore` instead of using initializers directly. */ -public final class SQLiteStore: LocalStorage, @unchecked Sendable { +public final class SQLiteStore: LocalStorage { /** Initializes an SQLite store interface from the given SQLite file URL. When this instance is passed to the `DataStack`'s `addStorage()` methods, a new SQLite file will be created if it does not exist. @@ -176,6 +176,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didAddToDataStack(_ dataStack: DataStack) { self.dataStack = dataStack @@ -184,6 +185,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) public func cs_didRemoveFromDataStack(_ dataStack: DataStack) { self.dataStack = nil @@ -205,7 +207,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Options that tell the `DataStack` how to setup the persistent store */ - public var localStorageOptions: LocalStorageOptions + public let localStorageOptions: LocalStorageOptions /** The options dictionary for the specified `LocalStorageOptions` @@ -231,6 +233,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Called by the `DataStack` to perform checkpoint operations on the storage. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE to force a checkpoint. */ + @_spi(Internals) public func cs_finalizeStorageAndWait( soureModelHint: NSManagedObjectModel ) throws(any Swift.Error) { @@ -252,6 +255,7 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ + @_spi(Internals) public func cs_eraseStorageAndWait( metadata: [String: Any], soureModelHint: NSManagedObjectModel? @@ -375,5 +379,5 @@ public final class SQLiteStore: LocalStorage, @unchecked Sendable { // MARK: Private - private weak var dataStack: DataStack? + private nonisolated(unsafe) weak var dataStack: DataStack? } diff --git a/Sources/SchemaHistory.swift b/Sources/SchemaHistory.swift index 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/StorageInterface.swift b/Sources/StorageInterface.swift index 51bc608c..de929a9b 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -54,11 +54,13 @@ public protocol StorageInterface: AnyObject, Sendable { /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) func cs_didAddToDataStack(_ dataStack: DataStack) /** Do not call directly. Used by the `DataStack` internally. */ + @_spi(Internals) func cs_didRemoveFromDataStack(_ dataStack: DataStack) } @@ -146,6 +148,7 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform checkpoint operations on the storage. (SQLite stores for example, can convert the database's WAL journaling mode to DELETE to force a checkpoint) */ + @_spi(Internals) func cs_finalizeStorageAndWait( soureModelHint: NSManagedObjectModel ) throws(any Swift.Error) @@ -153,6 +156,7 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ + @_spi(Internals) func cs_eraseStorageAndWait( metadata: [String: Any], soureModelHint: NSManagedObjectModel? diff --git a/Sources/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index c1dbad59..a219a51d 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -32,7 +32,8 @@ import CoreData /** The `SynchronousDataTransaction` provides an interface for `DynamicObject` creates, updates, and deletes. A transaction object should typically be only used from within a transaction block initiated from `DataStack.beginSynchronous(_:)`. */ -public final class SynchronousDataTransaction: BaseDataTransaction { +@_nonSendable +public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { /** Cancels a transaction by throwing `CoreStoreError.userCancelled`. @@ -67,6 +68,23 @@ public final class SynchronousDataTransaction: BaseDataTransaction { return super.create(into) } + /** + Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. + + - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ persistentID: DynamicObjectID? + ) -> O? { + + Internals.assert( + !self.isCommitted, + "Attempted to update an entity for \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))." + ) + return super.edit(persistentID) + } + /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. diff --git a/Sources/Tweak.swift b/Sources/Tweak.swift index 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/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? } From 1ea9ad7c4a7da978a2c95878a1b1c67b355940b5 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 15 Jul 2026 19:54:47 +0900 Subject: [PATCH 3/7] fix tests --- CoreStore.xcodeproj/project.pbxproj | 4 +- ...ed.EvolutionDemo.CreaturesDataSource.swift | 4 +- ...lassic.ColorsDemo.ListViewController.swift | 75 +++++++++++++------ Sources/CoreStoreObject.swift | 2 - Sources/DataStack+Observing.swift | 2 +- Sources/DynamicObject.swift | 8 -- Sources/DynamicObjectID.swift | 5 ++ Sources/ObjectMonitor.swift | 29 ++++--- Sources/ObjectPublisher.swift | 5 ++ Sources/ObjectRepresentation.swift | 14 ++++ Sources/ObjectSnapshot.swift | 5 ++ Sources/UnsafeDataTransaction+Observing.swift | 2 +- 12 files changed, 103 insertions(+), 52 deletions(-) diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 76e70ec8..ca9e981c 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -3047,7 +3047,7 @@ 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 = 17.0; VERSIONING_SYSTEM = "apple-generic"; @@ -3112,7 +3112,7 @@ 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 = 17.0; VALIDATE_PRODUCT = YES; diff --git a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift index 2efc9e20..87e6c42a 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.CreaturesDataSource.swift @@ -50,11 +50,11 @@ 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) }, 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/Sources/CoreStoreObject.swift b/Sources/CoreStoreObject.swift index 2307beb2..e848c85d 100644 --- a/Sources/CoreStoreObject.swift +++ b/Sources/CoreStoreObject.swift @@ -65,7 +65,6 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { Do not call this directly. This is exposed as public only as a required initializer. - Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations. */ - @_spi(Internals) public required init(rawObject: NSManagedObject) { self.isMeta = false @@ -85,7 +84,6 @@ open /*abstract*/ class CoreStoreObject: DynamicObject, Hashable { Do not call this directly. This is exposed as public only as a required initializer. - Important: subclasses that need a custom initializer should override both `init(rawObject:)` and `init(asMeta:)`, and to call their corresponding super implementations. */ - @_spi(Internals) public required init(asMeta: Void) { self.isMeta = true diff --git a/Sources/DataStack+Observing.swift b/Sources/DataStack+Observing.swift index 8dc9572b..b16333e0 100644 --- a/Sources/DataStack+Observing.swift +++ b/Sources/DataStack+Observing.swift @@ -46,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()) } /** diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index fb95c462..86417470 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -91,14 +91,6 @@ public nonisolated protocol DynamicObject: AnyObject, SendableMetatype { extension DynamicObject { - // MARK: Public - - public func persistentID() -> DynamicObjectID { - - return .init(managedObjectID: self.cs_id()) - } - - // MARK: Internal internal func runtimeType() -> Self.Type { diff --git a/Sources/DynamicObjectID.swift b/Sources/DynamicObjectID.swift index a2fb43f0..0959c654 100644 --- a/Sources/DynamicObjectID.swift +++ b/Sources/DynamicObjectID.swift @@ -95,6 +95,11 @@ public struct DynamicObjectID: Hashable, ObjectRepresentation, public typealias ObjectType = O + public func persistentID() -> ObjectType.ObjectID { + + return self + } + public func asPublisher(in dataStack: DataStack) -> ObjectPublisher { let context = dataStack.unsafeContext() diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index cb7c319b..cc994e1e 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -170,7 +170,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati @_spi(Internals) public func cs_id() -> NSManagedObjectID { - return self.id + return self.managedObjectID } @_spi(Internals) @@ -184,43 +184,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(managedObjectID: 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(managedObjectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } public func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? { let context = transaction.unsafeContext() - return ObjectSnapshot(managedObjectID: self.id, context: context) + return ObjectSnapshot(managedObjectID: self.managedObjectID, context: context) } // MARK: Internal internal init( - objectID: NSManagedObjectID, + managedObjectID: NSManagedObjectID, context: NSManagedObjectContext ) { let fetchRequest = Internals.CoreStoreFetchRequest() - fetchRequest.entity = objectID.entity + fetchRequest.entity = managedObjectID.entity fetchRequest.fetchLimit = 0 fetchRequest.resultType = .managedObjectResultType fetchRequest.sortDescriptors = [] @@ -230,13 +235,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 @@ -355,7 +360,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati // MARK: Private - private let id: NSManagedObjectID + private let managedObjectID: NSManagedObjectID private let fetchedResultsController: Internals.CoreStoreFetchedResultsController private let fetchedResultsControllerDelegate: Internals.FetchedResultsControllerDelegate private let lastCommittedAttributes: Internals.Mutex<[String: NSObject]> = .init([:]) diff --git a/Sources/ObjectPublisher.swift b/Sources/ObjectPublisher.swift index 12d2746d..c239cccd 100644 --- a/Sources/ObjectPublisher.swift +++ b/Sources/ObjectPublisher.swift @@ -177,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() diff --git a/Sources/ObjectRepresentation.swift b/Sources/ObjectRepresentation.swift index c30114f1..38ff5bc9 100644 --- a/Sources/ObjectRepresentation.swift +++ b/Sources/ObjectRepresentation.swift @@ -59,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. */ @@ -85,6 +90,7 @@ public protocol ObjectRepresentation: AnyObjectRepresentation { func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot? } + extension NSManagedObject: ObjectRepresentation {} extension CoreStoreObject: ObjectRepresentation {} @@ -92,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 diff --git a/Sources/ObjectSnapshot.swift b/Sources/ObjectSnapshot.swift index f3c7a969..042329e3 100644 --- a/Sources/ObjectSnapshot.swift +++ b/Sources/ObjectSnapshot.swift @@ -69,6 +69,11 @@ 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() diff --git a/Sources/UnsafeDataTransaction+Observing.swift b/Sources/UnsafeDataTransaction+Observing.swift index b56ae47a..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()) } /** From 13093d72d46672714f77d52ec03272496360cd6d Mon Sep 17 00:00:00 2001 From: John Estropia Date: Fri, 24 Jul 2026 11:51:36 +0900 Subject: [PATCH 4/7] cleanup --- CoreStoreTests/ImportTests.swift | 6 +- CoreStoreTests/SwiftUIReaderStateTests.swift | 545 ++++++++++++++++++ Demo/Demo.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/xcschemes/Demo.xcscheme | 18 +- ...ssic.ColorsDemo.DetailViewController.swift | 26 +- .../Modern.ColorsDemo.MainView.swift | 1 + ...Modern.ColorsDemo.SwiftUI.DetailView.swift | 8 +- .../Modern.ColorsDemo.SwiftUI.ItemView.swift | 1 + .../Modern.ColorsDemo.SwiftUI.ListView.swift | 3 +- ...olorsDemo.UIKit.DetailViewController.swift | 19 +- ....ColorsDemo.UIKit.ListViewController.swift | 2 +- .../Modern.PlacemarksDemo.MainView.swift | 6 +- .../PokedexDemo/Modern.PokedexDemo.Form.swift | 2 +- .../Modern.PokedexDemo.MainView.swift | 1 + .../Modern.PokedexDemo.PokedexEntry.swift | 2 +- .../Modern.PokedexDemo.Service.swift | 60 +- .../Modern.PokedexDemo.Species.swift | 9 +- ...PokedexDemo.UIKit.ListViewController.swift | 28 +- Demo/Sources/Helpers/Menu/Menu.MainView.swift | 96 +-- .../Helpers/WithMainActorImmediate.swift | 31 + README.md | 21 +- Sources/AsynchronousDataTransaction.swift | 47 +- Sources/BaseDataTransaction+Importing.swift | 12 +- Sources/BaseDataTransaction.swift | 46 +- Sources/CoreStoreError.swift | 6 +- Sources/CustomSchemaMappingProvider.swift | 8 +- Sources/DataStack+Concurrency.swift | 18 +- Sources/DataStack+Reactive.swift | 4 +- Sources/DataStack+Transaction.swift | 6 +- Sources/DataStack.swift | 2 +- ...aSource.CollectionViewAdapter-AppKit.swift | 2 +- Sources/DispatchQueue+CoreStore.swift | 2 +- Sources/DynamicObject.swift | 8 +- Sources/ImportableObject.swift | 2 +- Sources/ImportableUniqueObject.swift | 8 +- ...ls.CoreStoreFetchedResultsController.swift | 4 +- Sources/Internals.Mutext.swift | 4 +- Sources/Internals.swift | 8 +- Sources/ListPublisher.swift | 6 +- Sources/ListState.swift | 43 +- Sources/NSManagedObject+Convenience.swift | 8 +- Sources/NSManagedObjectContext+Querying.swift | 12 +- .../NSPersistentStoreCoordinator+Setup.swift | 2 +- Sources/ObjectMonitor.swift | 25 +- Sources/ObjectObserver.swift | 36 +- Sources/ObjectReader.swift | 2 +- Sources/ObjectState.swift | 59 +- Sources/SQLiteStore.swift | 6 +- Sources/StorageInterface.swift | 4 +- Sources/SynchronousDataTransaction.swift | 37 ++ Sources/UnsafeDataTransaction.swift | 2 +- 51 files changed, 1080 insertions(+), 240 deletions(-) create mode 100644 CoreStoreTests/SwiftUIReaderStateTests.swift create mode 100644 Demo/Sources/Helpers/WithMainActorImmediate.swift diff --git a/CoreStoreTests/ImportTests.swift b/CoreStoreTests/ImportTests.swift index 4c98ce2b..71c0e520 100644 --- a/CoreStoreTests/ImportTests.swift +++ b/CoreStoreTests/ImportTests.swift @@ -1060,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 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/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 1f0df5e3..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 */; }; @@ -38,7 +39,7 @@ 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 */; }; @@ -115,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 = ""; }; @@ -366,6 +368,7 @@ B5A3917A24E6A75F00E7E8BD /* Helpers */ = { isa = PBXGroup; children = ( + 5F035E4E300F725800E98F8F /* WithMainActorImmediate.swift */, B5A3917F24E787D900E7E8BD /* InstructionsView.swift */, B5E32C8F24FA41F9003F46AD /* ImageDownloader.swift */, B5A3915424E6857F00E7E8BD /* Menu */, @@ -685,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 */, 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"> + + + + , - 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/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift index eb81d407..5b0b5ee2 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -11,6 +11,7 @@ extension Modern.ColorsDemo { // MARK: - Modern.ColorsDemo.MainView + @MainActor struct MainView: View { // MARK: Internal 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 5e74d66f..8dc59dca 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.DetailView.swift @@ -11,6 +11,7 @@ extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.DetailView + @MainActor struct DetailView: View { /** @@ -33,6 +34,7 @@ extension Modern.ColorsDemo.SwiftUI { init(_ palette: ObjectPublisher) { + let persistentID = palette.persistentID() self._palette = .init(palette) self._hue = Binding( get: { palette.hue ?? 0 }, @@ -41,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 } @@ -55,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 } @@ -69,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 } 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 ed2e0442..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 { /** 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 52149d01..0a59f056 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.SwiftUI.ListView.swift @@ -11,6 +11,7 @@ extension Modern.ColorsDemo.SwiftUI { // MARK: - Modern.ColorsDemo.SwiftUI.ListView + @MainActor struct ListView: View { /** @@ -81,7 +82,7 @@ extension Modern.ColorsDemo.SwiftUI { Modern.ColorsDemo.dataStack.perform( asynchronous: { transaction in - transaction.delete(objectIDs: objectIDsToDelete) + transaction.delete(persistentIDs: objectIDsToDelete) }, completion: { _ in } ) 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 afb1ed10..d1e36261 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.DetailViewController.swift @@ -92,7 +92,7 @@ extension Modern.ColorsDemo.UIKit { _ monitor: ObjectMonitor, didUpdateObject object: sending Modern.ColorsDemo.Palette, changedPersistentKeys: Set, - sourceIdentifier: Any? + sourceIdentifier: (any Sendable)? ) { MainActor.assumeIsolated { @@ -253,10 +253,11 @@ extension Modern.ColorsDemo.UIKit { 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 } @@ -268,10 +269,11 @@ extension Modern.ColorsDemo.UIKit { 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 } @@ -283,10 +285,11 @@ extension Modern.ColorsDemo.UIKit { 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 } @@ -294,5 +297,3 @@ extension Modern.ColorsDemo.UIKit { } } } - - 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 e82b46f7..297e2b95 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.UIKit.ListViewController.swift @@ -81,7 +81,7 @@ extension Modern.ColorsDemo.UIKit { self.dataStack.perform( asynchronous: { (transaction) in - transaction.delete(objectIDs: [itemID]) + transaction.delete(itemID) }, sourceIdentifier: Modern.ColorsDemo.TransactionSource.delete, completion: { _ in } diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift index 7d5dfff9..de920bf4 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift @@ -14,6 +14,7 @@ extension Modern.PlacemarksDemo { // MARK: - Modern.PlacemarksDemo.MainView + @MainActor struct MainView: View { /** @@ -21,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 } @@ -107,7 +109,7 @@ extension Modern.PlacemarksDemo { return } let geocoded = await self.geocoder.geocode(place: place) - guard self.place?.objectID() == place.objectID() else { + guard self.place?.persistentID() == place.persistentID() else { return } 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 a25b7d20..7abb5b3e 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.MainView.swift @@ -11,6 +11,7 @@ extension Modern.PokedexDemo { // MARK: - Modern.PokedexDemo.MainView + @MainActor struct MainView: View { // MARK: Internal 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 37cd65d5..0729f13b 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -21,7 +21,7 @@ extension Modern.PokedexDemo { /** ⭐️ Sample 1: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are tuples */ - private static func importPokedexEntries(from data: Data) async throws { + private static func importPokedexEntries(from data: Data) async throws(Modern.PokedexDemo.Service.Error) { do { @@ -43,7 +43,7 @@ extension Modern.PokedexDemo { } catch { - throw self.mapError(error) + throw self.mapError(.init(error)) } } @@ -51,31 +51,31 @@ extension Modern.PokedexDemo { ⭐️ Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary` */ private static func importSpecies( - for detailsObjectID: NSManagedObjectID, + for detailsPersistentID: DynamicObjectID, from data: Data ) async throws -> ObjectSnapshot { - let speciesObjectID = try await Modern.PokedexDemo.dataStack.async.perform { transaction -> NSManagedObjectID in + let speciesPersistentID = try await Modern.PokedexDemo.dataStack.async.perform { transaction in - let json: Dictionary = try self.parseJSON( - try JSONSerialization.jsonObject(with: data, options: []) - ) - guard - let species = try transaction.importUniqueObject( - Into(), - source: json + let json: Dictionary = try self.parseJSON( + try JSONSerialization.jsonObject(with: data, options: []) ) - else { + guard + let species = try transaction.importUniqueObject( + Into(), + source: .init(json: json) + ) + else { throw Modern.PokedexDemo.Service.Error.unexpected } transaction - .edit(Into(), detailsObjectID)? + .edit(Into(), detailsPersistentID)? .species = species - return species.objectID() + return species.persistentID() } guard - let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesObjectID), + let species: Modern.PokedexDemo.Species = Modern.PokedexDemo.dataStack.fetchExisting(speciesPersistentID), let snapshot = species.asSnapshot() else { @@ -88,7 +88,7 @@ extension Modern.PokedexDemo { ⭐️ Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s */ private static func importForms( - for detailsObjectID: NSManagedObjectID, + for detailsPersistentID: DynamicObjectID, from dataArray: [Data] ) async throws { @@ -110,13 +110,13 @@ extension Modern.PokedexDemo { throw Modern.PokedexDemo.Service.Error.unexpected } transaction - .edit(Into(), detailsObjectID)? + .edit(Into(), detailsPersistentID)? .forms = forms } } catch { - throw self.mapError(error) + throw self.mapError(.init(error)) } } @@ -195,8 +195,8 @@ extension Modern.PokedexDemo { if let species = details.$species?.snapshot { self.fetchFormsIfNeeded( - key: species.$id, - detailsObjectID: details.objectID(), + key: String(species.$id), + detailsPersistentID: details.persistentID(), species: species ) return @@ -207,7 +207,7 @@ extension Modern.PokedexDemo { return } let speciesURL = pokedexEntry.$speciesURL - let detailsObjectID = details.objectID() + let detailsPersistentID = details.persistentID() self.detailTasks[key] = Task { [weak self] in guard let self else { @@ -220,7 +220,7 @@ extension Modern.PokedexDemo { } await self.fetchSpecies( key: key, - detailsObjectID: detailsObjectID, + detailsPersistentID: detailsPersistentID, speciesURL: speciesURL ) } @@ -289,7 +289,7 @@ extension Modern.PokedexDemo { private func fetchSpecies( key: String, - detailsObjectID: NSManagedObjectID, + detailsPersistentID: DynamicObjectID, speciesURL: URL ) async { @@ -299,7 +299,7 @@ extension Modern.PokedexDemo { try Task.checkCancellation() let species = try await Self.importSpecies( - for: detailsObjectID, + for: detailsPersistentID, from: data ) guard species.$details?.snapshot?.$forms.isEmpty == true else { @@ -307,7 +307,7 @@ extension Modern.PokedexDemo { return } await self.fetchForms( - detailsObjectID: detailsObjectID, + detailsPersistentID: detailsPersistentID, formsURLs: species.$formsURLs ) } @@ -331,7 +331,7 @@ extension Modern.PokedexDemo { private func fetchFormsIfNeeded( key: String, - detailsObjectID: NSManagedObjectID, + detailsPersistentID: DynamicObjectID, species: ObjectSnapshot ) { @@ -356,14 +356,14 @@ extension Modern.PokedexDemo { self.detailTasks.removeValue(forKey: key) } await self.fetchForms( - detailsObjectID: detailsObjectID, + detailsPersistentID: detailsPersistentID, formsURLs: formsURLs ) } } private func fetchForms( - detailsObjectID: NSManagedObjectID, + detailsPersistentID: DynamicObjectID, formsURLs: [URL] ) async { @@ -378,7 +378,7 @@ extension Modern.PokedexDemo { dataArray.append(data) } try await Self.importForms( - for: detailsObjectID, + for: detailsPersistentID, from: dataArray ) } @@ -408,7 +408,7 @@ extension Modern.PokedexDemo { 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.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/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift index 4373f770..b7406e5b 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -13,52 +13,70 @@ extension Menu { // MARK: - Menu.MainView struct MainView: View { - - @State - private var selection: Menu.Route? - + + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass // MARK: View + @ViewBuilder var body: some View { - - NavigationSplitView( - sidebar: { - - List(selection: self.$selection) { - - ForEach(Menu.Section.allCases, id: \.self) { section in - - Section(section.rawValue) { - - ForEach(section.routes) { route in - - Menu.ItemView( - title: route.title, - subtitle: route.subtitle, - isEnabled: route.isEnabled - ) - .tag(route as Menu.Route?) - .disabled(!route.isEnabled) - } - } - } - } - .navigationTitle("CoreStore Demos") - .listStyle(.sidebar) - }, - detail: { - - if let selection = self.selection { - - selection.destination - } - else { - + + 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: { + route.destination + }, + label: { + Menu.ItemView( + title: route.title, + subtitle: route.subtitle, + isEnabled: route.isEnabled + ) + } + ) + .disabled(!route.isEnabled) + } + }, + header: { + + Text(section.rawValue) + } + ) } - ) + } + .navigationTitle("CoreStore Demos") + .listStyle(.sidebar) } } } diff --git a/Demo/Sources/Helpers/WithMainActorImmediate.swift b/Demo/Sources/Helpers/WithMainActorImmediate.swift new file mode 100644 index 00000000..2f3aad35 --- /dev/null +++ b/Demo/Sources/Helpers/WithMainActorImmediate.swift @@ -0,0 +1,31 @@ +// +// 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/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 4e5546c1..eafe4165 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -80,19 +80,21 @@ public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction } /** - Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. + Returns an editable proxy of the object with the specified `DynamicObjectID`. - - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `DynamicObjectID` for the object to be edited - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. */ - public override func edit( + public override func edit( _ persistentID: DynamicObjectID? ) -> O? { Internals.assert( !self.isCommitted, - "Attempted to update an entity for \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))." + "Attempted to update an entity of type \(Internals.typeName(persistentID)) from an already committed \(Internals.typeName(self))." ) + return super.edit(persistentID) } @@ -114,6 +116,26 @@ public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction return super.edit(object) } + /** + Returns an editable proxy of the object with the specified `DynamicObjectID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `DynamicObjectID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ into: Into, + _ persistentID: DynamicObjectID + ) -> 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`. @@ -133,6 +155,23 @@ public nonisolated 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 == DynamicObjectID { + + 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. 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.swift b/Sources/BaseDataTransaction.swift index ee65c719..a890c1e5 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -121,7 +121,7 @@ public /*abstract*/ class BaseDataTransaction { - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. */ - public func edit( + public func edit( _ persistentID: DynamicObjectID? ) -> O? { @@ -157,6 +157,30 @@ public /*abstract*/ class BaseDataTransaction { return self.context.fetchExisting(object) } + /** + Returns an editable proxy of the object with the specified `DynamicObjectID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `DynamicObjectID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public func edit( + _ into: Into, + _ persistentID: DynamicObjectID + ) -> 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`. @@ -180,6 +204,26 @@ public /*abstract*/ class BaseDataTransaction { ) return self.fetchExisting(objectID) } + + /** + Deletes the objects with the specified `DynamicObjectID`s. + + - parameter persistentIDs: the `DynamicObjectID`s of the objects to delete + */ + public func delete( + persistentIDs: S + ) where S.Iterator.Element == DynamicObjectID { + + 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. diff --git a/Sources/CoreStoreError.swift b/Sources/CoreStoreError.swift index 136b9200..d657fc7e 100644 --- a/Sources/CoreStoreError.swift +++ b/Sources/CoreStoreError.swift @@ -32,7 +32,7 @@ import Foundation /** All errors thrown from CoreStore are expressed in `CoreStoreError` enum values. */ -public enum CoreStoreError: Error, CustomNSError, Hashable, Sendable { +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, Sendable { /** 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, Sendable { /** 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/CustomSchemaMappingProvider.swift b/Sources/CustomSchemaMappingProvider.swift index 3fdd5994..4937ccd4 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -108,7 +108,7 @@ public final class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider 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 final 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 final 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 final 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) } diff --git a/Sources/DataStack+Concurrency.swift b/Sources/DataStack+Concurrency.swift index f6c50fc9..a0add5c0 100644 --- a/Sources/DataStack+Concurrency.swift +++ b/Sources/DataStack+Concurrency.swift @@ -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,7 +118,7 @@ extension DataStack.AsyncNamespace { */ public func addStorage( _ storage: T - ) -> AsyncThrowingStream, any Swift.Error> { + ) -> AsyncThrowingStream, any Swift::Error> { return .init( bufferingPolicy: .unbounded, @@ -184,7 +184,7 @@ 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 @@ -226,7 +226,7 @@ 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 @@ -274,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 @@ -324,8 +324,8 @@ extension DataStack.AsyncNamespace { 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 @@ -374,8 +374,8 @@ extension DataStack.AsyncNamespace { - 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 { + _ 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+Reactive.swift b/Sources/DataStack+Reactive.swift index f4151248..780fe88b 100644 --- a/Sources/DataStack+Reactive.swift +++ b/Sources/DataStack+Reactive.swift @@ -326,7 +326,7 @@ extension DataStack.ReactiveNamespace { sourceArray: S, 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 @@ -379,7 +379,7 @@ extension DataStack.ReactiveNamespace { public func perform( _ asynchronous: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> Output + ) throws(any Swift::Error) -> Output ) -> Future { return .init { (promise) in diff --git a/Sources/DataStack+Transaction.swift b/Sources/DataStack+Transaction.swift index 68946fd9..14fffcb7 100644 --- a/Sources/DataStack+Transaction.swift +++ b/Sources/DataStack+Transaction.swift @@ -41,7 +41,7 @@ extension DataStack { public func perform( asynchronous task: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> T, + ) throws(any Swift::Error) -> T, sourceIdentifier: (any Sendable)? = nil, completion: @escaping @MainActor @Sendable (AsynchronousDataTransaction.Result) -> Void ) { @@ -65,7 +65,7 @@ extension DataStack { public func perform( asynchronous task: @escaping @Sendable ( _ transaction: AsynchronousDataTransaction - ) throws(any Swift.Error) -> T, + ) throws(any Swift::Error) -> T, sourceIdentifier: (any Sendable)? = nil, success: @escaping @MainActor @Sendable (sending T) -> Void, failure: @escaping @MainActor @Sendable (CoreStoreError) -> Void @@ -123,7 +123,7 @@ extension DataStack { public func perform( synchronous task: ( _ transaction: SynchronousDataTransaction - ) throws(any Swift.Error) -> T, + ) throws(any Swift::Error) -> T, waitForAllObservers: Bool = true, sourceIdentifier: (any Sendable)? = nil ) throws(CoreStoreError) -> T { diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index 5c5c2379..bfc6b74e 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -535,7 +535,7 @@ public final class DataStack: Equatable, Sendable { _ 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, diff --git a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift index e7a488cc..6cf9d250 100644 --- a/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift +++ b/Sources/DiffableDataSource.CollectionViewAdapter-AppKit.swift @@ -128,7 +128,7 @@ extension DiffableDataSource { 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") } diff --git a/Sources/DispatchQueue+CoreStore.swift b/Sources/DispatchQueue+CoreStore.swift index 6e20400a..90187d3c 100644 --- a/Sources/DispatchQueue+CoreStore.swift +++ b/Sources/DispatchQueue+CoreStore.swift @@ -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) } diff --git a/Sources/DynamicObject.swift b/Sources/DynamicObject.swift index 86417470..2448ce05 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -142,7 +142,9 @@ extension NSManagedObject: DynamicObject { } @_spi(Internals) - public class func cs_fromRaw(object: NSManagedObject) -> Self { + public class func cs_fromRaw( + object: NSManagedObject + ) -> Self { #if swift(>=5.9) return unsafeDowncast(object, to: self) @@ -309,7 +311,9 @@ extension CoreStoreObject { } @_spi(Internals) - public class func cs_fromRaw(object: NSManagedObject) -> Self { + public class func cs_fromRaw( + object: NSManagedObject + ) -> Self { if let coreStoreObject = object.coreStoreObject { diff --git a/Sources/ImportableObject.swift b/Sources/ImportableObject.swift index 2f7f3b97..c8756a57 100644 --- a/Sources/ImportableObject.swift +++ b/Sources/ImportableObject.swift @@ -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/Internals.CoreStoreFetchedResultsController.swift b/Sources/Internals.CoreStoreFetchedResultsController.swift index 7abbbce1..7e16ebbf 100644 --- a/Sources/Internals.CoreStoreFetchedResultsController.swift +++ b/Sources/Internals.CoreStoreFetchedResultsController.swift @@ -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.Mutext.swift b/Sources/Internals.Mutext.swift index d322129b..c4b59fa1 100644 --- a/Sources/Internals.Mutext.swift +++ b/Sources/Internals.Mutext.swift @@ -46,7 +46,7 @@ extension Internals { borrowing func withLock( _ body: (inout sending Value) throws(E) -> sending Result ) throws(E) -> sending Result - where E: Error, Result: ~Copyable { + where E: Swift::Error, Result: ~Copyable { let storage = self.storage storage.lock() @@ -60,7 +60,7 @@ extension Internals { borrowing func withLockUnchecked( _ body: (inout sending Value) throws(E) -> Result ) throws(E) -> sending Result - where E: Error { + where E: Swift::Error { let storage = self.storage storage.lock() diff --git a/Sources/Internals.swift b/Sources/Internals.swift index eeab9851..882279b6 100644 --- a/Sources/Internals.swift +++ b/Sources/Internals.swift @@ -133,8 +133,8 @@ internal enum Internals { @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) } @@ -142,8 +142,8 @@ internal enum Internals { @inline(__always) internal static func withCheckedThrowingContinuation( function: String = #function, - _ body: (CheckedContinuation) -> Void - ) async throws(any Swift.Error) -> sending T { + _ body: (CheckedContinuation) -> Void + ) async throws(any Swift::Error) -> sending T { return try await _Concurrency.withCheckedThrowingContinuation( function: function, diff --git a/Sources/ListPublisher.swift b/Sources/ListPublisher.swift index 1520847d..9afcd366 100644 --- a/Sources/ListPublisher.swift +++ b/Sources/ListPublisher.swift @@ -189,7 +189,7 @@ public final class ListPublisher: Hashable { public func refetch( _ clauseChain: B, sourceIdentifier: (any Sendable)? = nil - ) throws(any Swift.Error) where B.ObjectType == O { + ) throws(any Swift::Error) where B.ObjectType == O { try self.refetch( from: clauseChain.from, @@ -218,7 +218,7 @@ public final class ListPublisher: Hashable { public func refetch( _ clauseChain: B, sourceIdentifier: (any Sendable)? = nil - ) throws(any Swift.Error) where B.ObjectType == O { + ) throws(any Swift::Error) where B.ObjectType == O { try self.refetch( from: clauseChain.from, @@ -347,7 +347,7 @@ public final class ListPublisher: Hashable { sectionBy: SectionBy?, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest) -> Void, sourceIdentifier: (any Sendable)? - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( context: self.fetchedResultsController.managedObjectContext, diff --git a/Sources/ListState.swift b/Sources/ListState.swift index 9cc3ba26..050064a7 100644 --- a/Sources/ListState.swift +++ b/Sources/ListState.swift @@ -35,7 +35,7 @@ import SwiftUI A property wrapper type that can read `ListPublisher` changes. */ @propertyWrapper -public struct ListState: DynamicProperty { +public struct ListState: @MainActor DynamicProperty { // MARK: Public @@ -70,6 +70,7 @@ public struct ListState: DynamicProperty { _ listPublisher: ListPublisher ) { + self.sourceListPublisher = listPublisher self._observer = .init(wrappedValue: .init(listPublisher: listPublisher)) } @@ -338,9 +339,11 @@ public struct ListState: DynamicProperty { // MARK: DynamicProperty + @MainActor public mutating func update() { self._observer.update() + self.observer.rebind(to: self.sourceListPublisher) } @@ -349,13 +352,15 @@ public struct ListState: DynamicProperty { @State private var observer: Observer + private let sourceListPublisher: ListPublisher + // MARK: - Observer @MainActor private final class Observer: Observation.Observable { - let listPublisher: ListPublisher + private(set) var listPublisher: ListPublisher nonisolated var items: ListSnapshot { @@ -377,15 +382,7 @@ public struct ListState: DynamicProperty { self.listPublisher = listPublisher self.current = .init(listPublisher.snapshot) - - listPublisher.addObserver(self) { [weak self] (listPublisher) in - - guard let self = self else { - - return - } - self.items = listPublisher.snapshot - } + self.attachObserver() } isolated deinit { @@ -393,11 +390,35 @@ public struct ListState: DynamicProperty { 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() { + + self.listPublisher.addObserver(self) { [weak self] listPublisher in + + guard let self = self else { + + return + } + self.items = listPublisher.snapshot + } + } } } 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/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift index eac7a16f..531dacf3 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -76,6 +76,14 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { } } + @nonobjc + public func fetchExisting( + _ persistentID: DynamicObjectID + ) -> O? { + + return self.fetchExisting(persistentID.managedObjectID) + } + @nonobjc public func fetchExisting( _ objectID: NSManagedObjectID @@ -102,10 +110,10 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { @nonobjc public func fetchExisting( - _ objectIDs: S + _ persistentIDs: S ) -> [O] where S.Iterator.Element == DynamicObjectID { - return objectIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) + return persistentIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) } @nonobjc diff --git a/Sources/NSPersistentStoreCoordinator+Setup.swift b/Sources/NSPersistentStoreCoordinator+Setup.swift index 8b6c8e08..7ffa26c2 100644 --- a/Sources/NSPersistentStoreCoordinator+Setup.swift +++ b/Sources/NSPersistentStoreCoordinator+Setup.swift @@ -49,7 +49,7 @@ extension NSPersistentStoreCoordinator { @nonobjc internal func performSynchronously( - _ closure: @Sendable () throws(any Swift.Error) -> T + _ closure: @Sendable () throws(any Swift::Error) -> T ) throws(CoreStoreError) -> T { do { diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index cc994e1e..ae183a0d 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -72,32 +72,36 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ @MainActor - public func addObserver(_ observer: U) where U.ObjectEntityType == O { + 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 ) @@ -113,7 +117,8 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to unregister notifications to */ @MainActor - public func removeObserver(_ observer: U) where U.ObjectEntityType == O { + public func removeObserver(_ observer: U) + where U.ObjectEntityType == O { self.unregisterObserver(observer) } @@ -412,11 +417,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 cfe5aab9..2162d81f 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -53,8 +53,8 @@ public protocol ObjectObserver: AnyObject, Sendable { */ func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType, - sourceIdentifier: Any? + willUpdateObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) /** @@ -66,7 +66,7 @@ public protocol ObjectObserver: AnyObject, Sendable { */ func objectMonitor( _ monitor: ObjectMonitor, - willUpdateObject object: ObjectEntityType + willUpdateObject object: sending ObjectEntityType ) /** @@ -80,9 +80,9 @@ public protocol ObjectObserver: AnyObject, Sendable { */ 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, Sendable { */ func objectMonitor( _ monitor: ObjectMonitor, - didUpdateObject object: ObjectEntityType, + didUpdateObject object: sending ObjectEntityType, changedPersistentKeys: Set ) @@ -109,8 +109,8 @@ public protocol ObjectObserver: AnyObject, Sendable { */ func objectMonitor( _ monitor: ObjectMonitor, - didDeleteObject object: ObjectEntityType, - sourceIdentifier: Any? + didDeleteObject object: sending ObjectEntityType, + sourceIdentifier: (any Sendable)? ) /** @@ -122,7 +122,7 @@ public protocol ObjectObserver: AnyObject, Sendable { */ 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/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/ObjectState.swift b/Sources/ObjectState.swift index bf6156db..8f072a6c 100644 --- a/Sources/ObjectState.swift +++ b/Sources/ObjectState.swift @@ -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 @@ -65,6 +65,7 @@ public struct ObjectState: DynamicProperty { @MainActor public init(_ objectPublisher: ObjectPublisher?) { + self.sourceObjectPublisher = objectPublisher self._observer = .init(wrappedValue: .init(objectPublisher: objectPublisher)) } @@ -86,9 +87,11 @@ public struct ObjectState: DynamicProperty { // MARK: DynamicProperty + @MainActor public mutating func update() { self._observer.update() + self.observer.rebind(to: self.sourceObjectPublisher) } @@ -97,13 +100,15 @@ public struct ObjectState: DynamicProperty { @State private var observer: Observer + private let sourceObjectPublisher: ObjectPublisher? + // MARK: - Observer @MainActor private final class Observer: Observation.Observable { - let objectPublisher: ObjectPublisher? + private(set) var objectPublisher: ObjectPublisher? nonisolated var item: ObjectSnapshot? { @@ -122,21 +127,28 @@ public struct ObjectState: DynamicProperty { } init(objectPublisher: ObjectPublisher?) { - - guard - let dataStack = objectPublisher?.cs_dataStack(), - let objectPublisher = objectPublisher?.asPublisher(in: dataStack) - else { - - self.objectPublisher = nil - self.current = .init(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.current = .init(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 { @@ -146,16 +158,25 @@ public struct ObjectState: DynamicProperty { } } - isolated deinit { - - self.objectPublisher?.removeObserver(self) - } - // MARK: Private private let registrar = ObservationRegistrar() private let current: Internals.Mutex?> + + private static func canonicalPublisher( + for objectPublisher: ObjectPublisher? + ) -> ObjectPublisher? { + + guard + let objectPublisher = objectPublisher, + let dataStack = objectPublisher.cs_dataStack() + else { + + return nil + } + return objectPublisher.asPublisher(in: dataStack) + } } } diff --git a/Sources/SQLiteStore.swift b/Sources/SQLiteStore.swift index ccb339f3..61f69ed8 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -236,7 +236,7 @@ public final class SQLiteStore: LocalStorage { @_spi(Internals) public func cs_finalizeStorageAndWait( soureModelHint: NSManagedObjectModel - ) throws(any Swift.Error) { + ) throws(any Swift::Error) { _ = try withExtendedLifetime(NSPersistentStoreCoordinator(managedObjectModel: soureModelHint)) { (coordinator: NSPersistentStoreCoordinator) in @@ -259,12 +259,12 @@ public final class SQLiteStore: LocalStorage { 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] = [ diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index de929a9b..1ac74f6d 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -151,7 +151,7 @@ public protocol LocalStorage: StorageInterface { @_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) @@ -160,7 +160,7 @@ public protocol LocalStorage: StorageInterface { 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 a219a51d..f20e3950 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -103,6 +103,26 @@ public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { return super.edit(object) } + /** + Returns an editable proxy of the object with the specified `DynamicObjectID`. + + - parameter into: an `Into` clause specifying the entity type + - parameter persistentID: the `DynamicObjectID` for the object to be edited + - returns: an editable proxy for the specified `NSManagedObject` or `CoreStoreObject`. + */ + public override func edit( + _ into: Into, + _ persistentID: DynamicObjectID + ) -> 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`. @@ -122,6 +142,23 @@ public nonisolated 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 == DynamicObjectID { + + 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. diff --git a/Sources/UnsafeDataTransaction.swift b/Sources/UnsafeDataTransaction.swift index 1452a15b..b6f23561 100644 --- a/Sources/UnsafeDataTransaction.swift +++ b/Sources/UnsafeDataTransaction.swift @@ -114,7 +114,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction, @unchecked Sendab - 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() From 16c5bb3e15cb94684d9c3f574cff381033939159 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Mon, 27 Jul 2026 15:23:39 +0900 Subject: [PATCH 5/7] Rename DynamicObjectID -> PersistentID --- CoreStore.xcodeproj/project.pbxproj | 48 ++++++------- ... OSX.xcscheme => CoreStore macOS.xcscheme} | 10 +-- .../Advanced.EvolutionDemo.Migrator.swift | 6 ++ .../Modern.PokedexDemo.Service.swift | 10 +-- Demo/Sources/Helpers/Menu/Menu.MainView.swift | 27 ++++++- Sources/AsynchronousDataTransaction.swift | 16 ++--- Sources/BaseDataTransaction+Querying.swift | 10 +-- Sources/BaseDataTransaction.swift | 19 +++-- Sources/CoreStore+Logging.swift | 2 +- Sources/CoreStoreLogger.swift | 2 +- Sources/DataStack+Querying.swift | 10 +-- Sources/DefaultLogger.swift | 72 +++++++++---------- Sources/Differentiable.swift | 2 +- Sources/DynamicObject.swift | 2 +- Sources/Field.Relationship.swift | 2 +- ...Internals.DiffableDataSourceSnapshot.swift | 2 +- Sources/MigrationResult.swift | 2 +- Sources/NSManagedObjectContext+Querying.swift | 4 +- ...namicObjectID.swift => PersistentID.swift} | 2 +- Sources/Relationship.ToManyOrdered.swift | 2 +- Sources/Relationship.ToManyUnordered.swift | 2 +- Sources/SetupResult.swift | 2 +- Sources/SynchronousDataTransaction.swift | 12 ++-- 23 files changed, 146 insertions(+), 120 deletions(-) rename CoreStore.xcodeproj/xcshareddata/xcschemes/{CoreStore OSX.xcscheme => CoreStore macOS.xcscheme} (93%) rename Sources/{DynamicObjectID.swift => PersistentID.swift} (97%) diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index ca9e981c..c8122564 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -19,10 +19,10 @@ 53EB03E72A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; 53EB03E82A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; 53EB03E92A3AD5D400179430 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EB03E52A3AD5D400179430 /* Operators.swift */; }; - 5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; - 5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; - 5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; - 5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */; }; + 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 */; }; @@ -872,7 +872,7 @@ containerPortal = 2F03A52719C5C6DA005002A5 /* Project object */; proxyType = 1; remoteGlobalIDString = B52DD1731BE1F8CC00949AFE; - remoteInfo = "CoreStore OSX"; + remoteInfo = "CoreStore macOS"; }; B5D372871A39CF4D00F583D9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -891,7 +891,7 @@ 2F03A53E19C5C6DA005002A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2F03A54C19C5C872005002A5 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 53EB03E52A3AD5D400179430 /* Operators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; - 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicObjectID.swift; sourceTree = ""; }; + 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; }; @@ -1463,7 +1463,7 @@ B5831B6E1F3355C300A9F647 /* Legacy Properties */, B52F74391E9B8724005F3DAC /* Dynamic Schema */, B5D339DC1E9489C700C880DE /* DynamicObject.swift */, - 5F13955D2FFCDBAC001EE0A4 /* DynamicObjectID.swift */, + 5F13955D2FFCDBAC001EE0A4 /* PersistentID.swift */, B52F742E1E9B50D0005F3DAC /* SchemaHistory.swift */, B5D339E61E9493A500C880DE /* Entity.swift */, B5A991EB1E9DC2CE0091A2E3 /* VersionLock.swift */, @@ -1903,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 */, @@ -1916,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 */, @@ -1934,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"; }; @@ -2016,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 */ @@ -2143,7 +2143,7 @@ B50E175223517C6B004F033C /* Internals.DiffableDataUIDispatcher.Changeset.swift in Sources */, B5E84EE71AFF84610064E85B /* CoreStore+Logging.swift in Sources */, B546F9731C9C553300D5AC55 /* SetupResult.swift in Sources */, - 5F13955F2FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, + 5F13955F2FFCDBEF001EE0A4 /* PersistentID.swift in Sources */, B50C3EFE23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EDF23CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5C7959925D7D8B300BDACC1 /* ListReader.swift in Sources */, @@ -2362,7 +2362,7 @@ B5831F432212700400D8604C /* Where.Expression.swift in Sources */, B51260941E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, B5FE4DA81C84FB4400FA6A91 /* InMemoryStore.swift in Sources */, - 5F1395602FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, + 5F1395602FFCDBEF001EE0A4 /* PersistentID.swift in Sources */, B50C3EFF23D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE023CEBCF000E1708C /* FieldOptionalType.swift in Sources */, B5C7959A25D7D8B300BDACC1 /* ListReader.swift in Sources */, @@ -2616,7 +2616,7 @@ B52DD1AD1BE1F93900949AFE /* Where.swift in Sources */, B5F8496F234898240029D57B /* ListSnapshot.swift in Sources */, B5831F452212700500D8604C /* Where.Expression.swift in Sources */, - 5F13955E2FFCDBEF001EE0A4 /* DynamicObjectID.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 */, @@ -2835,7 +2835,7 @@ B5831F442212700500D8604C /* Where.Expression.swift in Sources */, B50C3F0023D1AB1400B29880 /* FieldCoders.Plist.swift in Sources */, B56E4EE123CEBCF000E1708C /* FieldOptionalType.swift in Sources */, - 5F1395612FFCDBEF001EE0A4 /* DynamicObjectID.swift in Sources */, + 5F1395612FFCDBEF001EE0A4 /* PersistentID.swift in Sources */, B5C7959B25D7D8B300BDACC1 /* ListReader.swift in Sources */, B5F8496E234898240029D57B /* ListSnapshot.swift in Sources */, B51260951E9B28F100402229 /* Internals.EntityIdentifier.swift in Sources */, @@ -2973,7 +2973,7 @@ }; B52DD1801BE1F8CD00949AFE /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = B52DD1731BE1F8CC00949AFE /* CoreStore OSX */; + target = B52DD1731BE1F8CC00949AFE /* CoreStore macOS */; targetProxy = B52DD17F1BE1F8CD00949AFE /* PBXContainerItemProxy */; }; B5D372881A39CF4D00F583D9 /* PBXTargetDependency */ = { @@ -3602,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 */, @@ -3611,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/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift index 6c906c1f..f9787a15 100644 --- a/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift +++ b/Demo/Sources/Demos/Advanced/EvolutionDemo/Advanced.EvolutionDemo.Migrator.swift @@ -37,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] ] ) ], diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index 0729f13b..e64ac301 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -51,7 +51,7 @@ extension Modern.PokedexDemo { ⭐️ Sample 2: Importing a single JSON data into an `ImportableUniqueObject` whose `ImportSource` is a JSON `Dictionary` */ private static func importSpecies( - for detailsPersistentID: DynamicObjectID, + for detailsPersistentID: PersistentID, from data: Data ) async throws -> ObjectSnapshot { @@ -88,7 +88,7 @@ extension Modern.PokedexDemo { ⭐️ Sample 3: Importing a list of JSON data into `ImportableUniqueObject`s whose `ImportSource` are JSON `Dictionary`s */ private static func importForms( - for detailsPersistentID: DynamicObjectID, + for detailsPersistentID: PersistentID, from dataArray: [Data] ) async throws { @@ -289,7 +289,7 @@ extension Modern.PokedexDemo { private func fetchSpecies( key: String, - detailsPersistentID: DynamicObjectID, + detailsPersistentID: PersistentID, speciesURL: URL ) async { @@ -331,7 +331,7 @@ extension Modern.PokedexDemo { private func fetchFormsIfNeeded( key: String, - detailsPersistentID: DynamicObjectID, + detailsPersistentID: PersistentID, species: ObjectSnapshot ) { @@ -363,7 +363,7 @@ extension Modern.PokedexDemo { } private func fetchForms( - detailsPersistentID: DynamicObjectID, + detailsPersistentID: PersistentID, formsURLs: [URL] ) async { diff --git a/Demo/Sources/Helpers/Menu/Menu.MainView.swift b/Demo/Sources/Helpers/Menu/Menu.MainView.swift index b7406e5b..946f4647 100644 --- a/Demo/Sources/Helpers/Menu/Menu.MainView.swift +++ b/Demo/Sources/Helpers/Menu/Menu.MainView.swift @@ -24,15 +24,18 @@ extension Menu { if self.horizontalSizeClass == .compact { NavigationStack { + self.menuList } } else { NavigationSplitView( sidebar: { + self.menuList }, detail: { + Menu.PlaceholderView() } ) @@ -48,14 +51,16 @@ extension Menu { ForEach(Menu.Section.allCases, id: \.self) { section in - SwiftUI.Section( + SwiftUI::Section( content: { ForEach(section.routes) { route in NavigationLink( destination: { - route.destination + LazyDestination { + route.destination + } }, label: { Menu.ItemView( @@ -78,5 +83,23 @@ extension Menu { .navigationTitle("CoreStore Demos") .listStyle(.sidebar) } + + + // MARK: - LazyDestination + + private struct LazyDestination: View { + + init(@ViewBuilder content: @escaping () -> Content) { + + self.content = content + } + + var body: some View { + + self.content() + } + + private let content: () -> Content + } } } diff --git a/Sources/AsynchronousDataTransaction.swift b/Sources/AsynchronousDataTransaction.swift index eafe4165..62d48d3a 100644 --- a/Sources/AsynchronousDataTransaction.swift +++ b/Sources/AsynchronousDataTransaction.swift @@ -55,7 +55,7 @@ public nonisolated 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: - @@ -80,14 +80,14 @@ public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction } /** - Returns an editable proxy of the object with the specified `DynamicObjectID`. + Returns an editable proxy of the object with the specified `PersistentID`. - parameter into: an `Into` clause specifying the entity type - - parameter persistentID: the `DynamicObjectID` for the object to be edited + - 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: DynamicObjectID? + _ persistentID: PersistentID? ) -> O? { Internals.assert( @@ -117,15 +117,15 @@ public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction } /** - Returns an editable proxy of the object with the specified `DynamicObjectID`. + Returns an editable proxy of the object with the specified `PersistentID`. - parameter into: an `Into` clause specifying the entity type - - parameter persistentID: the `DynamicObjectID` for the object to be edited + - 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: DynamicObjectID + _ persistentID: PersistentID ) -> O? { Internals.assert( @@ -163,7 +163,7 @@ public nonisolated final class AsynchronousDataTransaction: BaseDataTransaction */ public override func delete( persistentIDs: S - ) where S.Iterator.Element == DynamicObjectID { + ) where S.Iterator.Element == PersistentID { Internals.assert( !self.isCommitted, diff --git a/Sources/BaseDataTransaction+Querying.swift b/Sources/BaseDataTransaction+Querying.swift index 78a217a9..5c1f2825 100644 --- a/Sources/BaseDataTransaction+Querying.swift +++ b/Sources/BaseDataTransaction+Querying.swift @@ -113,11 +113,11 @@ extension BaseDataTransaction: FetchableSource, QueryableSource { /** Fetches the `DynamicObject` instance in the transaction's context from an `NSManagedObjectID`. - - parameter persistentID: the `DynamicObjectID` for the object + - 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: DynamicObjectID + _ persistentID: PersistentID ) -> O? { return self.context.fetchExisting(persistentID.managedObjectID) @@ -150,14 +150,14 @@ extension BaseDataTransaction: FetchableSource, QueryableSource { } /** - Fetches the `DynamicObject` instances in the transaction's context from a list of `DynamicObjectID`. + Fetches the `DynamicObject` instances in the transaction's context from a list of `PersistentID`. - - parameter objectIDs: the `DynamicObjectID` array for the objects + - 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 == DynamicObjectID { + ) -> [O] where S.Iterator.Element == PersistentID { return self.context.fetchExisting(objectIDs) } diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index a890c1e5..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`. */ @@ -118,11 +115,11 @@ public /*abstract*/ class BaseDataTransaction { /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. - - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - 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: DynamicObjectID? + _ persistentID: PersistentID? ) -> O? { Internals.assert( @@ -158,15 +155,15 @@ public /*abstract*/ class BaseDataTransaction { } /** - Returns an editable proxy of the object with the specified `DynamicObjectID`. + Returns an editable proxy of the object with the specified `PersistentID`. - parameter into: an `Into` clause specifying the entity type - - parameter persistentID: the `DynamicObjectID` for the object to be edited + - 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: DynamicObjectID + _ persistentID: PersistentID ) -> O? { Internals.assert( @@ -206,13 +203,13 @@ public /*abstract*/ class BaseDataTransaction { } /** - Deletes the objects with the specified `DynamicObjectID`s. + Deletes the objects with the specified `PersistentID`s. - - parameter persistentIDs: the `DynamicObjectID`s of the objects to delete + - parameter persistentIDs: the `PersistentID`s of the objects to delete */ public func delete( persistentIDs: S - ) where S.Iterator.Element == DynamicObjectID { + ) where S.Iterator.Element == PersistentID { Internals.assert( self.isRunningInAllowedQueue(), 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/CoreStoreLogger.swift b/Sources/CoreStoreLogger.swift index 7e6c1173..0bf0a5c5 100644 --- a/Sources/CoreStoreLogger.swift +++ b/Sources/CoreStoreLogger.swift @@ -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/DataStack+Querying.swift b/Sources/DataStack+Querying.swift index f1cbb599..572949cc 100644 --- a/Sources/DataStack+Querying.swift +++ b/Sources/DataStack+Querying.swift @@ -49,11 +49,11 @@ extension DataStack: FetchableSource, QueryableSource { /** Fetches the `DynamicObject` instance in the `DataStack`'s context from an `NSManagedObjectID`. - - parameter persistentID: the `DynamicObjectID` for the object + - 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: DynamicObjectID + _ persistentID: PersistentID ) -> O? { return self.mainContext.fetchExisting(persistentID.managedObjectID) @@ -86,14 +86,14 @@ extension DataStack: FetchableSource, QueryableSource { } /** - Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `DynamicObjectID`. + Fetches the `DynamicObject` instances in the `DataStack`'s context from a list of `PersistentID`. - - parameter objectIDs: the `DynamicObjectID` array for the objects + - 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 == DynamicObjectID { + ) -> [O] where S.Iterator.Element == PersistentID { return self.mainContext.fetchExisting(objectIDs) } 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/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/DynamicObject.swift b/Sources/DynamicObject.swift index 2448ce05..7293326a 100644 --- a/Sources/DynamicObject.swift +++ b/Sources/DynamicObject.swift @@ -38,7 +38,7 @@ public nonisolated protocol DynamicObject: AnyObject, SendableMetatype { /** The object ID for this instance */ - typealias ObjectID = DynamicObjectID + typealias ObjectID = PersistentID /** 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/Internals.DiffableDataSourceSnapshot.swift b/Sources/Internals.DiffableDataSourceSnapshot.swift index 9a5d4c97..5d3a0fb8 100644 --- a/Sources/Internals.DiffableDataSourceSnapshot.swift +++ b/Sources/Internals.DiffableDataSourceSnapshot.swift @@ -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/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/NSManagedObjectContext+Querying.swift b/Sources/NSManagedObjectContext+Querying.swift index 531dacf3..6cac0774 100644 --- a/Sources/NSManagedObjectContext+Querying.swift +++ b/Sources/NSManagedObjectContext+Querying.swift @@ -78,7 +78,7 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { @nonobjc public func fetchExisting( - _ persistentID: DynamicObjectID + _ persistentID: PersistentID ) -> O? { return self.fetchExisting(persistentID.managedObjectID) @@ -111,7 +111,7 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource { @nonobjc public func fetchExisting( _ persistentIDs: S - ) -> [O] where S.Iterator.Element == DynamicObjectID { + ) -> [O] where S.Iterator.Element == PersistentID { return persistentIDs.compactMap({ self.fetchExisting($0.managedObjectID) }) } diff --git a/Sources/DynamicObjectID.swift b/Sources/PersistentID.swift similarity index 97% rename from Sources/DynamicObjectID.swift rename to Sources/PersistentID.swift index 0959c654..2a745b6c 100644 --- a/Sources/DynamicObjectID.swift +++ b/Sources/PersistentID.swift @@ -28,7 +28,7 @@ import CoreData // MARK: - DynamicObjectID -public struct DynamicObjectID: Hashable, ObjectRepresentation, Sendable { +public struct PersistentID: Hashable, ObjectRepresentation, Sendable { /** The associated `NSManagedObject` or `CoreStoreObject` entity class diff --git a/Sources/Relationship.ToManyOrdered.swift b/Sources/Relationship.ToManyOrdered.swift index 10bfeee9..da4e772d 100644 --- a/Sources/Relationship.ToManyOrdered.swift +++ b/Sources/Relationship.ToManyOrdered.swift @@ -203,7 +203,7 @@ extension RelationshipContainer { 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 fe3818fc..21dd0c82 100644 --- a/Sources/Relationship.ToManyUnordered.swift +++ b/Sources/Relationship.ToManyUnordered.swift @@ -203,7 +203,7 @@ extension RelationshipContainer { 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/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/SynchronousDataTransaction.swift b/Sources/SynchronousDataTransaction.swift index f20e3950..965bfe35 100644 --- a/Sources/SynchronousDataTransaction.swift +++ b/Sources/SynchronousDataTransaction.swift @@ -71,11 +71,11 @@ public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { /** Returns an editable proxy of a specified `NSManagedObject` or `CoreStoreObject`. - - parameter persistentID: the `DynamicObjectID` pertaining ot the `NSManagedObject` or `CoreStoreObject` type to be edited + - 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: DynamicObjectID? + _ persistentID: PersistentID? ) -> O? { Internals.assert( @@ -104,15 +104,15 @@ public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { } /** - Returns an editable proxy of the object with the specified `DynamicObjectID`. + Returns an editable proxy of the object with the specified `PersistentID`. - parameter into: an `Into` clause specifying the entity type - - parameter persistentID: the `DynamicObjectID` for the object to be edited + - 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: DynamicObjectID + _ persistentID: PersistentID ) -> O? { Internals.assert( @@ -150,7 +150,7 @@ public nonisolated final class SynchronousDataTransaction: BaseDataTransaction { */ public override func delete( persistentIDs: S - ) where S.Iterator.Element == DynamicObjectID { + ) where S.Iterator.Element == PersistentID { Internals.assert( !self.isCommitted, From 802065e85ad9074bd330ecb48db529fbba512c10 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Tue, 28 Jul 2026 14:25:01 +0900 Subject: [PATCH 6/7] cleanup --- .../Modern.PokedexDemo.Service.swift | 8 +-- .../Helpers/WithMainActorImmediate.swift | 1 + Sources/ListMonitor.swift | 63 ++++++++++--------- Sources/ListObserver.swift | 2 +- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index e64ac301..a696f4d8 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -27,10 +27,10 @@ extension Modern.PokedexDemo { try await Modern.PokedexDemo.dataStack.async.perform { transaction -> Void in - let json: Dictionary = try self.parseJSON( + let json: Dictionary = try self.parseJSON( try JSONSerialization.jsonObject(with: data, options: []) ) - let results: [Dictionary] = try self.parseJSON( + let results: [Dictionary] = try self.parseJSON( json["results"] ) _ = try transaction.importUniqueObjects( @@ -57,7 +57,7 @@ extension Modern.PokedexDemo { let speciesPersistentID = try await Modern.PokedexDemo.dataStack.async.perform { transaction in - let json: Dictionary = try self.parseJSON( + let json: Dictionary = try self.parseJSON( try JSONSerialization.jsonObject(with: data, options: []) ) guard @@ -102,7 +102,7 @@ extension Modern.PokedexDemo { try self.parseJSON( try JSONSerialization.jsonObject(with: data, options: []) - ) as [String: Any] + ) as [String: any Sendable] } ) guard !forms.isEmpty else { diff --git a/Demo/Sources/Helpers/WithMainActorImmediate.swift b/Demo/Sources/Helpers/WithMainActorImmediate.swift index 2f3aad35..22f6c43f 100644 --- a/Demo/Sources/Helpers/WithMainActorImmediate.swift +++ b/Demo/Sources/Helpers/WithMainActorImmediate.swift @@ -15,6 +15,7 @@ func withMainActorImmediate( ) { if #available(iOS 26.0, *) { + Task.immediate(operation: task) } else if Thread.isMainThread { diff --git a/Sources/ListMonitor.swift b/Sources/ListMonitor.swift index 828b5f69..8b4c36fd 100644 --- a/Sources/ListMonitor.swift +++ b/Sources/ListMonitor.swift @@ -842,7 +842,8 @@ public final class ListMonitor: Hashable, Sendable { _ object: O, _ indexPath: IndexPath?, _ newIndexPath: IndexPath? - ) -> Void) { + ) -> Void + ) { Internals.setAssociatedRetainedObject( Internals.NotificationObserver( @@ -902,7 +903,7 @@ public final class ListMonitor: Hashable, Sendable { } @MainActor - internal func registerObserver( + internal func registerObserver( _ observer: U, willChange: @escaping @Sendable ( _ observer: U, @@ -919,19 +920,21 @@ public final class ListMonitor: Hashable, Sendable { 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 } @@ -942,9 +945,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -955,9 +958,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -968,9 +971,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -980,7 +983,7 @@ public final class ListMonitor: Hashable, Sendable { } @MainActor - internal func registerObserver( + internal func registerObserver( _ observer: U, didInsertObject: @escaping @Sendable ( _ observer: U, @@ -1006,20 +1009,21 @@ public final class ListMonitor: Hashable, Sendable { _ 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 } @@ -1030,9 +1034,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -1043,9 +1047,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -1056,9 +1060,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } @@ -1068,7 +1072,7 @@ public final class ListMonitor: Hashable, Sendable { } @MainActor - internal func registerObserver( + internal func registerObserver( _ observer: U, didInsertSection: @escaping @Sendable ( _ observer: U, @@ -1081,20 +1085,21 @@ public final class ListMonitor: Hashable, Sendable { _ 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 } @@ -1105,9 +1110,9 @@ public final class ListMonitor: Hashable, Sendable { &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 } diff --git a/Sources/ListObserver.swift b/Sources/ListObserver.swift index 6ef6512c..cb9294a0 100644 --- a/Sources/ListObserver.swift +++ b/Sources/ListObserver.swift @@ -39,7 +39,7 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ListObserver: AnyObject, Sendable { +public protocol ListObserver: AnyObject, SendableMetatype { /** The `NSManagedObject` type for the observed list From 838637f29f9464f7882f248d33b8e6bddcd3b428 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 29 Jul 2026 13:56:43 +0900 Subject: [PATCH 7/7] cleanup warnings --- Sources/ObjectMonitor.swift | 23 +++++++++++++++-------- Sources/ObjectObserver.swift | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sources/ObjectMonitor.swift b/Sources/ObjectMonitor.swift index ae183a0d..a8621918 100644 --- a/Sources/ObjectMonitor.swift +++ b/Sources/ObjectMonitor.swift @@ -72,7 +72,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati - parameter observer: an `ObjectObserver` to send change notifications to */ @MainActor - public func addObserver(_ observer: U) + public func addObserver(_ observer: U) where U.ObjectEntityType == O { self.unregisterObserver(observer) @@ -261,7 +261,7 @@ public final class ObjectMonitor: Hashable, ObjectRepresentati } @MainActor - internal func registerObserver( + internal func registerObserver( _ observer: U, willChangeObject: @escaping @Sendable ( _ observer: U, @@ -285,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 } @@ -302,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 } @@ -315,9 +319,12 @@ 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 } diff --git a/Sources/ObjectObserver.swift b/Sources/ObjectObserver.swift index 2162d81f..b1dcd684 100644 --- a/Sources/ObjectObserver.swift +++ b/Sources/ObjectObserver.swift @@ -36,7 +36,7 @@ import CoreData monitor.addObserver(self) ``` */ -public protocol ObjectObserver: AnyObject, Sendable { +public protocol ObjectObserver: AnyObject, SendableMetatype { /** The `DynamicObject` type for the observed object