diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index b432683..697b110 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 f12acde..13c3968 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 dfb1a37..49590d3 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 d187b9e..0bc9e6a 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 0e6e850..369505f 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 18169b2..f5d6923 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 8ca76d3..2e09cd1 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 - events += 1 + 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 { + + 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 b6ff73a..1bf8a0b 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 e9841dc..7b56d12 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 ab8b7cc..3becb11 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 7cf2578..3ca4618 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 46e0891..ce01227 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 e09bd9b..df88ee7 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 26ddec6..f66dec4 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 a7f9a80..4f463e0 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 31e5946..7e6c117 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 d663ecb..b332859 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 46fd5b2..a0e3cd0 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 8d84b25..e848c85 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 4523712..f6c50fc 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 8f94f90..14e5005 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 62bac1f..2ec7f30 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 7ae01e4..f415124 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 e0e0988..2994b44 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 cc2ec73..62caaac 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 0567b4d..be5a3f7 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 e3ffb46..c4ced63 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 fb27d34..e7a488c 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 108cdd0..73014d1 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 690d892..a3f3936 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 8408444..961d5ca 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 ceed4de..6e20400 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 a34916c..2f7f3b9 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 3b3a5f2..8794d72 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 c365514..3659c9c 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 d0e8f55..d8ea970 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 c0d8953..7abbbce 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 b7940ae..9a5d4c9 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 213cde6..f468fbd 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 dc48716..9db3711 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 ce86cf4..b5702b2 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 f41ad16..f9777a5 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 d7401bb..f63fd11 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 42a4247..e6f3670 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 ebdc3de..2a56c0a 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 501f5e9..a281d70 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 10b675c..eba923d 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 efd0e8a..e5053be 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 30940b1..6ef6512 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 65e904f..36285d1 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 ec9d435..5657f34 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 2c82d6b..3c9c986 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 5cd92da..23811fb 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 1ef4525..1079859 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 a23572b..b2d2517 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 6ddbeff..601ccb7 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 { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - return fetchResults.first } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing fetch request." + ) + throw coreStoreError + } } @nonobjc @@ -473,89 +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 - } - } - if count == NSNotFound { + guard count != NSNotFound else { - let coreStoreError = CoreStoreError(countError) + throw CoreStoreError(nil) + } + return count + } + catch { + + let coreStoreError = CoreStoreError(error) Internals.log( coreStoreError, "Failed executing count request." ) throw coreStoreError } - return count } @nonobjc internal func fetchObjectID( _ fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> NSManagedObjectID? { - - var fetchResults: [NSManagedObjectID]? - var fetchError: (any Swift.Error)? - self.performAndWait { + + do { - do { + let fetchResults = try self.performAndWait { - fetchResults = try self.fetch(fetchRequest.staticCast()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - return fetchResults.first } - let coreStoreError = CoreStoreError(fetchError) - Internals.log( - coreStoreError, - "Failed executing fetch request." - ) - throw coreStoreError + catch { + + let coreStoreError = CoreStoreError(error) + Internals.log( + coreStoreError, + "Failed executing fetch request." + ) + throw coreStoreError + } } @@ -566,35 +538,29 @@ extension NSManagedObjectContext { _ selectTerms: [SelectTerm], fetchRequest: Internals.CoreStoreFetchRequest ) throws(CoreStoreError) -> U? { - - 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] 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()) + return try self.fetch(fetchRequest.staticCast()) } - catch { - - fetchError = error - } - } - if let fetchResults = fetchResults { - return NSDictionary.cs_fromQueryResultsNativeType(fetchResults) } - 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 + } } @@ -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 f179adf..c5362fd 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 9162070..ab24259 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 a8c7483..21b9f52 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 78a645f..8b6c8e0 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 { + return try self.performAndWait { - result = try closure() - } - catch { - - 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 2d7bf57..83a7e73 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 109ed92..3149e29 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 2a78d24..a610caa 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 32b11b3..6a6388e 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 ae79bf6..efa336f 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 9f383b4..5599251 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 2e2e8e2..3f762b8 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 3ab4529..51bc608 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 ced1489..c1dbad5 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 0c24c91..b56ae47 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 0dd4309..1452a15 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(