Initial prototype for Swift 6 mode

This commit is contained in:
John Estropia
2026-07-07 11:18:17 +09:00
parent 38ec439b18
commit 49f65601cd
71 changed files with 998 additions and 684 deletions
+12 -12
View File
@@ -3016,8 +3016,8 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = Sources/Info.plist; INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "-D DEBUG"; OTHER_SWIFT_FLAGS = "-D DEBUG";
@@ -3027,12 +3027,12 @@
SWIFT_COMPILATION_MODE = singlefile; SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.6; TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = ""; VERSION_INFO_PREFIX = "";
WATCHOS_DEPLOYMENT_TARGET = 9.6; WATCHOS_DEPLOYMENT_TARGET = 10.0;
}; };
name = Debug; name = Debug;
}; };
@@ -3083,8 +3083,8 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = Sources/Info.plist; INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6; IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStore; PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStore;
PRODUCT_NAME = CoreStore; PRODUCT_NAME = CoreStore;
@@ -3092,13 +3092,13 @@
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.6; TVOS_DEPLOYMENT_TARGET = 17.0;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = ""; VERSION_INFO_PREFIX = "";
WATCHOS_DEPLOYMENT_TARGET = 9.6; WATCHOS_DEPLOYMENT_TARGET = 10.0;
}; };
name = Release; name = Release;
}; };
@@ -3366,7 +3366,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 9.3.0; MARKETING_VERSION = 9.3.0;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-weak_framework", "-weak_framework",
@@ -3401,7 +3401,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
"@loader_path/Frameworks", "@loader_path/Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 13.5; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 9.3.0; MARKETING_VERSION = 9.3.0;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-weak_framework", "-weak_framework",
+24 -12
View File
@@ -24,6 +24,7 @@
// //
import XCTest import XCTest
import os
@testable @testable
import CoreStore import CoreStore
@@ -47,7 +48,11 @@ class BaseTestCase: XCTestCase {
// MARK: Internal // MARK: Internal
@nonobjc @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( let stack = DataStack(
xcodeModelName: "Model", xcodeModelName: "Model",
@@ -79,6 +84,7 @@ class BaseTestCase: XCTestCase {
} }
@nonobjc @nonobjc
@MainActor
func expectLogger<T>(_ expectations: [TestLogger.Expectation], closure: () throws -> T) rethrows -> T { func expectLogger<T>(_ expectations: [TestLogger.Expectation], closure: () throws -> T) rethrows -> T {
CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations(expectations)) CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations(expectations))
@@ -97,6 +103,7 @@ class BaseTestCase: XCTestCase {
} }
@nonobjc @nonobjc
@MainActor
func expectError<T>(code: CoreStoreErrorCode, closure: () throws -> T) { func expectError<T>(code: CoreStoreErrorCode, closure: () throws -> T) {
CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations([.logError])) CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations([.logError]))
@@ -135,12 +142,14 @@ class BaseTestCase: XCTestCase {
} }
@nonobjc @nonobjc
@MainActor
func checkExpectationsImmediately() { func checkExpectationsImmediately() {
self.waitForExpectations(timeout: 0, handler: { _ in }) self.waitForExpectations(timeout: 0, handler: { _ in })
} }
@nonobjc @nonobjc
@MainActor
func waitAndCheckExpectations() { func waitAndCheckExpectations() {
self.waitForExpectations(timeout: 10, handler: {_ in }) self.waitForExpectations(timeout: 10, handler: {_ in })
@@ -174,7 +183,7 @@ class BaseTestCase: XCTestCase {
// MARK: - TestLogger // MARK: - TestLogger
class TestLogger: CoreStoreLogger { final class TestLogger: CoreStoreLogger {
enum Expectation { enum Expectation {
@@ -187,13 +196,13 @@ class TestLogger: CoreStoreLogger {
init(_ expectations: [Expectation: XCTestExpectation]) { init(_ expectations: [Expectation: XCTestExpectation]) {
self.expectations = expectations self.expectations = .init(initialState: expectations)
} }
// MARK: CoreStoreLogger // MARK: CoreStoreLogger
var enableObjectConcurrencyDebugging: Bool = true let enableObjectConcurrencyDebugging: Bool = true
func log(level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) { func log(level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
@@ -227,18 +236,21 @@ class TestLogger: CoreStoreLogger {
// MARK: Private // MARK: Private
private var expectations: [Expectation: XCTestExpectation] private let expectations: OSAllocatedUnfairLock<[Expectation: XCTestExpectation]>
private func fulfill(_ expectation: Expectation) { private func fulfill(_ expectation: Expectation) {
if let instance = self.expectations[expectation] { self.expectations.withLock {
instance.fulfill() if let instance = $0[expectation] {
self.expectations[expectation] = nil
} instance.fulfill()
else { $0[expectation] = nil
}
XCTFail("Unexpected Logger Action: \(expectation)") else {
XCTFail("Unexpected Logger Action: \(expectation)")
}
} }
} }
} }
+2
View File
@@ -35,6 +35,7 @@ import CoreStore
class ConvenienceTests: BaseTestCase { class ConvenienceTests: BaseTestCase {
@objc @objc
@MainActor
dynamic func test_ThatDataStacks_CanCreateFetchedResultsControllers() { dynamic func test_ThatDataStacks_CanCreateFetchedResultsControllers() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -62,6 +63,7 @@ class ConvenienceTests: BaseTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatUnsafeDataTransactions_CanCreateFetchedResultsControllers() { dynamic func test_ThatUnsafeDataTransactions_CanCreateFetchedResultsControllers() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
+35 -1
View File
@@ -162,6 +162,18 @@ class Person: CoreStoreObject {
@Field.Relationship("_spouseInverse", inverse: \.$spouse) @Field.Relationship("_spouseInverse", inverse: \.$spouse)
private var spouseInverse: Person? 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<UUID>
}
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? { private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
if let value = field.primitiveValue { if let value = field.primitiveValue {
@@ -203,7 +215,7 @@ class DynamicModelTests: BaseTestDataTestCase {
versionLock: [ versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a], "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e], "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 willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done")
let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done") let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done")
let didSetObserverDone = self.expectation(description: "didSet-observe-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( stack.perform(
asynchronous: { (transaction) in asynchronous: { (transaction) in
@@ -373,11 +393,19 @@ class DynamicModelTests: BaseTestDataTestCase {
person.job = .engineer person.job = .engineer
XCTAssertEqual(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)! let personSnapshot2 = person.asSnapshot(in: transaction)!
XCTAssertEqual(person.name, personSnapshot2.$name) XCTAssertEqual(person.name, personSnapshot2.$name)
XCTAssertEqual(person.title, personSnapshot2.$title) XCTAssertEqual(person.title, personSnapshot2.$title)
XCTAssertEqual(person.displayName, personSnapshot2.$displayName) XCTAssertEqual(person.displayName, personSnapshot2.$displayName)
XCTAssertEqual(person.job, personSnapshot2.$job) XCTAssertEqual(person.job, personSnapshot2.$job)
XCTAssertEqual(person.ownerInfosJson, personSnapshot2.$ownerInfosJson)
XCTAssertEqual(person.ownerInfosPlist, personSnapshot2.$ownerInfosPlist)
var personSnapshot3 = personSnapshot2 var personSnapshot3 = personSnapshot2
personSnapshot3.$name = "James" personSnapshot3.$name = "James"
@@ -390,6 +418,8 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertEqual(personSnapshot3.$name, "James") XCTAssertEqual(personSnapshot3.$name, "James")
XCTAssertEqual(personSnapshot3.$displayName, "Sir John") XCTAssertEqual(personSnapshot3.$displayName, "Sir John")
XCTAssertEqual(personSnapshot3.$job, .engineer) 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.$name, "John")
XCTAssertEqual(personPublisher.$displayName, "Sir John") XCTAssertEqual(personPublisher.$displayName, "Sir John")
XCTAssertEqual(personPublisher.$job, .engineer) XCTAssertEqual(personPublisher.$job, .engineer)
XCTAssertEqual(personPublisher.$ownerInfosJson, ownerInfos)
XCTAssertEqual(personPublisher.$ownerInfosPlist, ownerInfos)
updateDone.fulfill() updateDone.fulfill()
}, },
@@ -444,6 +476,8 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertEqual(person!.customField.string, "customString") XCTAssertEqual(person!.customField.string, "customString")
XCTAssertEqual(person!.job, .engineer) XCTAssertEqual(person!.job, .engineer)
XCTAssertEqual(person!.pets.first, dog) XCTAssertEqual(person!.pets.first, dog)
XCTAssertEqual(person!.ownerInfosJson, ownerInfos)
XCTAssertEqual(person!.ownerInfosPlist, ownerInfos)
let p3 = Where<Dog>({ $0.$age == 10 }) let p3 = Where<Dog>({ $0.$age == 10 })
XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10)) XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))
+1
View File
@@ -63,6 +63,7 @@ final class GroupByTests: BaseTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatGroupByClauses_ApplyToFetchRequestsCorrectly() { dynamic func test_ThatGroupByClauses_ApplyToFetchRequestsCorrectly() {
self.prepareStack { (dataStack) in self.prepareStack { (dataStack) in
+9 -5
View File
@@ -36,6 +36,7 @@ import CoreStore
class ListPublisherTests: BaseTestDataTestCase { class ListPublisherTests: BaseTestDataTestCase {
@objc @objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveInsertNotifications() { dynamic func test_ThatListPublishers_CanReceiveInsertNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -62,7 +63,7 @@ class ListPublisherTests: BaseTestDataTestCase {
let saveExpectation = self.expectation(description: "save") let saveExpectation = self.expectation(description: "save")
stack.perform( stack.perform(
asynchronous: { (transaction) -> Bool in asynchronous: { [dateFormatter] (transaction) -> Bool in
let object = transaction.create(Into<TestEntity1>()) let object = transaction.create(Into<TestEntity1>())
object.testBoolean = NSNumber(value: true) object.testBoolean = NSNumber(value: true)
@@ -70,7 +71,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "1") object.testDecimal = NSDecimalNumber(string: "1")
object.testString = "nil:TestEntity1:1" object.testString = "nil:TestEntity1:1"
object.testData = ("nil:TestEntity1:1" as NSString).data(using: String.Encoding.utf8.rawValue)! 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 return transaction.hasChanges
}, },
@@ -92,6 +93,7 @@ class ListPublisherTests: BaseTestDataTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveUpdateNotifications() { dynamic func test_ThatListPublishers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -126,7 +128,7 @@ class ListPublisherTests: BaseTestDataTestCase {
let saveExpectation = self.expectation(description: "save") let saveExpectation = self.expectation(description: "save")
stack.perform( stack.perform(
asynchronous: { (transaction) -> Bool in asynchronous: { [dateFormatter] (transaction) -> Bool in
if let object = try transaction.fetchOne( if let object = try transaction.fetchOne(
From<TestEntity1>(), From<TestEntity1>(),
@@ -136,7 +138,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "11") object.testDecimal = NSDecimalNumber(string: "11")
object.testString = "nil:TestEntity1:11" object.testString = "nil:TestEntity1:11"
object.testData = ("nil:TestEntity1:11" as NSString).data(using: String.Encoding.utf8.rawValue)! 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 { else {
@@ -150,7 +152,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "22") object.testDecimal = NSDecimalNumber(string: "22")
object.testString = "nil:TestEntity1:22" object.testString = "nil:TestEntity1:22"
object.testData = ("nil:TestEntity1:22" as NSString).data(using: String.Encoding.utf8.rawValue)! 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 { else {
@@ -176,6 +178,7 @@ class ListPublisherTests: BaseTestDataTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveMoveNotifications() { dynamic func test_ThatListPublishers_CanReceiveMoveNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -242,6 +245,7 @@ class ListPublisherTests: BaseTestDataTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveDeleteNotifications() { dynamic func test_ThatListPublishers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
+54 -39
View File
@@ -24,6 +24,7 @@
// //
import XCTest import XCTest
import os
@testable @testable
import CoreStore import CoreStore
@@ -34,6 +35,7 @@ import CoreStore
class ObjectObserverTests: BaseTestDataTestCase { class ObjectObserverTests: BaseTestDataTestCase {
@objc @objc
@MainActor
dynamic func test_ThatObjectObservers_CanReceiveUpdateNotifications() { dynamic func test_ThatObjectObservers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -54,23 +56,27 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object) XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted) XCTAssertFalse(monitor.isObjectDeleted)
var events = 0 let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
_ = self.expectation( _ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"), forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"),
object: observer, object: observer,
handler: { (note) -> Bool in handler: { (note) -> Bool in
XCTAssertEqual(events, 0) nonisolated(unsafe) let note = note
XCTAssertEqual( return events.withLock { events in
((note.userInfo as NSDictionary?) ?? [:]),
["object": object] as NSDictionary
)
defer {
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( _ = self.expectation(
@@ -78,28 +84,32 @@ class ObjectObserverTests: BaseTestDataTestCase {
object: observer, object: observer,
handler: { (note) -> Bool in handler: { (note) -> Bool in
XCTAssertEqual(events, 1) nonisolated(unsafe) let note = note
XCTAssertEqual( return events.withLock { events in
((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 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") let saveExpectation = self.expectation(description: "save")
@@ -131,6 +141,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatObjectObservers_CanReceiveDeleteNotifications() { dynamic func test_ThatObjectObservers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -151,23 +162,27 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object) XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted) XCTAssertFalse(monitor.isObjectDeleted)
var events = 0 let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
_ = self.expectation( _ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"), forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"),
object: observer, object: observer,
handler: { (note) -> Bool in handler: { (note) -> Bool in
XCTAssertEqual(events, 0) nonisolated(unsafe) let note = note
XCTAssertEqual( return events.withLock { events in
((note.userInfo as NSDictionary?) ?? [:]),
["object": object] as NSDictionary
)
defer {
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") let saveExpectation = self.expectation(description: "save")
@@ -202,7 +217,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
// MARK: TestObjectObserver // MARK: TestObjectObserver
class TestObjectObserver: ObjectObserver { final class TestObjectObserver: ObjectObserver {
typealias ObjectEntityType = TestEntity1 typealias ObjectEntityType = TestEntity1
@@ -34,6 +34,7 @@ import CoreStore
class ObjectPublisherTests: BaseTestDataTestCase { class ObjectPublisherTests: BaseTestDataTestCase {
@objc @objc
@MainActor
dynamic func test_ThatObjectPublishers_CanReceiveUpdateNotifications() { dynamic func test_ThatObjectPublishers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
@@ -93,6 +94,7 @@ class ObjectPublisherTests: BaseTestDataTestCase {
} }
@objc @objc
@MainActor
dynamic func test_ThatObjectPublishers_CanReceiveDeleteNotifications() { dynamic func test_ThatObjectPublishers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in self.prepareStack { (stack) in
+1
View File
@@ -35,6 +35,7 @@ import CoreStore
class SetupTests: BaseTestDataTestCase { class SetupTests: BaseTestDataTestCase {
@objc @objc
@MainActor
dynamic func test_ThatDataStacks_ConfigureCorrectly() { dynamic func test_ThatDataStacks_ConfigureCorrectly() {
do { do {
@@ -74,7 +74,7 @@ extension Modern {
// MARK: - TransactionSource // MARK: - TransactionSource
enum TransactionSource { enum TransactionSource: Sendable {
case add case add
case delete case delete
+1 -1
View File
@@ -29,7 +29,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "CoreStore", name: "CoreStore",
platforms: [ platforms: [
.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9) .macOS(.v14), .iOS(.v17), .tvOS(.v17), .watchOS(.v10)
], ],
products: [ products: [
.library(name: "CoreStore", targets: ["CoreStore"]) .library(name: "CoreStore", targets: ["CoreStore"])
+5 -3
View File
@@ -175,7 +175,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
internal init( internal init(
mainContext: NSManagedObjectContext, mainContext: NSManagedObjectContext,
queue: DispatchQueue, queue: DispatchQueue,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
super.init( super.init(
@@ -188,7 +188,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
} }
internal func autoCommit( internal func autoCommit(
_ completion: @escaping ( _ completion: @escaping @MainActor (
_ hasChanges: Bool, _ hasChanges: Bool,
_ error: CoreStoreError? _ error: CoreStoreError?
) -> Void ) -> Void
@@ -197,12 +197,14 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
self.isCommitted = true self.isCommitted = true
let group = DispatchGroup() let group = DispatchGroup()
group.enter() group.enter()
nonisolated(unsafe) let transaction = self
self.context.saveAsynchronously( self.context.saveAsynchronously(
sourceIdentifier: self.sourceIdentifier, sourceIdentifier: self.sourceIdentifier,
completion: { (hasChanges, error) -> Void in completion: { (hasChanges, error) -> Void in
completion(hasChanges, error) completion(hasChanges, error)
self.result = (hasChanges, error) transaction.result = (hasChanges, error)
group.leave() group.leave()
} }
) )
+6 -3
View File
@@ -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. 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. 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, queue: DispatchQueue,
supportsUndo: Bool, supportsUndo: Bool,
bypassesQueueing: Bool, bypassesQueueing: Bool,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
let context = mainContext.temporaryContextInTransactionWithConcurrencyType( let context = mainContext.temporaryContextInTransactionWithConcurrencyType(
@@ -493,7 +493,10 @@ public /*abstract*/ class BaseDataTransaction {
} }
else if context.undoManager == nil { else if context.undoManager == nil {
context.undoManager = UndoManager() Internals.mainActorImmediate {
context.undoManager = UndoManager()
}
} }
} }
+38 -14
View File
@@ -24,6 +24,7 @@
// //
import Foundation import Foundation
import os
// MARK: - CoreStoreDefaults // MARK: - CoreStoreDefaults
@@ -36,7 +37,29 @@ public enum CoreStoreDefaults {
/** /**
The `CoreStoreLogger` instance to be used. The default logger is an instance of a `DefaultLogger`. 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. 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 { public static var dataStack: DataStack {
get { get {
self.defaultStackBarrierQueue.sync(flags: .barrier) { return self.defaultStackInstance.withLock {
if self.defaultStackInstance == nil { if let dataStack = $0 {
self.defaultStackInstance = DataStack() return dataStack
} }
let dataStack = DataStack()
$0 = dataStack
return dataStack
} }
return self.defaultStackInstance!
} }
set { set {
self.defaultStackBarrierQueue.async(flags: .barrier) { self.defaultStackInstance.withLock {
self.defaultStackInstance = newValue $0 = newValue
} }
} }
} }
@@ -69,7 +94,6 @@ public enum CoreStoreDefaults {
// MARK: Private // MARK: Private
private static let defaultStackBarrierQueue = DispatchQueue.concurrent("com.coreStore.defaultStackBarrierQueue", qos: .userInteractive) private static let defaultStackInstance: OSAllocatedUnfairLock<DataStack?> = .init(initialState: nil)
private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil)
private static var defaultStackInstance: DataStack?
} }
+2 -2
View File
@@ -24,7 +24,7 @@
// //
import Foundation import Foundation
import CoreData @preconcurrency import CoreData
// MARK: - CoreStoreError // MARK: - CoreStoreError
@@ -32,7 +32,7 @@ import CoreData
/** /**
All errors thrown from CoreStore are expressed in `CoreStoreError` enum values. 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. A failure occured because of an unknown error.
+1 -1
View File
@@ -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. 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. Handles log messages sent by the `CoreStore` framework.
+2 -10
View File
@@ -29,7 +29,8 @@ import Foundation
// MARK: - CoreStoreManagedObject // MARK: - CoreStoreManagedObject
@objc internal class CoreStoreManagedObject: NSManagedObject { @objc
internal class CoreStoreManagedObject: NSManagedObject {
internal typealias CustomGetter = @convention(block) (_ rawObject: Any) -> Any? internal typealias CustomGetter = @convention(block) (_ rawObject: Any) -> Any?
internal typealias CustomSetter = @convention(block) (_ rawObject: Any, _ newValue: Any?) -> Void 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)" 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<KeyPathString>]] = [:]
}
+1 -1
View File
@@ -439,7 +439,7 @@ fileprivate final class _CoreStoreObjectKeyValueObservation: NSObject, CoreStore
// workaround for <rdar://problem/31640524> Erroneous (?) error when using bridging in the Foundation overlay // workaround for <rdar://problem/31640524> Erroneous (?) error when using bridging in the Foundation overlay
@nonobjc @nonobjc
static var swizzler: Any? = Internals.with { static let swizzler: Void? = Internals.with {
let bridgeClass: AnyClass = _CoreStoreObjectKeyValueObservation.self let bridgeClass: AnyClass = _CoreStoreObjectKeyValueObservation.self
let rootObserveImpl = class_getInstanceMethod( let rootObserveImpl = class_getInstanceMethod(
+2 -2
View File
@@ -266,6 +266,6 @@ fileprivate enum Static {
// MARK: FilePrivate // MARK: FilePrivate
fileprivate static var metaCache: [ObjectIdentifier: Any] = [:] fileprivate static nonisolated(unsafe) var metaCache: [ObjectIdentifier: Any] = [:]
fileprivate static var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:] fileprivate static nonisolated(unsafe) var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:]
} }
+36 -20
View File
@@ -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`. 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 // MARK: Public
@@ -123,8 +123,8 @@ extension DataStack.AsyncNamespace {
return .init( return .init(
bufferingPolicy: .unbounded, bufferingPolicy: .unbounded,
{ continuation in { continuation in
var progress: Progress? = nil nonisolated(unsafe) var progress: Progress? = nil
progress = self.base.addStorage( progress = self.base.addStorage(
storage, storage,
completion: { result in completion: { result in
@@ -151,14 +151,17 @@ extension DataStack.AsyncNamespace {
) )
if let progress = progress { if let progress = progress {
progress.setProgressHandler { progress in Internals.mainActorImmediate { @MainActor in
progress.setProgressHandler { progress in
continuation.yield( continuation.yield(
.migrating( .migrating(
storage: storage, storage: storage,
progressObject: progress progressObject: progress
)
) )
) }
} }
} }
} }
@@ -185,7 +188,7 @@ extension DataStack.AsyncNamespace {
return try await Internals.withCheckedThrowingContinuation { continuation in return try await Internals.withCheckedThrowingContinuation { continuation in
self.base.perform( return self.base.perform(
asynchronous: { (transaction) -> O? in asynchronous: { (transaction) -> O? in
return try transaction.importObject( return try transaction.importObject(
@@ -196,10 +199,13 @@ extension DataStack.AsyncNamespace {
success: { success: {
continuation.resume( 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 source: O.ImportSource
) async throws(any Swift.Error) -> O? { ) async throws(any Swift.Error) -> O? {
nonisolated(unsafe) let object = object
return try await Internals.withCheckedThrowingContinuation { continuation in return try await Internals.withCheckedThrowingContinuation { continuation in
self.base.perform( self.base.perform(
@@ -240,10 +247,13 @@ extension DataStack.AsyncNamespace {
success: { success: {
continuation.resume( 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: { success: {
continuation.resume( 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`. - returns: The imported objects correctly associated for the `DataStack`.
- throws: A `CoreStoreError` value indicating the failure reason - throws: A `CoreStoreError` value indicating the failure reason
*/ */
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence>( public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence & Sendable>(
_ into: Into<O>, _ into: Into<O>,
sourceArray: S, sourceArray: S,
preProcess: @escaping @Sendable ( preProcess: @escaping @Sendable (
@@ -329,10 +342,13 @@ extension DataStack.AsyncNamespace {
success: { success: {
continuation.resume( 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. - returns: The value returned from the `task` closure.
- throws: A `CoreStoreError` value indicating the failure reason - throws: A `CoreStoreError` value indicating the failure reason
*/ */
public func perform<Output>( public func perform<Output: Sendable>(
_ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift.Error) -> Output _ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift.Error) -> Output
) async throws(any Swift.Error) -> Output { ) async throws(any Swift.Error) -> Output {
+61 -29
View File
@@ -24,7 +24,8 @@
// //
import Foundation import Foundation
import CoreData @preconcurrency import CoreData
import os
// MARK: - DataStack // MARK: - DataStack
@@ -49,7 +50,7 @@ extension DataStack {
*/ */
public func addStorage<T>( public func addStorage<T>(
_ storage: T, _ storage: T,
completion: @escaping (SetupResult<T>) -> Void completion: @escaping @MainActor (SetupResult<T>) -> Void
) { ) {
self.coordinator.performAsynchronously { self.coordinator.performAsynchronously {
@@ -110,7 +111,7 @@ extension DataStack {
*/ */
public func addStorage<T: LocalStorage>( public func addStorage<T: LocalStorage>(
_ storage: T, _ storage: T,
completion: @escaping (SetupResult<T>) -> Void completion: @escaping @MainActor (SetupResult<T>) -> Void
) -> Progress? { ) -> Progress? {
let fileURL = storage.fileURL let fileURL = storage.fileURL
@@ -162,7 +163,7 @@ extension DataStack {
attributes: nil attributes: nil
) )
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( nonisolated(unsafe) let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: type(of: storage).storeType, ofType: type(of: storage).storeType,
at: fileURL as URL, at: fileURL as URL,
options: storage.storeOptions options: storage.storeOptions
@@ -192,12 +193,18 @@ extension DataStack {
} }
catch { catch {
completion(.failure(CoreStoreError(error))) DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
} }
return return
} }
completion(.failure(CoreStoreError(error))) DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
return return
} }
@@ -212,7 +219,10 @@ extension DataStack {
} }
catch { catch {
completion(.failure(CoreStoreError(error))) DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
} }
} }
) )
@@ -264,7 +274,7 @@ extension DataStack {
*/ */
public func upgradeStorageIfNeeded<T: LocalStorage>( public func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T, _ storage: T,
completion: @escaping (MigrationResult) -> Void completion: @escaping @MainActor (MigrationResult) -> Void
) throws(CoreStoreError) -> Progress? { ) throws(CoreStoreError) -> Progress? {
return try self.coordinator.performSynchronously { return try self.coordinator.performSynchronously {
@@ -376,7 +386,7 @@ extension DataStack {
private func upgradeStorageIfNeeded<T: LocalStorage>( private func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T, _ storage: T,
metadata: [String: Any], metadata: [String: Any],
completion: @escaping (MigrationResult) -> Void completion: @escaping @MainActor (MigrationResult) -> Void
) -> Progress? { ) -> Progress? {
guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else { guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else {
@@ -423,9 +433,13 @@ extension DataStack {
} }
let migrationTypes = migrationSteps.map { $0.migrationType } 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 operations = [Operation]()
var cancelled = false
let progress = Progress(parent: nil, userInfo: nil) let progress = Progress(parent: nil, userInfo: nil)
progress.totalUnitCount = numberOfMigrations progress.totalUnitCount = numberOfMigrations
@@ -440,12 +454,15 @@ extension DataStack {
operations.append( operations.append(
BlockOperation { [weak self] in BlockOperation { [weak self] in
guard let self = self, !cancelled else { guard
let self = self,
!migrationState.withLock({ $0.cancelled })
else {
return return
} }
autoreleasepool { Internals.autoreleasepool {
do { do {
@@ -465,8 +482,14 @@ extension DataStack {
migrationError, migrationError,
"Failed to migrate version model \"\(migrationType.sourceVersion)\" to version \"\(migrationType.destinationVersion)\"." "Failed to migrate version model \"\(migrationType.sourceVersion)\" to version \"\(migrationType.destinationVersion)\"."
) )
migrationResult = .failure(migrationError) migrationState.withLock { state in
cancelled = true
if !state.cancelled {
state.migrationResult = .failure(migrationError)
state.cancelled = true
}
}
} }
} }
@@ -488,7 +511,10 @@ extension DataStack {
DispatchQueue.main.async { DispatchQueue.main.async {
progress.setProgressHandler(nil) progress.setProgressHandler(nil)
completion(migrationResult ?? .success(migrationTypes)) completion(
migrationState.withLock { $0.migrationResult }
?? .success(migrationTypes)
)
return return
} }
} }
@@ -594,22 +620,28 @@ extension DataStack {
let estimatedTime: TimeInterval = 60 * 3 // 3 mins let estimatedTime: TimeInterval = 60 * 3 // 3 mins
let interval: TimeInterval = 1 let interval: TimeInterval = 1
let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount) let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount)
var fakeProgress: Float = 0 let fakeProgress: OSAllocatedUnfairLock<Float> = .init(initialState: 0)
var recursiveCheck: () -> Void = {} @Sendable
recursiveCheck = { [weak timerQueue] in 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) timerQueue.async(execute: recursiveCheck)
@@ -626,7 +658,7 @@ extension DataStack {
} }
timerQueue.sync { timerQueue.sync {
fakeProgress = 1 fakeProgress.withLock({ $0 = 1.0 })
} }
_ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel) _ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel)
progress.completedUnitCount = progress.totalUnitCount progress.completedUnitCount = progress.totalUnitCount
+6 -6
View File
@@ -125,7 +125,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorList<O>( public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ fetchClauses: FetchClause... _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorList<O>( public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ fetchClauses: [FetchClause] _ fetchClauses: [FetchClause]
) { ) {
@@ -188,7 +188,7 @@ extension DataStack {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/ */
public func monitorList<B: FetchChainableBuilderType>( public func monitorList<B: FetchChainableBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorSectionedList<O>( public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ sectionBy: SectionBy<O>, _ sectionBy: SectionBy<O>,
_ fetchClauses: FetchClause... _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorSectionedList<O>( public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ sectionBy: SectionBy<O>, _ sectionBy: SectionBy<O>,
_ fetchClauses: [FetchClause] _ fetchClauses: [FetchClause]
@@ -356,7 +356,7 @@ extension DataStack {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/ */
public func monitorSectionedList<B: SectionMonitorBuilderType>( public func monitorSectionedList<B: SectionMonitorBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B _ clauseChain: B
) { ) {
+12 -5
View File
@@ -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`. 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 // MARK: Public
@@ -99,6 +99,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.addStorage( self.base.addStorage(
storage, storage,
completion: { (result) in completion: { (result) in
@@ -179,6 +180,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform( self.base.perform(
asynchronous: { (transaction) -> O? in asynchronous: { (transaction) -> O? in
@@ -222,8 +224,10 @@ extension DataStack.ReactiveNamespace {
source: O.ImportSource source: O.ImportSource
) -> Future<O?, CoreStoreError> { ) -> Future<O?, CoreStoreError> {
nonisolated(unsafe) let object = object
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform( self.base.perform(
asynchronous: { (transaction) -> O? in asynchronous: { (transaction) -> O? in
@@ -273,6 +277,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform( self.base.perform(
asynchronous: { (transaction) -> O? in 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. - 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`. - returns: A `Future` for the imported objects. The event values will be the object instances correctly associated for the `DataStack`.
*/ */
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence>( public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence & Sendable>(
_ into: Into<O>, _ into: Into<O>,
sourceArray: S, sourceArray: S,
preProcess: @escaping ( preProcess: @escaping @Sendable (
_ mapping: [O.UniqueIDType: O.ImportSource] _ mapping: [O.UniqueIDType: O.ImportSource]
) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 } ) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 }
) -> Future<[O], CoreStoreError> where S.Iterator.Element == O.ImportSource { ) -> Future<[O], CoreStoreError> where S.Iterator.Element == O.ImportSource {
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform( self.base.perform(
asynchronous: { (transaction) -> [O] in 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`. - 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. - returns: A `Future` whose event value be the value returned from the `task` closure.
*/ */
public func perform<Output>( public func perform<Output: Sendable>(
_ asynchronous: @escaping ( _ asynchronous: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction _ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> Output ) throws(any Swift.Error) -> Output
) -> Future<Output, CoreStoreError> { ) -> Future<Output, CoreStoreError> {
return .init { (promise) in return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform( self.base.perform(
asynchronous: asynchronous, asynchronous: asynchronous,
success: { promise(.success($0)) }, success: { promise(.success($0)) },
+12 -12
View File
@@ -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 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)`. - 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<T>( public func perform<T: Sendable>(
asynchronous task: @escaping ( asynchronous task: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction _ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T, ) throws(any Swift.Error) -> T,
sourceIdentifier: Any? = nil, sourceIdentifier: (any Sendable)? = nil,
completion: @escaping (AsynchronousDataTransaction.Result<T>) -> Void completion: @escaping @Sendable (AsynchronousDataTransaction.Result<T>) -> Void
) { ) {
self.perform( 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)`. - 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<T>( public func perform<T>(
asynchronous task: @escaping ( asynchronous task: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction _ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T, ) throws(any Swift.Error) -> T,
sourceIdentifier: Any? = nil, sourceIdentifier: (any Sendable)? = nil,
success: @escaping (T) -> Void, success: @escaping @Sendable (sending T) -> Void,
failure: @escaping (CoreStoreError) -> Void failure: @escaping @Sendable (CoreStoreError) -> Void
) { ) {
let transaction = AsynchronousDataTransaction( nonisolated(unsafe) let transaction = AsynchronousDataTransaction(
mainContext: self.rootSavingContext, mainContext: self.rootSavingContext,
queue: self.childTransactionQueue, queue: self.childTransactionQueue,
sourceIdentifier: sourceIdentifier sourceIdentifier: sourceIdentifier
) )
transaction.transactionQueue.cs_async { transaction.transactionQueue.cs_async {
let userInfo: T nonisolated(unsafe) let userInfo: T
do { do {
userInfo = try task(transaction) userInfo = try task(transaction)
@@ -125,7 +125,7 @@ extension DataStack {
_ transaction: SynchronousDataTransaction _ transaction: SynchronousDataTransaction
) throws(any Swift.Error) -> T, ) throws(any Swift.Error) -> T,
waitForAllObservers: Bool = true, waitForAllObservers: Bool = true,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) throws(CoreStoreError) -> T { ) throws(CoreStoreError) -> T {
let transaction = SynchronousDataTransaction( let transaction = SynchronousDataTransaction(
@@ -172,7 +172,7 @@ extension DataStack {
*/ */
public func beginUnsafe( public func beginUnsafe(
supportsUndo: Bool = false, supportsUndo: Bool = false,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) -> UnsafeDataTransaction { ) -> UnsafeDataTransaction {
return UnsafeDataTransaction( return UnsafeDataTransaction(
+18 -14
View File
@@ -90,7 +90,8 @@ extension DataStack {
// MARK: - AddStorageSubscription // MARK: - AddStorageSubscription
fileprivate final class AddStorageSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == CoreStoreError { fileprivate final class AddStorageSubscription<S: Subscriber>: Subscription, @unchecked Sendable
where S.Input == Output, S.Failure == CoreStoreError {
// MARK: FilePrivate // MARK: FilePrivate
@@ -114,7 +115,7 @@ extension DataStack {
return return
} }
var progress: Progress? = nil nonisolated(unsafe) var progress: Progress? = nil
progress = self.dataStack.addStorage( progress = self.dataStack.addStorage(
self.storage, self.storage,
completion: { [weak self] result in completion: { [weak self] result in
@@ -150,21 +151,24 @@ extension DataStack {
) )
if let progress = progress { if let progress = progress {
progress.setProgressHandler { [weak self] progress in Internals.mainActorImmediate { @MainActor [weak self] in
guard progress.setProgressHandler { progress in
let self = self,
let subscriber = self.subscriber
else {
return guard
} let self = self,
_ = subscriber.receive( let subscriber = self.subscriber
.migrating( else {
storage: self.storage,
progressObject: progress return
}
_ = subscriber.receive(
.migrating(
storage: self.storage,
progressObject: progress
)
) )
) }
} }
} }
} }
+3 -3
View File
@@ -32,7 +32,7 @@ import CoreData
/** /**
The `DataStack` encapsulates the data model for the Core Data stack. Each `DataStack` can have multiple data stores, usually specified as a "Configuration" in the model editor. Behind the scenes, the DataStack manages its own `NSPersistentStoreCoordinator`, a root `NSManagedObjectContext` for disk saves, and a shared `NSManagedObjectContext` designed as a read-only model interface for `NSManagedObjects`. 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. 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 - parameter completion: the closure to execute after all persistent stores are removed
*/ */
public func unsafeRemoveAllPersistentStores( public func unsafeRemoveAllPersistentStores(
completion: @escaping () -> Void = {} completion: @escaping @MainActor () -> Void = {}
) { ) {
let coordinator = self.coordinator let coordinator = self.coordinator
@@ -459,7 +459,7 @@ public final class DataStack: Equatable {
// MARK: Internal // MARK: Internal
internal static var defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME" internal static let defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME"
internal let coordinator: NSPersistentStoreCoordinator internal let coordinator: NSPersistentStoreCoordinator
internal let rootSavingContext: NSManagedObjectContext internal let rootSavingContext: NSManagedObjectContext
+13 -3
View File
@@ -95,7 +95,10 @@ extension DiffableDataSource {
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from - 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 - 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.target = target
self.dataStack = dataStack self.dataStack = dataStack
@@ -106,7 +109,10 @@ extension DiffableDataSource {
Clears the target. Clears the target.
- parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`. - 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( self.dispatcher.purge(
target: self.target, 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 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`. - parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`.
*/ */
open func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) { open func apply(
_ snapshot: ListSnapshot<O>,
animatingDifferences: Bool = true,
completion: @escaping @Sendable () -> Void = {}
) {
let diffableSnapshot = snapshot.diffableSnapshot let diffableSnapshot = snapshot.diffableSnapshot
self.dispatcher.apply( self.dispatcher.apply(
@@ -83,6 +83,7 @@ extension DiffableDataSource {
- parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object - parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object
*/ */
@nonobjc @nonobjc
@MainActor
public init( public init(
collectionView: NSCollectionView, collectionView: NSCollectionView,
dataStack: DataStack, dataStack: DataStack,
@@ -102,6 +103,7 @@ extension DiffableDataSource {
// MARK: - NSCollectionViewDataSource // MARK: - NSCollectionViewDataSource
@objc @objc
@MainActor
public dynamic func numberOfSections( public dynamic func numberOfSections(
in collectionView: NSCollectionView in collectionView: NSCollectionView
) -> Int { ) -> Int {
@@ -110,6 +112,7 @@ extension DiffableDataSource {
} }
@objc @objc
@MainActor
public dynamic func collectionView( public dynamic func collectionView(
_ collectionView: NSCollectionView, _ collectionView: NSCollectionView,
numberOfItemsInSection section: Int numberOfItemsInSection section: Int
@@ -119,6 +122,7 @@ extension DiffableDataSource {
} }
@objc @objc
@MainActor
open dynamic func collectionView( open dynamic func collectionView(
_ collectionView: NSCollectionView, _ collectionView: NSCollectionView,
itemForRepresentedObjectAt indexPath: IndexPath itemForRepresentedObjectAt indexPath: IndexPath
@@ -140,6 +144,7 @@ extension DiffableDataSource {
} }
@objc @objc
@MainActor
open dynamic func collectionView( open dynamic func collectionView(
_ collectionView: NSCollectionView, _ collectionView: NSCollectionView,
viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind,
@@ -181,57 +186,90 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) {
self.base?.animator().performBatchUpdates(updates, completionHandler: { _ in completion() }) MainActor.assumeIsolated {
self.base?.animator().performBatchUpdates(updates, completionHandler: { _ in completion() })
}
} }
public func reloadData() { public func reloadData() {
self.base?.reloadData() MainActor.assumeIsolated {
self.base?.reloadData()
}
} }
} }
} }
@@ -185,57 +185,90 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) {
self.base?.performBatchUpdates(updates, completion: { _ in completion() }) Internals.mainActorImmediate {
self.base?.performBatchUpdates(updates, completion: { _ in completion() })
}
} }
public func reloadData() { public func reloadData() {
self.base?.reloadData() Internals.mainActorImmediate {
self.base?.reloadData()
}
} }
} }
} }
@@ -85,7 +85,7 @@ extension DiffableDataSource {
public init( public init(
tableView: UITableView, tableView: UITableView,
dataStack: DataStack, dataStack: DataStack,
cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell? cellProvider: @escaping @MainActor (UITableView, IndexPath, O) -> UITableViewCell?
) { ) {
self.cellProvider = cellProvider self.cellProvider = cellProvider
@@ -97,6 +97,7 @@ extension DiffableDataSource {
/** /**
The target `UITableView` The target `UITableView`
*/ */
@MainActor
public var tableView: UITableView? { public var tableView: UITableView? {
return self.target.base return self.target.base
@@ -240,61 +241,94 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { 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) { public func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void) {
guard let base = self.base else { MainActor.assumeIsolated {
return guard let base = self.base else {
return
}
base.performBatchUpdates(updates, completion: { _ in completion() })
} }
base.performBatchUpdates(updates, completion: { _ in completion() })
} }
public func reloadData() { public func reloadData() {
self.base?.reloadData() MainActor.assumeIsolated {
self.base?.reloadData()
}
} }
} }
} }
+11 -5
View File
@@ -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. 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 // MARK: Public
@@ -98,7 +98,7 @@ public protocol DiffableDataSourceTarget {
/** /**
Animates multiple insert, delete, reload, and move operations as a group. 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. Reloads all sections and items.
@@ -110,18 +110,24 @@ extension DiffableDataSource.Target {
// MARK: Internal // MARK: Internal
internal func reload<C, O>( internal func reload<C: Sendable, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>, using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
animated: Bool, animated: Bool,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil, interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void, setData: @escaping @Sendable (C) -> Void,
completion: @escaping () -> Void completion: @escaping () -> Void
) { ) {
let group = DispatchGroup() let group = DispatchGroup()
defer { defer {
group.notify(queue: .main, execute: completion) group.notify(
queue: .main,
execute: {
completion()
}
)
} }
if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data { if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data {
+2 -2
View File
@@ -99,7 +99,7 @@ extension DispatchQueue {
@nonobjc @inline(__always) @nonobjc @inline(__always)
internal func cs_async( internal func cs_async(
_ closure: @escaping () -> Void _ closure: @escaping @Sendable () -> Void
) { ) {
self.async { autoreleasepool(invoking: closure) } self.async { autoreleasepool(invoking: closure) }
@@ -115,7 +115,7 @@ extension DispatchQueue {
@nonobjc @inline(__always) @nonobjc @inline(__always)
internal func cs_barrierAsync( internal func cs_barrierAsync(
_ closure: @escaping () -> Void _ closure: @escaping @Sendable () -> Void
) { ) {
self.async(flags: .barrier) { autoreleasepool(invoking: closure) } self.async(flags: .barrier) { autoreleasepool(invoking: closure) }
+1 -1
View File
@@ -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`. 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`. Return `true` if an object should be created from `source`. Return `false` to ignore and skip `source`. The default implementation returns `true`.
+5 -2
View File
@@ -31,7 +31,7 @@ import CoreData
/** /**
A storage interface that is backed only in memory. 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 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`. 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. Do not call directly. Used by the `DataStack` internally.
+1 -1
View File
@@ -95,7 +95,7 @@ extension Internals {
// MARK: FilePrivate // MARK: FilePrivate
fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:] fileprivate static nonisolated(unsafe) var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:]
// MARK: - TransformableDefaultValueCodingBox // MARK: - TransformableDefaultValueCodingBox
@@ -36,7 +36,7 @@ extension Internals {
// Bugfix for NSFetchRequest messing up memory management for `affectedStores` // Bugfix for NSFetchRequest messing up memory management for `affectedStores`
// http://stackoverflow.com/questions/14396375/nsfetchedresultscontroller-crashes-in-ios-6-if-affectedstores-is-specified // http://stackoverflow.com/questions/14396375/nsfetchedresultscontroller-crashes-in-ios-6-if-affectedstores-is-specified
internal final class CoreStoreFetchRequest<T: NSFetchRequestResult>: NSFetchRequest<NSFetchRequestResult> { internal final class CoreStoreFetchRequest<T: NSFetchRequestResult>: NSFetchRequest<NSFetchRequestResult>, @unchecked Sendable {
@nonobjc @nonobjc
internal func safeAffectedStores() -> [NSPersistentStore]? { internal func safeAffectedStores() -> [NSPersistentStore]? {
@@ -33,7 +33,7 @@ extension Internals {
// MARK: - CoreStoreFetchedResultsController // MARK: - CoreStoreFetchedResultsController
internal final class CoreStoreFetchedResultsController: NSFetchedResultsController<NSManagedObject> { internal final class CoreStoreFetchedResultsController: NSFetchedResultsController<NSManagedObject>, @unchecked Sendable {
// MARK: Internal // MARK: Internal
@@ -43,7 +43,7 @@ extension Internals {
// MARK: - DiffableDataSourceSnapshot // MARK: - DiffableDataSourceSnapshot
// Implementation based on https://github.com/ra1028/DiffableDataSources // Implementation based on https://github.com/ra1028/DiffableDataSources
internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol { internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol, @unchecked Sendable {
// MARK: Internal // MARK: Internal
@@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - ChangeSet // MARK: - ChangeSet
// Implementation based on https://github.com/ra1028/DifferenceKit // Implementation based on https://github.com/ra1028/DifferenceKit
internal struct Changeset<C: Collection>: Equatable where C: Equatable { internal struct Changeset<C: Collection & Sendable>: Equatable, Sendable
where C: Equatable {
var data: C var data: C
var sectionDeleted: [Int] var sectionDeleted: [Int]
@@ -36,7 +36,7 @@ extension Internals.DiffableDataUIDispatcher {
// Implementation based on https://github.com/ra1028/DifferenceKit // Implementation based on https://github.com/ra1028/DifferenceKit
@usableFromInline @usableFromInline
internal struct DiffResult<Index> { internal struct DiffResult<Index: Sendable>: Sendable {
@usableFromInline @usableFromInline
internal let deleted: [Index] internal let deleted: [Index]
@@ -213,7 +213,7 @@ extension Internals.DiffableDataUIDispatcher {
// Implementation based on https://github.com/ra1028/DifferenceKit // Implementation based on https://github.com/ra1028/DifferenceKit
@usableFromInline @usableFromInline
internal struct Trace<I> { internal struct Trace<I: Sendable>: Sendable {
@usableFromInline @usableFromInline
internal var reference: I? internal var reference: I?
@@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - StagedChangeset // MARK: - StagedChangeset
// Implementation based on https://github.com/ra1028/DifferenceKit // Implementation based on https://github.com/ra1028/DifferenceKit
internal struct StagedChangeset<C: Collection>: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection where C: Equatable { internal struct StagedChangeset<C: Collection & Sendable>: ExpressibleByArrayLiteral, Equatable, RandomAccessCollection, RangeReplaceableCollection, Sendable
where C: Equatable {
@usableFromInline @usableFromInline
var changesets: ContiguousArray<Changeset<C>> var changesets: ContiguousArray<Changeset<C>>
@@ -117,7 +118,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable // 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 @inlinable
internal init(source: C, target: C) { 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 // 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 @inlinable
internal init(source: C, target: C) { internal init(source: C, target: C) {
@@ -32,6 +32,8 @@ import QuartzCore
#endif #endif
import os
// MARK: - Internals // MARK: - Internals
@@ -41,7 +43,7 @@ extension Internals {
// Implementation based on https://github.com/ra1028/DiffableDataSources // Implementation based on https://github.com/ra1028/DiffableDataSources
@usableFromInline @usableFromInline
internal final class DiffableDataUIDispatcher<O: DynamicObject> { internal final class DiffableDataUIDispatcher<O: DynamicObject>: @unchecked Sendable {
// MARK: Internal // MARK: Internal
@@ -55,10 +57,10 @@ extension Internals {
func purge<Target: DiffableDataSource.Target>( func purge<Target: DiffableDataSource.Target>(
target: Target?, target: Target?,
animatingDifferences: Bool, animatingDifferences: Bool,
performUpdates: @escaping ( performUpdates: @escaping @Sendable (
Target, Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void ) -> Void
) { ) {
@@ -74,14 +76,14 @@ extension Internals {
_ snapshot: DiffableDataSourceSnapshot, _ snapshot: DiffableDataSourceSnapshot,
target: Target?, target: Target?,
animatingDifferences: Bool, animatingDifferences: Bool,
performUpdates: @escaping ( performUpdates: @escaping @Sendable (
Target, Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>, StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void ) -> Void
) { ) {
self.dispatcher.dispatch { [weak self] in self.dispatcher.dispatch { @MainActor [weak self] in
guard let self = self else { guard let self = self else {
@@ -97,13 +99,17 @@ extension Internals {
return return
} }
let performDiffingUpdates: () -> Void = { let performDiffingUpdates: @MainActor () -> Void = {
let changeset = StagedChangeset(source: self.sections, target: newSections) let changeset = StagedChangeset(source: self.sections, target: newSections)
performUpdates(target, changeset) { sections in performUpdates(
target,
self.sections = sections changeset,
} { sections in
self.sections = sections
}
)
} }
#if os(watchOS) || !canImport(QuartzCore) #if os(watchOS) || !canImport(QuartzCore)
@@ -212,7 +218,7 @@ extension Internals {
// MARK: - ElementPath // MARK: - ElementPath
@usableFromInline @usableFromInline
internal struct ElementPath: Hashable { internal struct ElementPath: Hashable, Sendable {
@usableFromInline @usableFromInline
var element: Int var element: Int
@@ -231,18 +237,21 @@ extension Internals {
// MARK: - MainThreadSerialDispatcher // MARK: - MainThreadSerialDispatcher
fileprivate final class MainThreadSerialDispatcher { fileprivate final class MainThreadSerialDispatcher: Sendable {
// MARK: FilePrivate // MARK: FilePrivate
fileprivate init() {} fileprivate init() {}
fileprivate func dispatch(_ action: @escaping () -> Void) { fileprivate func dispatch(_ action: @escaping @MainActor () -> Void) {
let count = self.executingCount.incrementAndGet() let count = self.executingCount.incrementAndGet()
if Thread.isMainThread && count == 1 { if Thread.isMainThread && count == 1 {
action() MainActor.assumeIsolated {
action()
}
self.executingCount.decrement() self.executingCount.decrement()
} }
else { else {
@@ -267,36 +276,31 @@ extension Internals {
// MARK: - AtomicInt // MARK: - AtomicInt
fileprivate class AtomicInt { fileprivate final class AtomicInt: Sendable {
// MARK: FilePrivate // MARK: FilePrivate
fileprivate func incrementAndGet() -> Int { fileprivate func incrementAndGet() -> Int {
self.lock.wait() return self.value.withLock {
defer {
self.lock.signal() $0 += 1
return $0
} }
self.value += 1
return self.value
} }
fileprivate func decrement() { fileprivate func decrement() {
self.lock.wait() self.value.withLock {
defer {
self.lock.signal() $0 -= 1
} }
self.value -= 1
} }
// MARK: Private // MARK: Private
private let lock = DispatchSemaphore(value: 1) private let value: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
private var value = 0
} }
} }
@@ -64,7 +64,7 @@ extension Internals {
// MARK: - FetchedResultsControllerDelegate // MARK: - FetchedResultsControllerDelegate
internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate { internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate, @unchecked Sendable {
// MARK: Internal // MARK: Internal
+1 -1
View File
@@ -42,7 +42,7 @@ extension Internals {
notificationName: Notification.Name, notificationName: Notification.Name,
object: Any?, object: Any?,
queue: OperationQueue? = nil, queue: OperationQueue? = nil,
closure: @escaping (_ note: Notification) -> Void closure: @escaping @Sendable (_ note: Notification) -> Void
) { ) {
self.observer = NotificationCenter.default.addObserver( self.observer = NotificationCenter.default.addObserver(
@@ -31,7 +31,7 @@ extension Internals {
// MARK: - SharedNotificationObserver // MARK: - SharedNotificationObserver
internal final class SharedNotificationObserver<T> { internal final class SharedNotificationObserver<T>: @unchecked Sendable {
// MARK: Internal // MARK: Internal
@@ -39,7 +39,7 @@ extension Internals {
notificationName: Notification.Name, notificationName: Notification.Name,
object: Any?, object: Any?,
queue: OperationQueue? = nil, queue: OperationQueue? = nil,
sharedValue: @escaping (_ note: Notification) -> T sharedValue: @escaping @Sendable (_ note: Notification) -> T
) { ) {
self.observer = NotificationCenter.default.addObserver( self.observer = NotificationCenter.default.addObserver(
+51 -16
View File
@@ -121,6 +121,7 @@ internal enum Internals {
return closure() return closure()
} }
@inline(__always) @inline(__always)
internal static func autoreleasepool<T>( internal static func autoreleasepool<T>(
@@ -129,7 +130,7 @@ internal enum Internals {
return ObjectiveC.autoreleasepool(invoking: closure) return ObjectiveC.autoreleasepool(invoking: closure)
} }
@inline(__always) @inline(__always)
internal static func autoreleasepool<T>( internal static func autoreleasepool<T>(
_ closure: () throws(any Swift.Error) -> T _ closure: () throws(any Swift.Error) -> T
@@ -137,31 +138,65 @@ internal enum Internals {
return try ObjectiveC.autoreleasepool(invoking: closure) return try ObjectiveC.autoreleasepool(invoking: closure)
} }
@inline(__always) @inline(__always)
internal static func autoreleasepool<T>( internal static func autoreleasepool<T, E>(
_ closure: () throws(CoreStoreError) -> T _ closure: () throws(E) -> T
) throws(CoreStoreError) -> T { ) throws(E) -> T {
do { return try ObjectiveC.autoreleasepool(invoking: closure)
return try ObjectiveC.autoreleasepool(invoking: closure)
}
catch {
throw CoreStoreError(error)
}
} }
@inline(__always) @inline(__always)
internal static func withCheckedThrowingContinuation<T>( internal static func withCheckedThrowingContinuation<T>(
function: String = #function, function: String = #function,
_ body: (CheckedContinuation<T, any Swift.Error>) -> Void _ body: (CheckedContinuation<T, any Swift.Error>) -> Void
) async throws(any Swift.Error) -> T { ) async throws(any Swift.Error) -> sending T {
return try await _Concurrency.withCheckedThrowingContinuation( return try await _Concurrency.withCheckedThrowingContinuation(
function: function, function: function,
body body
) )
} }
@inline(__always)
internal static func withCheckedThrowingContinuation<T, E>(
function: String = #function,
_ body: (CheckedContinuation<T, E>) -> 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()
}
}
}
} }
+1 -1
View File
@@ -39,7 +39,7 @@ import CoreData
let person = transaction.create(Into<MyPersonEntity>("Configuration1")) let person = transaction.create(Into<MyPersonEntity>("Configuration1"))
``` ```
*/ */
public struct Into<O: DynamicObject>: Hashable { public struct Into<O: DynamicObject>: Hashable, @unchecked Sendable {
/** /**
The associated `NSManagedObject` or `CoreStoreObject` entity class The associated `NSManagedObject` or `CoreStoreObject` entity class
+23 -23
View File
@@ -66,7 +66,7 @@ import CoreData
``` ```
In the example above, both `person1` and `person2` will contain the object at section=2, index=3. In the example above, both `person1` and `person2` will contain the object at section=2, index=3.
*/ */
public final class ListMonitor<O: DynamicObject>: Hashable { public final class ListMonitor<O: DynamicObject>: Hashable, @unchecked Sendable {
// MARK: Public (Accessors) // MARK: Public (Accessors)
@@ -648,7 +648,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
*/ */
public func refetch( public func refetch(
_ fetchClauses: FetchClause..., _ fetchClauses: FetchClause...,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) { ) {
self.refetch( self.refetch(
@@ -668,7 +668,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
*/ */
public func refetch( public func refetch(
_ fetchClauses: [FetchClause], _ fetchClauses: [FetchClause],
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) { ) {
self.refetch( self.refetch(
@@ -751,7 +751,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>, from: From<O>,
sectionBy: SectionBy<O>?, sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: @escaping (ListMonitor<O>) -> Void createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void
) { ) {
self.init( self.init(
@@ -786,7 +786,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>, from: From<O>,
sectionBy: SectionBy<O>?, sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: @escaping (ListMonitor<O>) -> Void createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void
) { ) {
self.init( self.init(
@@ -803,7 +803,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer, _ notificationKey: UnsafeRawPointer,
name: Notification.Name, name: Notification.Name,
toObserver observer: AnyObject, toObserver observer: AnyObject,
callback: @escaping (_ monitor: ListMonitor<O>) -> Void callback: @escaping @Sendable (_ monitor: ListMonitor<O>) -> Void
) { ) {
Internals.setAssociatedRetainedObject( Internals.setAssociatedRetainedObject(
@@ -828,7 +828,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer, _ notificationKey: UnsafeRawPointer,
name: Notification.Name, name: Notification.Name,
toObserver observer: AnyObject, toObserver observer: AnyObject,
callback: @escaping ( callback: @escaping @Sendable (
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ object: O, _ object: O,
_ indexPath: IndexPath?, _ indexPath: IndexPath?,
@@ -864,7 +864,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer, _ notificationKey: UnsafeRawPointer,
name: Notification.Name, name: Notification.Name,
toObserver observer: AnyObject, toObserver observer: AnyObject,
callback: @escaping ( callback: @escaping @Sendable (
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo, _ sectionInfo: NSFetchedResultsSectionInfo,
_ sectionIndex: Int _ sectionIndex: Int
@@ -892,21 +892,21 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
) )
} }
internal func registerObserver<U: AnyObject>( internal func registerObserver<U: AnyObject & Sendable>(
_ observer: U, _ observer: U,
willChange: @escaping ( willChange: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O> _ monitor: ListMonitor<O>
) -> Void, ) -> Void,
didChange: @escaping ( didChange: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O> _ monitor: ListMonitor<O>
) -> Void, ) -> Void,
willRefetch: @escaping ( willRefetch: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O> _ monitor: ListMonitor<O>
) -> Void, ) -> Void,
didRefetch: @escaping ( didRefetch: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O> _ monitor: ListMonitor<O>
) -> Void) { ) -> Void) {
@@ -969,27 +969,27 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
) )
} }
internal func registerObserver<U: AnyObject>( internal func registerObserver<U: AnyObject & Sendable>(
_ observer: U, _ observer: U,
didInsertObject: @escaping ( didInsertObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ object: O, _ object: O,
_ toIndexPath: IndexPath _ toIndexPath: IndexPath
) -> Void, ) -> Void,
didDeleteObject: @escaping ( didDeleteObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ object: O, _ object: O,
_ fromIndexPath: IndexPath _ fromIndexPath: IndexPath
) -> Void, ) -> Void,
didUpdateObject: @escaping ( didUpdateObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ object: O, _ object: O,
_ atIndexPath: IndexPath _ atIndexPath: IndexPath
) -> Void, ) -> Void,
didMoveObject: @escaping ( didMoveObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ object: O, _ object: O,
@@ -1056,15 +1056,15 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
) )
} }
internal func registerObserver<U: AnyObject>( internal func registerObserver<U: AnyObject & Sendable>(
_ observer: U, _ observer: U,
didInsertSection: @escaping ( didInsertSection: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo, _ sectionInfo: NSFetchedResultsSectionInfo,
_ toIndex: Int _ toIndex: Int
) -> Void, ) -> Void,
didDeleteSection: @escaping ( didDeleteSection: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ListMonitor<O>, _ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo, _ sectionInfo: NSFetchedResultsSectionInfo,
@@ -1127,7 +1127,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
internal func refetch( internal func refetch(
_ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, _ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
Internals.assert( Internals.assert(
@@ -1302,7 +1302,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>, from: From<O>,
sectionBy: SectionBy<O>?, sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: ((ListMonitor<O>) -> Void)? createAsynchronously: (@Sendable (ListMonitor<O>) -> Void)?
) { ) {
self.isSectioned = (sectionBy != nil) self.isSectioned = (sectionBy != nil)
+1 -1
View File
@@ -39,7 +39,7 @@ import CoreData
monitor.addObserver(self) monitor.addObserver(self)
``` ```
*/ */
public protocol ListObserver: AnyObject { public protocol ListObserver: AnyObject, Sendable {
/** /**
The `NSManagedObject` type for the observed list The `NSManagedObject` type for the observed list
+19 -22
View File
@@ -90,7 +90,8 @@ extension ListPublisher {
// MARK: - ListSnapshotSubscription // MARK: - ListSnapshotSubscription
fileprivate final class ListSnapshotSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == Never { fileprivate final class ListSnapshotSubscription<S: Subscriber>: Subscription, @unchecked Sendable
where S.Input == Output, S.Failure == Never {
// MARK: FilePrivate // MARK: FilePrivate
@@ -114,21 +115,24 @@ extension ListPublisher {
return return
} }
self.publisher.addObserver( Internals.mainActorImmediate { [self] in
self,
notifyInitial: self.emitInitialValue, self.publisher.addObserver(
{ [weak self] (publisher) in self,
notifyInitial: self.emitInitialValue,
guard { [weak self] (publisher) in
let self = self,
let subscriber = self.subscriber
else {
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 self.subscriber = nil
if Thread.isMainThread { Internals.mainActorImmediate {
self.publisher.removeObserver(self) self.publisher.removeObserver(self)
} }
else {
DispatchQueue.main.async {
self.publisher.removeObserver(self)
}
}
} }
+6 -3
View File
@@ -94,6 +94,7 @@ public final class ListPublisher<O: DynamicObject>: 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 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 - parameter callback: the closure to execute when changes occur
*/ */
@MainActor
public func addObserver<T: AnyObject>( public func addObserver<T: AnyObject>(
_ observer: T, _ observer: T,
notifyInitial: Bool = false, notifyInitial: Bool = false,
@@ -128,6 +129,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
- parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`.
- parameter callback: the closure to execute when changes occur - parameter callback: the closure to execute when changes occur
*/ */
@MainActor
public func addObserver<T: AnyObject>( public func addObserver<T: AnyObject>(
_ observer: T, _ observer: T,
notifyInitial: Bool = false, notifyInitial: Bool = false,
@@ -159,6 +161,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
- parameter observer: the object whose notifications will be unregistered - parameter observer: the object whose notifications will be unregistered
*/ */
@MainActor
public func removeObserver<T: AnyObject>(_ observer: T) { public func removeObserver<T: AnyObject>(_ observer: T) {
Internals.assert( Internals.assert(
@@ -185,7 +188,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
*/ */
public func refetch<B: FetchChainableBuilderType>( public func refetch<B: FetchChainableBuilderType>(
_ clauseChain: B, _ clauseChain: B,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) throws(any Swift.Error) where B.ObjectType == O { ) throws(any Swift.Error) where B.ObjectType == O {
try self.refetch( try self.refetch(
@@ -214,7 +217,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
*/ */
public func refetch<B: SectionMonitorBuilderType>( public func refetch<B: SectionMonitorBuilderType>(
_ clauseChain: B, _ clauseChain: B,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) throws(any Swift.Error) where B.ObjectType == O { ) throws(any Swift.Error) where B.ObjectType == O {
try self.refetch( try self.refetch(
@@ -342,7 +345,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
from: From<O>, from: From<O>,
sectionBy: SectionBy<O>?, sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void, applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) throws(any Swift.Error) { ) throws(any Swift.Error) {
let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController( let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController(
+35 -9
View File
@@ -23,9 +23,9 @@
// SOFTWARE. // SOFTWARE.
// //
#if canImport(Combine) && canImport(SwiftUI) #if canImport(Observation) && canImport(SwiftUI)
import Combine import Observation
import SwiftUI import SwiftUI
@@ -65,11 +65,12 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for - parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for
*/ */
@MainActor
public init( public init(
_ listPublisher: ListPublisher<Object> _ listPublisher: ListPublisher<Object>
) { ) {
self.observer = .init(listPublisher: listPublisher) self._observer = .init(wrappedValue: .init(listPublisher: listPublisher))
} }
/** /**
@@ -98,6 +99,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/ */
@MainActor
public init<B: FetchChainableBuilderType>( public init<B: FetchChainableBuilderType>(
_ clauseChain: B, _ clauseChain: B,
in dataStack: DataStack in dataStack: DataStack
@@ -139,6 +141,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/ */
@MainActor
public init<B: SectionMonitorBuilderType>( public init<B: SectionMonitorBuilderType>(
_ clauseChain: B, _ clauseChain: B,
in dataStack: DataStack in dataStack: DataStack
@@ -174,6 +177,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter from: a `From` clause indicating the entity type - 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
@MainActor
public init( public init(
_ from: From<Object>, _ from: From<Object>,
_ fetchClauses: FetchClause..., _ fetchClauses: FetchClause...,
@@ -212,6 +216,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter from: a `From` clause indicating the entity type - 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
@MainActor
public init( public init(
_ from: From<Object>, _ from: From<Object>,
_ fetchClauses: [FetchClause], _ fetchClauses: [FetchClause],
@@ -256,6 +261,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. - 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
@MainActor
public init( public init(
_ from: From<Object>, _ from: From<Object>,
_ sectionBy: SectionBy<Object>, _ sectionBy: SectionBy<Object>,
@@ -303,6 +309,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter sectionBy: a `SectionBy` clause indicating the keyPath for the attribute to use when sorting the list into sections. - 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
@MainActor
public init( public init(
_ from: From<Object>, _ from: From<Object>,
_ sectionBy: SectionBy<Object>, _ sectionBy: SectionBy<Object>,
@@ -316,11 +323,13 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
// MARK: @propertyWrapper // MARK: @propertyWrapper
@MainActor
public var wrappedValue: ListSnapshot<Object> { public var wrappedValue: ListSnapshot<Object> {
return self.observer.items return self.observer.items
} }
@MainActor
public var projectedValue: ListPublisher<Object> { public var projectedValue: ListPublisher<Object> {
return self.observer.listPublisher return self.observer.listPublisher
@@ -337,23 +346,40 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
// MARK: Private // MARK: Private
@ObservedObject @State
private var observer: Observer private var observer: Observer
// MARK: - Observer // MARK: - Observer
private final class Observer: ObservableObject { @MainActor
private final class Observer: Observation.Observable {
@Published private let registrar = ObservationRegistrar()
var items: ListSnapshot<Object> private var _items: ListSnapshot<Object>
let listPublisher: ListPublisher<Object> let listPublisher: ListPublisher<Object>
var items: ListSnapshot<Object> {
get {
self.registrar.access(self, keyPath: \.items)
return self._items
}
set {
self.registrar.withMutation(of: self, keyPath: \.items) {
self._items = newValue
}
}
}
init(listPublisher: ListPublisher<Object>) { init(listPublisher: ListPublisher<Object>) {
self.listPublisher = listPublisher self.listPublisher = listPublisher
self.items = listPublisher.snapshot self._items = listPublisher.snapshot
listPublisher.addObserver(self) { [weak self] (listPublisher) in listPublisher.addObserver(self) { [weak self] (listPublisher) in
@@ -365,7 +391,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
} }
} }
deinit { isolated deinit {
self.listPublisher.removeObserver(self) self.listPublisher.removeObserver(self)
} }
+1 -1
View File
@@ -31,7 +31,7 @@ import Foundation
/** /**
The `MigrationType` specifies the type of migration required for a store. 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 Indicates that the persistent store matches the latest model version and no migration is needed
+1 -1
View File
@@ -56,6 +56,6 @@ extension NSManagedObject {
private struct PropertyKeys { private struct PropertyKeys {
static var coreStoreObject: Void? static nonisolated(unsafe) var coreStoreObject: Void?
} }
} }
@@ -192,8 +192,8 @@ extension NSManagedObjectContext {
private struct PropertyKeys { private struct PropertyKeys {
static var observerForWillSaveNotification: Void? static nonisolated(unsafe) var observerForWillSaveNotification: Void?
static var shouldCascadeSavesToParent: Void? static nonisolated(unsafe) var shouldCascadeSavesToParent: Void?
} }
+114 -167
View File
@@ -292,30 +292,23 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource {
internal func fetchObjectIDs( internal func fetchObjectIDs(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID> _ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID>
) throws(CoreStoreError) -> [NSManagedObjectID] { ) throws(CoreStoreError) -> [NSManagedObjectID] {
var fetchResults: [NSManagedObjectID]? do {
var fetchError: Error?
self.performAndWait {
do { return try self.performAndWait {
fetchResults = try self.fetch(fetchRequest.dynamicCast()) return try self.fetch(fetchRequest.staticCast())
}
catch {
fetchError = error
} }
} }
if let fetchResults = fetchResults { catch {
return fetchResults let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing query request."
)
throw coreStoreError
} }
let coreStoreError = CoreStoreError(fetchError)
Internals.log(
coreStoreError,
"Failed executing fetch request."
)
throw coreStoreError
} }
@@ -442,30 +435,24 @@ extension NSManagedObjectContext {
internal func fetchOne<O: NSManagedObject>( internal func fetchOne<O: NSManagedObject>(
_ fetchRequest: Internals.CoreStoreFetchRequest<O> _ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> O? { ) throws(CoreStoreError) -> O? {
var fetchResults: [O]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
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 return fetchResults.first
} }
let coreStoreError = CoreStoreError(fetchError) catch {
Internals.log(
coreStoreError, let coreStoreError = CoreStoreError(error)
"Failed executing fetch request." Internals.log(
) coreStoreError,
throw coreStoreError "Failed executing fetch request."
)
throw coreStoreError
}
} }
@nonobjc @nonobjc
@@ -473,89 +460,74 @@ extension NSManagedObjectContext {
_ fetchRequest: Internals.CoreStoreFetchRequest<O> _ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> [O] { ) throws(CoreStoreError) -> [O] {
var fetchResults: [O]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
do { return try self.performAndWait {
fetchResults = try self.fetch(fetchRequest.staticCast()) return try self.fetch(fetchRequest.staticCast())
}
catch {
fetchError = error
} }
} }
if let fetchResults = fetchResults { catch {
return fetchResults 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 @nonobjc
internal func fetchCount( internal func fetchCount(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSNumber> _ fetchRequest: Internals.CoreStoreFetchRequest<NSNumber>
) throws(CoreStoreError) -> Int { ) throws(CoreStoreError) -> Int {
var count = 0 do {
var countError: (any Swift.Error)?
self.performAndWait {
do { let count = try self.performAndWait {
count = try self.count(for: fetchRequest.staticCast()) return try self.count(for: fetchRequest.staticCast())
} }
catch { guard count != NSNotFound else {
countError = error
}
}
if count == NSNotFound {
let coreStoreError = CoreStoreError(countError) throw CoreStoreError(nil)
}
return count
}
catch {
let coreStoreError = CoreStoreError(error)
Internals.log( Internals.log(
coreStoreError, coreStoreError,
"Failed executing count request." "Failed executing count request."
) )
throw coreStoreError throw coreStoreError
} }
return count
} }
@nonobjc @nonobjc
internal func fetchObjectID( internal func fetchObjectID(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID> _ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID>
) throws(CoreStoreError) -> NSManagedObjectID? { ) throws(CoreStoreError) -> NSManagedObjectID? {
var fetchResults: [NSManagedObjectID]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
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 return fetchResults.first
} }
let coreStoreError = CoreStoreError(fetchError) catch {
Internals.log(
coreStoreError, let coreStoreError = CoreStoreError(error)
"Failed executing fetch request." Internals.log(
) coreStoreError,
throw coreStoreError "Failed executing fetch request."
)
throw coreStoreError
}
} }
@@ -566,35 +538,29 @@ extension NSManagedObjectContext {
_ selectTerms: [SelectTerm<O>], _ selectTerms: [SelectTerm<O>],
fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary> fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> U? { ) throws(CoreStoreError) -> U? {
var fetchResults: [Any]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
do { let fetchResults = try self.performAndWait {
fetchResults = try self.fetch(fetchRequest.staticCast()) return try self.fetch(fetchRequest.staticCast())
} }
catch { if let rawResult = fetchResults.first,
fetchError = error
}
}
if let fetchResults = fetchResults {
if let rawResult = fetchResults.first as? NSDictionary,
let rawObject = rawResult[selectTerms.first!.keyPathString] as? U.QueryableNativeType { let rawObject = rawResult[selectTerms.first!.keyPathString] as? U.QueryableNativeType {
return Select<O, U>.ReturnType.cs_fromQueryableNativeType(rawObject) return Select<O, U>.ReturnType.cs_fromQueryableNativeType(rawObject)
} }
return nil return nil
} }
let coreStoreError = CoreStoreError(fetchError) catch {
Internals.log(
coreStoreError, let coreStoreError = CoreStoreError(error)
"Failed executing fetch request." Internals.log(
) coreStoreError,
throw coreStoreError "Failed executing query request."
)
throw coreStoreError
}
} }
@nonobjc @nonobjc
@@ -603,64 +569,52 @@ extension NSManagedObjectContext {
fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary> fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> Any? { ) throws(CoreStoreError) -> Any? {
var fetchResults: [Any]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
do { let fetchResults = try self.performAndWait {
fetchResults = try self.fetch(fetchRequest.staticCast()) return try self.fetch(fetchRequest.staticCast())
} }
catch { if let rawResult = fetchResults.first,
fetchError = error
}
}
if let fetchResults = fetchResults {
if let rawResult = fetchResults.first as? NSDictionary,
let rawObject = rawResult[selectTerms.first!.keyPathString] { let rawObject = rawResult[selectTerms.first!.keyPathString] {
return rawObject return rawObject
} }
return nil return nil
} }
let coreStoreError = CoreStoreError(fetchError) catch {
Internals.log(
coreStoreError, let coreStoreError = CoreStoreError(error)
"Failed executing fetch request." Internals.log(
) coreStoreError,
throw coreStoreError "Failed executing query request."
)
throw coreStoreError
}
} }
@nonobjc @nonobjc
internal func queryAttributes( internal func queryAttributes(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary> _ fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> [[String: Any]] { ) throws(CoreStoreError) -> [[String: Any]] {
var fetchResults: [Any]? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
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) return NSDictionary.cs_fromQueryResultsNativeType(fetchResults)
} }
let coreStoreError = CoreStoreError(fetchError) catch {
Internals.log(
coreStoreError, let coreStoreError = CoreStoreError(error)
"Failed executing fetch request." Internals.log(
) coreStoreError,
throw coreStoreError "Failed executing query request."
)
throw coreStoreError
}
} }
@@ -671,36 +625,29 @@ extension NSManagedObjectContext {
_ fetchRequest: Internals.CoreStoreFetchRequest<O> _ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> Int { ) throws(CoreStoreError) -> Int {
var numberOfDeletedObjects: Int? do {
var fetchError: (any Swift.Error)?
self.performAndWait {
Internals.autoreleasepool { return try self.performAndWait {
do { return try Internals.autoreleasepool {
let fetchResults = try self.fetch(fetchRequest.staticCast()) let fetchResults = try self.fetch(fetchRequest.staticCast())
for object in fetchResults { for object in fetchResults {
self.delete(object) self.delete(object)
} }
numberOfDeletedObjects = fetchResults.count return fetchResults.count
}
catch {
fetchError = error
} }
} }
} }
if let numberOfDeletedObjects = numberOfDeletedObjects { catch {
return numberOfDeletedObjects 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
} }
} }
+21 -13
View File
@@ -65,7 +65,7 @@ extension NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = coordinator context.persistentStoreCoordinator = coordinator
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
context.undoManager = nil context.undoManager = nil
context.setupForCoreStoreWithContextName("com.corestore.rootcontext") context.setupForCoreStoreWithContextName("com.corestore.rootcontext")
@@ -76,16 +76,21 @@ extension NSManagedObjectContext {
object: coordinator, object: coordinator,
closure: { [weak context] (note) -> Void in 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<NSManagedObjectID>) { if let updatedObjectIDs = (note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObjectID>) {
for objectID in updatedObjectIDs { 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) let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = rootContext context.parent = rootContext
context.mergePolicy = NSRollbackMergePolicy context.mergePolicy = NSMergePolicy(merge: .rollbackMergePolicyType)
context.undoManager = nil context.undoManager = nil
context.setupForCoreStoreWithContextName("com.corestore.maincontext") context.setupForCoreStoreWithContextName("com.corestore.maincontext")
context.observerForDidSaveNotification = Internals.NotificationObserver( context.observerForDidSaveNotification = Internals.NotificationObserver(
@@ -108,16 +113,19 @@ extension NSManagedObjectContext {
object: rootContext, object: rootContext,
closure: { [weak context] (note) -> Void in closure: { [weak context] (note) -> Void in
guard let rootContext = note.object as? NSManagedObjectContext, guard
let context = context else { let rootContext = note.object as? NSManagedObjectContext,
let context = context
return else {
return
} }
let saveMetadata = rootContext.saveMetadata let saveMetadata = rootContext.saveMetadata
context.saveMetadata = 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<NSManagedObject>) { if let updatedObjects = (note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>) {
@@ -147,9 +155,9 @@ extension NSManagedObjectContext {
private struct PropertyKeys { private struct PropertyKeys {
static var parentStack: Void? static nonisolated(unsafe) var parentStack: Void?
static var observerForDidSaveNotification: Void? static nonisolated(unsafe) var observerForDidSaveNotification: Void?
static var observerForDidImportUbiquitousContentChangesNotification: Void? static nonisolated(unsafe) var observerForDidImportUbiquitousContentChangesNotification: Void?
} }
@nonobjc @nonobjc
@@ -142,15 +142,14 @@ extension NSManagedObjectContext {
@nonobjc @nonobjc
internal func saveSynchronously( internal func saveSynchronously(
waitForMerge: Bool, waitForMerge: Bool,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) -> (hasChanges: Bool, error: CoreStoreError?) { ) -> (hasChanges: Bool, error: CoreStoreError?) {
var result: (hasChanges: Bool, error: CoreStoreError?) = (false, nil) return self.performAndWait {
self.performAndWait {
guard self.hasChanges else { guard self.hasChanges else {
return return (false, nil)
} }
do { do {
@@ -168,8 +167,7 @@ extension NSManagedObjectContext {
saveError, saveError,
"Failed to save \(Internals.typeName(NSManagedObjectContext.self))." "Failed to save \(Internals.typeName(NSManagedObjectContext.self))."
) )
result = (true, saveError) return (true, saveError)
return
} }
if let parentContext = self.parent, self.shouldCascadeSavesToParent { if let parentContext = self.parent, self.shouldCascadeSavesToParent {
@@ -177,20 +175,19 @@ extension NSManagedObjectContext {
waitForMerge: waitForMerge, waitForMerge: waitForMerge,
sourceIdentifier: sourceIdentifier sourceIdentifier: sourceIdentifier
) )
result = (true, error) return (true, error)
} }
else { else {
result = (true, nil) return (true, nil)
} }
} }
return result
} }
@nonobjc @nonobjc
internal func saveAsynchronously( internal func saveAsynchronously(
sourceIdentifier: Any?, sourceIdentifier: (any Sendable)?,
completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in } completion: @escaping @MainActor (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in }
) { ) {
self.perform { self.perform {
@@ -259,11 +256,11 @@ extension NSManagedObjectContext {
// MARK: Internal // MARK: Internal
internal let isSavingSynchronously: Bool internal let isSavingSynchronously: Bool
internal let sourceIdentifier: Any? internal let sourceIdentifier: (any Sendable)?
internal init( internal init(
isSavingSynchronously: Bool, isSavingSynchronously: Bool,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
self.isSavingSynchronously = isSavingSynchronously self.isSavingSynchronously = isSavingSynchronously
@@ -276,9 +273,9 @@ extension NSManagedObjectContext {
private struct PropertyKeys { private struct PropertyKeys {
static var parentTransaction: Void? static nonisolated(unsafe) var parentTransaction: Void?
static var saveMetadata: Void? static nonisolated(unsafe) var saveMetadata: Void?
static var isTransactionContext: Void? static nonisolated(unsafe) var isTransactionContext: Void?
static var isDataStackContext: Void? static nonisolated(unsafe) var isDataStackContext: Void?
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ extension NSPersistentStore {
private struct PropertyKeys { private struct PropertyKeys {
static var storageInterface: Void? static nonisolated(unsafe) var storageInterface: Void?
} }
} }
@@ -33,7 +33,7 @@ extension NSPersistentStoreCoordinator {
@nonobjc @nonobjc
internal func performAsynchronously( internal func performAsynchronously(
_ closure: @escaping () -> Void _ closure: @escaping @Sendable () -> Void
) { ) {
self.perform(closure) self.perform(closure)
@@ -41,40 +41,28 @@ extension NSPersistentStoreCoordinator {
@nonobjc @nonobjc
internal func performSynchronously<T>( internal func performSynchronously<T>(
_ closure: @escaping () -> T _ closure: @Sendable () -> T
) -> T { ) -> T {
var result: T? return self.performAndWait(closure)
self.performAndWait {
result = closure()
}
return result!
} }
@nonobjc @nonobjc
internal func performSynchronously<T>( internal func performSynchronously<T>(
_ closure: @escaping () throws(any Swift.Error) -> T _ closure: @Sendable () throws(any Swift.Error) -> T
) throws(CoreStoreError) -> T { ) throws(CoreStoreError) -> T {
var closureError: (any Swift.Error)? do {
var result: T?
self.performAndWait {
do { return try self.performAndWait {
result = try closure() return try closure()
}
catch {
closureError = error
} }
} }
if let closureError = closureError { catch {
throw CoreStoreError(closureError) throw CoreStoreError(error)
} }
return result!
} }
@nonobjc @nonobjc
@@ -85,6 +73,7 @@ extension NSPersistentStoreCoordinator {
options: [AnyHashable: Any]? options: [AnyHashable: Any]?
) throws(CoreStoreError) -> NSPersistentStore { ) throws(CoreStoreError) -> NSPersistentStore {
nonisolated(unsafe) let options = options
return try self.performSynchronously { return try self.performSynchronously {
return try self.addPersistentStore( return try self.addPersistentStore(
+8 -8
View File
@@ -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. 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<O: DynamicObject>: Hashable, ObjectRepresentation { public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentation, @unchecked Sendable {
/** /**
Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted. Returns the `DynamicObject` instance being observed, or `nil` if the object was already deleted.
@@ -71,7 +71,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
- parameter observer: an `ObjectObserver` to send change notifications to - parameter observer: an `ObjectObserver` to send change notifications to
*/ */
public func addObserver<U: ObjectObserver>(_ observer: U) where U.ObjectEntityType == O { public func addObserver<U: ObjectObserver & Sendable>(_ observer: U) where U.ObjectEntityType == O {
self.unregisterObserver(observer) self.unregisterObserver(observer)
self.registerObserver( self.registerObserver(
@@ -243,19 +243,19 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:] self.lastCommittedAttributes = (self.object?.cs_toRaw().committedValues(forKeys: nil) as? [String: NSObject]) ?? [:]
} }
internal func registerObserver<U: AnyObject>( internal func registerObserver<U: AnyObject & Sendable>(
_ observer: U, _ observer: U,
willChangeObject: @escaping ( willChangeObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ObjectMonitor<O>, _ monitor: ObjectMonitor<O>,
_ object: O _ object: O
) -> Void, ) -> Void,
didDeleteObject: @escaping ( didDeleteObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ObjectMonitor<O>, _ monitor: ObjectMonitor<O>,
_ object: O _ object: O
) -> Void, ) -> Void,
didUpdateObject: @escaping ( didUpdateObject: @escaping @Sendable (
_ observer: U, _ observer: U,
_ monitor: ObjectMonitor<O>, _ monitor: ObjectMonitor<O>,
_ object: O, _ object: O,
@@ -361,7 +361,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
_ notificationKey: UnsafeRawPointer, _ notificationKey: UnsafeRawPointer,
name: Notification.Name, name: Notification.Name,
toObserver observer: AnyObject, toObserver observer: AnyObject,
callback: @escaping (_ monitor: ObjectMonitor<O>) -> Void callback: @escaping @Sendable (_ monitor: ObjectMonitor<O>) -> Void
) { ) {
Internals.setAssociatedRetainedObject( Internals.setAssociatedRetainedObject(
@@ -386,7 +386,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
_ notificationKey: UnsafeRawPointer, _ notificationKey: UnsafeRawPointer,
name: Notification.Name, name: Notification.Name,
toObserver observer: AnyObject, toObserver observer: AnyObject,
callback: @escaping (_ monitor: ObjectMonitor<O>, _ object: O) -> Void callback: @escaping @Sendable (_ monitor: ObjectMonitor<O>, _ object: O) -> Void
) { ) {
Internals.setAssociatedRetainedObject( Internals.setAssociatedRetainedObject(
+2 -2
View File
@@ -36,12 +36,12 @@ import CoreData
monitor.addObserver(self) monitor.addObserver(self)
``` ```
*/ */
public protocol ObjectObserver: AnyObject { public protocol ObjectObserver: AnyObject & Sendable {
/** /**
The `DynamicObject` type for the observed object 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) Handles processing just before a change to the observed `object` occurs. (Optional)
+19 -22
View File
@@ -90,7 +90,8 @@ extension ObjectPublisher {
// MARK: - ObjectSnapshotSubscription // MARK: - ObjectSnapshotSubscription
fileprivate final class ObjectSnapshotSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == Never { fileprivate final class ObjectSnapshotSubscription<S: Subscriber>: Subscription, @unchecked Sendable
where S.Input == Output, S.Failure == Never {
// MARK: FilePrivate // MARK: FilePrivate
@@ -114,21 +115,24 @@ extension ObjectPublisher {
return return
} }
self.publisher.addObserver( Internals.mainActorImmediate { [self] in
self,
notifyInitial: self.emitInitialValue, self.publisher.addObserver(
{ [weak self] (publisher) in self,
notifyInitial: self.emitInitialValue,
guard { [weak self] (publisher) in
let self = self,
let subscriber = self.subscriber
else {
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 self.subscriber = nil
if Thread.isMainThread { Internals.mainActorImmediate {
self.publisher.removeObserver(self) self.publisher.removeObserver(self)
} }
else {
DispatchQueue.main.async {
self.publisher.removeObserver(self)
}
}
} }
+3
View File
@@ -76,6 +76,7 @@ public final class ObjectPublisher<O: DynamicObject>: 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 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 - parameter callback: the closure to execute when changes occur
*/ */
@MainActor
public func addObserver<T: AnyObject>( public func addObserver<T: AnyObject>(
_ observer: T, _ observer: T,
notifyInitial: Bool = false, notifyInitial: Bool = false,
@@ -112,6 +113,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
- parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`. - parameter initialSourceIdentifier: an optional value that identifies the initial callback invocation if `notifyInitial` is `true`.
- parameter callback: the closure to execute when changes occur - parameter callback: the closure to execute when changes occur
*/ */
@MainActor
public func addObserver<T: AnyObject>( public func addObserver<T: AnyObject>(
_ observer: T, _ observer: T,
notifyInitial: Bool = false, notifyInitial: Bool = false,
@@ -145,6 +147,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
- parameter observer: the object whose notifications will be unregistered - parameter observer: the object whose notifications will be unregistered
*/ */
@MainActor
public func removeObserver<T: AnyObject>(_ observer: T) { public func removeObserver<T: AnyObject>(_ observer: T) {
Internals.assert( Internals.assert(
+30 -10
View File
@@ -23,9 +23,9 @@
// SOFTWARE. // SOFTWARE.
// //
#if canImport(Combine) && canImport(SwiftUI) #if canImport(Observation) && canImport(SwiftUI)
import Combine import Observation
import SwiftUI import SwiftUI
@@ -62,19 +62,22 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
- parameter objectPublisher: The `ObjectPublisher` that the `ObjectState` will observe changes for - parameter objectPublisher: The `ObjectPublisher` that the `ObjectState` will observe changes for
*/ */
@MainActor
public init(_ objectPublisher: ObjectPublisher<O>?) { public init(_ objectPublisher: ObjectPublisher<O>?) {
self.observer = .init(objectPublisher: objectPublisher) self._observer = .init(wrappedValue: .init(objectPublisher: objectPublisher))
} }
// MARK: @propertyWrapper // MARK: @propertyWrapper
@MainActor
public var wrappedValue: ObjectSnapshot<O>? { public var wrappedValue: ObjectSnapshot<O>? {
return self.observer.item return self.observer.item
} }
@MainActor
public var projectedValue: ObjectPublisher<O>? { public var projectedValue: ObjectPublisher<O>? {
return self.observer.objectPublisher return self.observer.objectPublisher
@@ -91,19 +94,36 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
// MARK: Private // MARK: Private
@ObservedObject @State
private var observer: Observer private var observer: Observer
// MARK: - Observer // MARK: - Observer
private final class Observer: ObservableObject { @MainActor
private final class Observer: Observation.Observable {
@Published private let registrar = ObservationRegistrar()
var item: ObjectSnapshot<O>? private var _item: ObjectSnapshot<O>?
let objectPublisher: ObjectPublisher<O>? let objectPublisher: ObjectPublisher<O>?
var item: ObjectSnapshot<O>? {
get {
self.registrar.access(self, keyPath: \.item)
return self._item
}
set {
self.registrar.withMutation(of: self, keyPath: \.item) {
self._item = newValue
}
}
}
init(objectPublisher: ObjectPublisher<O>?) { init(objectPublisher: ObjectPublisher<O>?) {
guard guard
@@ -112,12 +132,12 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
else { else {
self.objectPublisher = nil self.objectPublisher = nil
self.item = nil self._item = nil
return return
} }
self.objectPublisher = objectPublisher self.objectPublisher = objectPublisher
self.item = objectPublisher.snapshot self._item = objectPublisher.snapshot
objectPublisher.addObserver(self) { [weak self] (objectPublisher) in objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
@@ -129,7 +149,7 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
} }
} }
deinit { isolated deinit {
self.objectPublisher?.removeObserver(self) self.objectPublisher?.removeObserver(self)
} }
+11 -5
View File
@@ -36,7 +36,8 @@ extension Progress {
- parameter closure: the closure to execute on progress change - parameter closure: the closure to execute on progress change
*/ */
@nonobjc @nonobjc
public func setProgressHandler(_ closure: ((_ progress: Progress) -> Void)?) { @MainActor
public func setProgressHandler(_ closure: (@MainActor (_ progress: Progress) -> Void)?) {
self.progressObserver.progressHandler = closure self.progressObserver.progressHandler = closure
} }
@@ -46,15 +47,19 @@ extension Progress {
private struct PropertyKeys { private struct PropertyKeys {
static var progressObserver: Void? static nonisolated(unsafe) var progressObserver: Void?
} }
@nonobjc @nonobjc
@MainActor
private var progressObserver: ProgressObserver { private var progressObserver: ProgressObserver {
get { get {
let object: ProgressObserver? = Internals.getAssociatedObjectForKey(&PropertyKeys.progressObserver, inObject: self) let object: ProgressObserver? = Internals.getAssociatedObjectForKey(
&PropertyKeys.progressObserver,
inObject: self
)
if let observer = object { if let observer = object {
return observer return observer
@@ -76,11 +81,12 @@ extension Progress {
// MARK: - ProgressObserver // MARK: - ProgressObserver
@objc @objc
private final class ProgressObserver: NSObject { private final class ProgressObserver: NSObject, @unchecked Sendable {
private unowned let progress: Progress private unowned let progress: Progress
fileprivate var progressHandler: ((_ progress: Progress) -> Void)? { @MainActor
fileprivate var progressHandler: (@MainActor (_ progress: Progress) -> Void)? {
didSet { didSet {
+8 -5
View File
@@ -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. - 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. 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"]] [NSSQLitePragmasOption: ["journal_mode": "WAL"]]
``` ```
*/ */
public let storeOptions: [AnyHashable: Any]? = [ public var storeOptions: [AnyHashable: Any]? {
NSSQLitePragmasOption: ["journal_mode": "WAL"],
NSBinaryStoreInsecureDecodingCompatibilityOption: true return [
] NSSQLitePragmasOption: ["journal_mode": "WAL"],
NSBinaryStoreInsecureDecodingCompatibilityOption: true
]
}
/** /**
Do not call directly. Used by the `DataStack` internally. Do not call directly. Used by the `DataStack` internally.
+2 -2
View File
@@ -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. 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. 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. 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 Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch
+1 -1
View File
@@ -164,7 +164,7 @@ public final class SynchronousDataTransaction: BaseDataTransaction {
internal init( internal init(
mainContext: NSManagedObjectContext, mainContext: NSManagedObjectContext,
queue: DispatchQueue, queue: DispatchQueue,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
super.init( super.init(
@@ -119,7 +119,7 @@ extension UnsafeDataTransaction {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorList<O>( public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ fetchClauses: FetchClause... _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorList<O>( public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ fetchClauses: [FetchClause] _ fetchClauses: [FetchClause]
) { ) {
@@ -174,7 +174,7 @@ extension UnsafeDataTransaction {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses - parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/ */
public func monitorList<B: FetchChainableBuilderType>( public func monitorList<B: FetchChainableBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorSectionedList<O>( public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ sectionBy: SectionBy<O>, _ sectionBy: SectionBy<O>,
_ fetchClauses: FetchClause... _ 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. - parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/ */
public func monitorSectionedList<O>( public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>, _ from: From<O>,
_ sectionBy: SectionBy<O>, _ sectionBy: SectionBy<O>,
_ fetchClauses: [FetchClause] _ fetchClauses: [FetchClause]
@@ -323,7 +323,7 @@ extension UnsafeDataTransaction {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses - parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/ */
public func monitorSectionedList<B: SectionMonitorBuilderType>( public func monitorSectionedList<B: SectionMonitorBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void, createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B _ clauseChain: B
) { ) {
+4 -4
View File
@@ -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. 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: - // 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. - 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( public func commit(
_ completion: @escaping (_ error: CoreStoreError?) -> Void _ completion: @escaping @Sendable (_ error: CoreStoreError?) -> Void
) { ) {
self.context.saveAsynchronously( self.context.saveAsynchronously(
@@ -142,7 +142,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
*/ */
public func beginUnsafe( public func beginUnsafe(
supportsUndo: Bool = false, supportsUndo: Bool = false,
sourceIdentifier: Any? = nil sourceIdentifier: (any Sendable)? = nil
) -> UnsafeDataTransaction { ) -> UnsafeDataTransaction {
return UnsafeDataTransaction( return UnsafeDataTransaction(
@@ -160,7 +160,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
mainContext: NSManagedObjectContext, mainContext: NSManagedObjectContext,
queue: DispatchQueue, queue: DispatchQueue,
supportsUndo: Bool, supportsUndo: Bool,
sourceIdentifier: Any? sourceIdentifier: (any Sendable)?
) { ) {
super.init( super.init(