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_VARIABLE = YES;
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MACOSX_DEPLOYMENT_TARGET = 13.5;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "-D DEBUG";
@@ -3027,12 +3027,12 @@
SWIFT_COMPILATION_MODE = singlefile;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.6;
TVOS_DEPLOYMENT_TARGET = 17.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
WATCHOS_DEPLOYMENT_TARGET = 9.6;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Debug;
};
@@ -3083,8 +3083,8 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = Sources/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MACOSX_DEPLOYMENT_TARGET = 13.5;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = com.johnestropia.CoreStore;
PRODUCT_NAME = CoreStore;
@@ -3092,13 +3092,13 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_SWIFT3_OBJC_INFERENCE = Off;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.6;
TVOS_DEPLOYMENT_TARGET = 17.0;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
WATCHOS_DEPLOYMENT_TARGET = 9.6;
WATCHOS_DEPLOYMENT_TARGET = 10.0;
};
name = Release;
};
@@ -3366,7 +3366,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 9.3.0;
OTHER_LDFLAGS = (
"-weak_framework",
@@ -3401,7 +3401,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 9.3.0;
OTHER_LDFLAGS = (
"-weak_framework",
+19 -7
View File
@@ -24,6 +24,7 @@
//
import XCTest
import os
@testable
import CoreStore
@@ -47,7 +48,11 @@ class BaseTestCase: XCTestCase {
// MARK: Internal
@nonobjc
func prepareStack(configurations: [ModelConfiguration] = [nil], _ closure: (_ dataStack: DataStack) throws -> Void) {
@MainActor
func prepareStack(
configurations: [ModelConfiguration] = [nil],
_ closure: (_ dataStack: DataStack) throws -> Void
) {
let stack = DataStack(
xcodeModelName: "Model",
@@ -79,6 +84,7 @@ class BaseTestCase: XCTestCase {
}
@nonobjc
@MainActor
func expectLogger<T>(_ expectations: [TestLogger.Expectation], closure: () throws -> T) rethrows -> T {
CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations(expectations))
@@ -97,6 +103,7 @@ class BaseTestCase: XCTestCase {
}
@nonobjc
@MainActor
func expectError<T>(code: CoreStoreErrorCode, closure: () throws -> T) {
CoreStoreDefaults.logger = TestLogger(self.prepareLoggerExpectations([.logError]))
@@ -135,12 +142,14 @@ class BaseTestCase: XCTestCase {
}
@nonobjc
@MainActor
func checkExpectationsImmediately() {
self.waitForExpectations(timeout: 0, handler: { _ in })
}
@nonobjc
@MainActor
func waitAndCheckExpectations() {
self.waitForExpectations(timeout: 10, handler: {_ in })
@@ -174,7 +183,7 @@ class BaseTestCase: XCTestCase {
// MARK: - TestLogger
class TestLogger: CoreStoreLogger {
final class TestLogger: CoreStoreLogger {
enum Expectation {
@@ -187,13 +196,13 @@ class TestLogger: CoreStoreLogger {
init(_ expectations: [Expectation: XCTestExpectation]) {
self.expectations = expectations
self.expectations = .init(initialState: expectations)
}
// MARK: CoreStoreLogger
var enableObjectConcurrencyDebugging: Bool = true
let enableObjectConcurrencyDebugging: Bool = true
func log(level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
@@ -227,14 +236,16 @@ class TestLogger: CoreStoreLogger {
// MARK: Private
private var expectations: [Expectation: XCTestExpectation]
private let expectations: OSAllocatedUnfairLock<[Expectation: XCTestExpectation]>
private func fulfill(_ expectation: Expectation) {
if let instance = self.expectations[expectation] {
self.expectations.withLock {
if let instance = $0[expectation] {
instance.fulfill()
self.expectations[expectation] = nil
$0[expectation] = nil
}
else {
@@ -242,3 +253,4 @@ class TestLogger: CoreStoreLogger {
}
}
}
}
+2
View File
@@ -35,6 +35,7 @@ import CoreStore
class ConvenienceTests: BaseTestCase {
@objc
@MainActor
dynamic func test_ThatDataStacks_CanCreateFetchedResultsControllers() {
self.prepareStack { (stack) in
@@ -62,6 +63,7 @@ class ConvenienceTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatUnsafeDataTransactions_CanCreateFetchedResultsControllers() {
self.prepareStack { (stack) in
+35 -1
View File
@@ -162,6 +162,18 @@ class Person: CoreStoreObject {
@Field.Relationship("_spouseInverse", inverse: \.$spouse)
private var spouseInverse: Person?
@Field.Coded("ownerInfosJson", coder: FieldCoders.Json.self)
var ownerInfosJson: [OwnerInfo] = []
@Field.Coded("ownerInfosPlist", coder: FieldCoders.Plist.self)
var ownerInfosPlist: [OwnerInfo] = []
struct OwnerInfo: Codable, Equatable {
var id: UUID
var displayName: String?
var otherIDs: Set<UUID>
}
private static func getDisplayName(_ object: ObjectProxy<Person>, _ field: ObjectProxy<Person>.FieldProxy<String?>) -> String? {
if let value = field.primitiveValue {
@@ -203,7 +215,7 @@ class DynamicModelTests: BaseTestDataTestCase {
versionLock: [
"Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
"Dog": [0xad6de93adc5565d, 0x7897e51253eba5a3, 0xd12b9ce0b13600f3, 0x5a4827cd794cd15e],
"Person": [0xf3e6ba6016bbedc6, 0x50dedf64f0eba490, 0xa32088a0ee83468d, 0xb72d1d0b37bd0992]
"Person": [0xbaec6549a025b59f, 0x3415c71e9f46fcf3, 0xb86b33433cb335eb, 0xfe441fde120dab67]
]
)
)
@@ -223,6 +235,14 @@ class DynamicModelTests: BaseTestDataTestCase {
let willSetPriorObserverDone = self.expectation(description: "willSet-observe-prior-done")
let willSetNotPriorObserverDone = self.expectation(description: "willSet-observe-notPrior-done")
let didSetObserverDone = self.expectation(description: "didSet-observe-done")
let ownerInfos: [Person.OwnerInfo] = (1...3).map {
.init(
id: .init(),
displayName: "ownerInfo_\($0)",
otherIDs: Set((1...3).map({ _ in .init() }))
)
}
stack.perform(
asynchronous: { (transaction) in
@@ -373,11 +393,19 @@ class DynamicModelTests: BaseTestDataTestCase {
person.job = .engineer
XCTAssertEqual(person.job, .engineer)
person.ownerInfosJson = ownerInfos
XCTAssertEqual(person.ownerInfosJson, ownerInfos)
person.ownerInfosPlist = ownerInfos
XCTAssertEqual(person.ownerInfosPlist, ownerInfos)
let personSnapshot2 = person.asSnapshot(in: transaction)!
XCTAssertEqual(person.name, personSnapshot2.$name)
XCTAssertEqual(person.title, personSnapshot2.$title)
XCTAssertEqual(person.displayName, personSnapshot2.$displayName)
XCTAssertEqual(person.job, personSnapshot2.$job)
XCTAssertEqual(person.ownerInfosJson, personSnapshot2.$ownerInfosJson)
XCTAssertEqual(person.ownerInfosPlist, personSnapshot2.$ownerInfosPlist)
var personSnapshot3 = personSnapshot2
personSnapshot3.$name = "James"
@@ -390,6 +418,8 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertEqual(personSnapshot3.$name, "James")
XCTAssertEqual(personSnapshot3.$displayName, "Sir John")
XCTAssertEqual(personSnapshot3.$job, .engineer)
XCTAssertEqual(personSnapshot3.$ownerInfosJson, ownerInfos)
XCTAssertEqual(personSnapshot3.$ownerInfosPlist, ownerInfos)
@@ -409,6 +439,8 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertEqual(personPublisher.$name, "John")
XCTAssertEqual(personPublisher.$displayName, "Sir John")
XCTAssertEqual(personPublisher.$job, .engineer)
XCTAssertEqual(personPublisher.$ownerInfosJson, ownerInfos)
XCTAssertEqual(personPublisher.$ownerInfosPlist, ownerInfos)
updateDone.fulfill()
},
@@ -444,6 +476,8 @@ class DynamicModelTests: BaseTestDataTestCase {
XCTAssertEqual(person!.customField.string, "customString")
XCTAssertEqual(person!.job, .engineer)
XCTAssertEqual(person!.pets.first, dog)
XCTAssertEqual(person!.ownerInfosJson, ownerInfos)
XCTAssertEqual(person!.ownerInfosPlist, ownerInfos)
let p3 = Where<Dog>({ $0.$age == 10 })
XCTAssertEqual(p3.predicate, NSPredicate(format: "%K == %d", "age", 10))
+1
View File
@@ -63,6 +63,7 @@ final class GroupByTests: BaseTestCase {
}
@objc
@MainActor
dynamic func test_ThatGroupByClauses_ApplyToFetchRequestsCorrectly() {
self.prepareStack { (dataStack) in
+9 -5
View File
@@ -36,6 +36,7 @@ import CoreStore
class ListPublisherTests: BaseTestDataTestCase {
@objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveInsertNotifications() {
self.prepareStack { (stack) in
@@ -62,7 +63,7 @@ class ListPublisherTests: BaseTestDataTestCase {
let saveExpectation = self.expectation(description: "save")
stack.perform(
asynchronous: { (transaction) -> Bool in
asynchronous: { [dateFormatter] (transaction) -> Bool in
let object = transaction.create(Into<TestEntity1>())
object.testBoolean = NSNumber(value: true)
@@ -70,7 +71,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "1")
object.testString = "nil:TestEntity1:1"
object.testData = ("nil:TestEntity1:1" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-01T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-01T00:00:00Z")!
return transaction.hasChanges
},
@@ -92,6 +93,7 @@ class ListPublisherTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in
@@ -126,7 +128,7 @@ class ListPublisherTests: BaseTestDataTestCase {
let saveExpectation = self.expectation(description: "save")
stack.perform(
asynchronous: { (transaction) -> Bool in
asynchronous: { [dateFormatter] (transaction) -> Bool in
if let object = try transaction.fetchOne(
From<TestEntity1>(),
@@ -136,7 +138,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "11")
object.testString = "nil:TestEntity1:11"
object.testData = ("nil:TestEntity1:11" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-11T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-11T00:00:00Z")!
}
else {
@@ -150,7 +152,7 @@ class ListPublisherTests: BaseTestDataTestCase {
object.testDecimal = NSDecimalNumber(string: "22")
object.testString = "nil:TestEntity1:22"
object.testData = ("nil:TestEntity1:22" as NSString).data(using: String.Encoding.utf8.rawValue)!
object.testDate = self.dateFormatter.date(from: "2000-01-22T00:00:00Z")!
object.testDate = dateFormatter.date(from: "2000-01-22T00:00:00Z")!
}
else {
@@ -176,6 +178,7 @@ class ListPublisherTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveMoveNotifications() {
self.prepareStack { (stack) in
@@ -242,6 +245,7 @@ class ListPublisherTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatListPublishers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in
+18 -3
View File
@@ -24,6 +24,7 @@
//
import XCTest
import os
@testable
import CoreStore
@@ -34,6 +35,7 @@ import CoreStore
class ObjectObserverTests: BaseTestDataTestCase {
@objc
@MainActor
dynamic func test_ThatObjectObservers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in
@@ -54,13 +56,16 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted)
var events = 0
let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:willUpdateObject:"),
object: observer,
handler: { (note) -> Bool in
nonisolated(unsafe) let note = note
return events.withLock { events in
XCTAssertEqual(events, 0)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
@@ -72,12 +77,16 @@ class ObjectObserverTests: BaseTestDataTestCase {
}
return events == 0
}
}
)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didUpdateObject:changedPersistentKeys:"),
object: observer,
handler: { (note) -> Bool in
nonisolated(unsafe) let note = note
return events.withLock { events in
XCTAssertEqual(events, 1)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
@@ -101,6 +110,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
}
return events == 1
}
}
)
let saveExpectation = self.expectation(description: "save")
stack.perform(
@@ -131,6 +141,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatObjectObservers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in
@@ -151,13 +162,16 @@ class ObjectObserverTests: BaseTestDataTestCase {
XCTAssertEqual(monitor.object, object)
XCTAssertFalse(monitor.isObjectDeleted)
var events = 0
let events: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
_ = self.expectation(
forNotification: NSNotification.Name(rawValue: "objectMonitor:didDeleteObject:"),
object: observer,
handler: { (note) -> Bool in
nonisolated(unsafe) let note = note
return events.withLock { events in
XCTAssertEqual(events, 0)
XCTAssertEqual(
((note.userInfo as NSDictionary?) ?? [:]),
@@ -169,6 +183,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
}
return events == 0
}
}
)
let saveExpectation = self.expectation(description: "save")
stack.perform(
@@ -202,7 +217,7 @@ class ObjectObserverTests: BaseTestDataTestCase {
// MARK: TestObjectObserver
class TestObjectObserver: ObjectObserver {
final class TestObjectObserver: ObjectObserver {
typealias ObjectEntityType = TestEntity1
@@ -34,6 +34,7 @@ import CoreStore
class ObjectPublisherTests: BaseTestDataTestCase {
@objc
@MainActor
dynamic func test_ThatObjectPublishers_CanReceiveUpdateNotifications() {
self.prepareStack { (stack) in
@@ -93,6 +94,7 @@ class ObjectPublisherTests: BaseTestDataTestCase {
}
@objc
@MainActor
dynamic func test_ThatObjectPublishers_CanReceiveDeleteNotifications() {
self.prepareStack { (stack) in
+1
View File
@@ -35,6 +35,7 @@ import CoreStore
class SetupTests: BaseTestDataTestCase {
@objc
@MainActor
dynamic func test_ThatDataStacks_ConfigureCorrectly() {
do {
@@ -74,7 +74,7 @@ extension Modern {
// MARK: - TransactionSource
enum TransactionSource {
enum TransactionSource: Sendable {
case add
case delete
+1 -1
View File
@@ -29,7 +29,7 @@ import PackageDescription
let package = Package(
name: "CoreStore",
platforms: [
.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9)
.macOS(.v14), .iOS(.v17), .tvOS(.v17), .watchOS(.v10)
],
products: [
.library(name: "CoreStore", targets: ["CoreStore"])
+5 -3
View File
@@ -175,7 +175,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
internal init(
mainContext: NSManagedObjectContext,
queue: DispatchQueue,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
super.init(
@@ -188,7 +188,7 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
}
internal func autoCommit(
_ completion: @escaping (
_ completion: @escaping @MainActor (
_ hasChanges: Bool,
_ error: CoreStoreError?
) -> Void
@@ -197,12 +197,14 @@ public final class AsynchronousDataTransaction: BaseDataTransaction {
self.isCommitted = true
let group = DispatchGroup()
group.enter()
nonisolated(unsafe) let transaction = self
self.context.saveAsynchronously(
sourceIdentifier: self.sourceIdentifier,
completion: { (hasChanges, error) -> Void in
completion(hasChanges, error)
self.result = (hasChanges, error)
transaction.result = (hasChanges, error)
group.leave()
}
)
+5 -2
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.
*/
public let sourceIdentifier: Any?
public let sourceIdentifier: (any Sendable)?
/**
Allow external libraries to store custom data in the transaction. App code should rarely have a need for this.
@@ -471,7 +471,7 @@ public /*abstract*/ class BaseDataTransaction {
queue: DispatchQueue,
supportsUndo: Bool,
bypassesQueueing: Bool,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
let context = mainContext.temporaryContextInTransactionWithConcurrencyType(
@@ -493,9 +493,12 @@ public /*abstract*/ class BaseDataTransaction {
}
else if context.undoManager == nil {
Internals.mainActorImmediate {
context.undoManager = UndoManager()
}
}
}
internal func isRunningInAllowedQueue() -> Bool {
+34 -10
View File
@@ -24,6 +24,7 @@
//
import Foundation
import os
// MARK: - CoreStoreDefaults
@@ -36,7 +37,29 @@ public enum CoreStoreDefaults {
/**
The `CoreStoreLogger` instance to be used. The default logger is an instance of a `DefaultLogger`.
*/
public static var logger: CoreStoreLogger = DefaultLogger()
public static var logger: CoreStoreLogger {
get {
return self.loggerInstance.withLock {
if let logger = $0 {
return logger
}
let logger = DefaultLogger()
$0 = logger
return logger
}
}
set {
self.loggerInstance.withLock {
$0 = newValue
}
}
}
/**
The default `DataStack` instance to be used. If `defaultStack` is not set during the first time accessed, a default-configured `DataStack` will be created.
@@ -48,20 +71,22 @@ public enum CoreStoreDefaults {
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 {
self.defaultStackBarrierQueue.async(flags: .barrier) {
self.defaultStackInstance.withLock {
self.defaultStackInstance = newValue
$0 = newValue
}
}
}
@@ -69,7 +94,6 @@ public enum CoreStoreDefaults {
// MARK: Private
private static let defaultStackBarrierQueue = DispatchQueue.concurrent("com.coreStore.defaultStackBarrierQueue", qos: .userInteractive)
private static var defaultStackInstance: DataStack?
private static let defaultStackInstance: OSAllocatedUnfairLock<DataStack?> = .init(initialState: nil)
private static let loggerInstance: OSAllocatedUnfairLock<(any CoreStoreLogger)?> = .init(initialState: nil)
}
+2 -2
View File
@@ -24,7 +24,7 @@
//
import Foundation
import CoreData
@preconcurrency import CoreData
// MARK: - CoreStoreError
@@ -32,7 +32,7 @@ import CoreData
/**
All errors thrown from CoreStore are expressed in `CoreStoreError` enum values.
*/
public enum CoreStoreError: Error, CustomNSError, Hashable {
public enum CoreStoreError: Error, CustomNSError, Hashable, @unchecked Sendable {
/**
A failure occured because of an unknown error.
+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.
*/
public protocol CoreStoreLogger {
public protocol CoreStoreLogger: Sendable {
/**
Handles log messages sent by the `CoreStore` framework.
+2 -10
View File
@@ -29,7 +29,8 @@ import Foundation
// MARK: - CoreStoreManagedObject
@objc internal class CoreStoreManagedObject: NSManagedObject {
@objc
internal class CoreStoreManagedObject: NSManagedObject {
internal typealias CustomGetter = @convention(block) (_ rawObject: Any) -> Any?
internal typealias CustomSetter = @convention(block) (_ rawObject: Any, _ newValue: Any?) -> Void
@@ -42,12 +43,3 @@ import Foundation
return "_\(NSStringFromClass(CoreStoreManagedObject.self))__\(modelVersion)__\(NSStringFromClass(entity.type))__\(entity.entityName)"
}
}
// MARK: - Private
private enum Static {
static let queue = DispatchQueue.concurrent("com.coreStore.coreStoreManagerObjectBarrierQueue", qos: .userInteractive)
static var cache: [ObjectIdentifier: [KeyPathString: Set<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
@nonobjc
static var swizzler: Any? = Internals.with {
static let swizzler: Void? = Internals.with {
let bridgeClass: AnyClass = _CoreStoreObjectKeyValueObservation.self
let rootObserveImpl = class_getInstanceMethod(
+2 -2
View File
@@ -266,6 +266,6 @@ fileprivate enum Static {
// MARK: FilePrivate
fileprivate static var metaCache: [ObjectIdentifier: Any] = [:]
fileprivate static var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:]
fileprivate static nonisolated(unsafe) var metaCache: [ObjectIdentifier: Any] = [:]
fileprivate static nonisolated(unsafe) var propertiesCache: [ObjectIdentifier: [PropertyProtocol]] = [:]
}
+29 -13
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`.
*/
public struct AsyncNamespace {
public struct AsyncNamespace: Sendable {
// MARK: Public
@@ -124,7 +124,7 @@ extension DataStack.AsyncNamespace {
bufferingPolicy: .unbounded,
{ continuation in
var progress: Progress? = nil
nonisolated(unsafe) var progress: Progress? = nil
progress = self.base.addStorage(
storage,
completion: { result in
@@ -151,6 +151,8 @@ extension DataStack.AsyncNamespace {
)
if let progress = progress {
Internals.mainActorImmediate { @MainActor in
progress.setProgressHandler { progress in
continuation.yield(
@@ -162,6 +164,7 @@ extension DataStack.AsyncNamespace {
}
}
}
}
)
}
@@ -185,7 +188,7 @@ extension DataStack.AsyncNamespace {
return try await Internals.withCheckedThrowingContinuation { continuation in
self.base.perform(
return self.base.perform(
asynchronous: { (transaction) -> O? in
return try transaction.importObject(
@@ -196,10 +199,13 @@ extension DataStack.AsyncNamespace {
success: {
continuation.resume(
with: .success($0.flatMap(self.base.fetchExisting))
returning: $0.flatMap(self.base.fetchExisting)
)
},
failure: continuation.resume(throwing:)
failure: {
continuation.resume(throwing: $0)
}
)
}
}
@@ -222,6 +228,7 @@ extension DataStack.AsyncNamespace {
source: O.ImportSource
) async throws(any Swift.Error) -> O? {
nonisolated(unsafe) let object = object
return try await Internals.withCheckedThrowingContinuation { continuation in
self.base.perform(
@@ -240,10 +247,13 @@ extension DataStack.AsyncNamespace {
success: {
continuation.resume(
with: .success($0.flatMap(self.base.fetchExisting))
returning: $0.flatMap(self.base.fetchExisting)
)
},
failure: continuation.resume(throwing:)
failure: {
continuation.resume(throwing: $0)
}
)
}
}
@@ -279,10 +289,13 @@ extension DataStack.AsyncNamespace {
success: {
continuation.resume(
with: .success($0.flatMap(self.base.fetchExisting))
returning: $0.flatMap(self.base.fetchExisting)
)
},
failure: continuation.resume(throwing:)
failure: {
continuation.resume(throwing: $0)
}
)
}
}
@@ -306,7 +319,7 @@ extension DataStack.AsyncNamespace {
- returns: The imported objects correctly associated for the `DataStack`.
- throws: A `CoreStoreError` value indicating the failure reason
*/
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence>(
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence & Sendable>(
_ into: Into<O>,
sourceArray: S,
preProcess: @escaping @Sendable (
@@ -329,10 +342,13 @@ extension DataStack.AsyncNamespace {
success: {
continuation.resume(
with: .success(self.base.fetchExisting($0))
returning: self.base.fetchExisting($0)
)
},
failure: continuation.resume(throwing:)
failure: {
continuation.resume(throwing: $0)
}
)
}
}
@@ -357,7 +373,7 @@ extension DataStack.AsyncNamespace {
- returns: The value returned from the `task` closure.
- throws: A `CoreStoreError` value indicating the failure reason
*/
public func perform<Output>(
public func perform<Output: Sendable>(
_ asynchronous: @escaping @Sendable (AsynchronousDataTransaction) throws(any Swift.Error) -> Output
) async throws(any Swift.Error) -> Output {
+53 -21
View File
@@ -24,7 +24,8 @@
//
import Foundation
import CoreData
@preconcurrency import CoreData
import os
// MARK: - DataStack
@@ -49,7 +50,7 @@ extension DataStack {
*/
public func addStorage<T>(
_ storage: T,
completion: @escaping (SetupResult<T>) -> Void
completion: @escaping @MainActor (SetupResult<T>) -> Void
) {
self.coordinator.performAsynchronously {
@@ -110,7 +111,7 @@ extension DataStack {
*/
public func addStorage<T: LocalStorage>(
_ storage: T,
completion: @escaping (SetupResult<T>) -> Void
completion: @escaping @MainActor (SetupResult<T>) -> Void
) -> Progress? {
let fileURL = storage.fileURL
@@ -162,7 +163,7 @@ extension DataStack {
attributes: nil
)
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
nonisolated(unsafe) let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: type(of: storage).storeType,
at: fileURL as URL,
options: storage.storeOptions
@@ -192,12 +193,18 @@ extension DataStack {
}
catch {
DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
}
return
}
DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
return
}
@@ -212,9 +219,12 @@ extension DataStack {
}
catch {
DispatchQueue.main.async {
completion(.failure(CoreStoreError(error)))
}
}
}
)
}
catch let error as NSError
@@ -264,7 +274,7 @@ extension DataStack {
*/
public func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T,
completion: @escaping (MigrationResult) -> Void
completion: @escaping @MainActor (MigrationResult) -> Void
) throws(CoreStoreError) -> Progress? {
return try self.coordinator.performSynchronously {
@@ -376,7 +386,7 @@ extension DataStack {
private func upgradeStorageIfNeeded<T: LocalStorage>(
_ storage: T,
metadata: [String: Any],
completion: @escaping (MigrationResult) -> Void
completion: @escaping @MainActor (MigrationResult) -> Void
) -> Progress? {
guard let migrationSteps = self.computeMigrationFromStorage(storage, metadata: metadata) else {
@@ -423,9 +433,13 @@ extension DataStack {
}
let migrationTypes = migrationSteps.map { $0.migrationType }
var migrationResult: MigrationResult?
let migrationState: OSAllocatedUnfairLock<(migrationResult: MigrationResult?, cancelled: Bool)> = .init(
initialState: (
migrationResult: nil,
cancelled: false
)
)
var operations = [Operation]()
var cancelled = false
let progress = Progress(parent: nil, userInfo: nil)
progress.totalUnitCount = numberOfMigrations
@@ -440,12 +454,15 @@ extension DataStack {
operations.append(
BlockOperation { [weak self] in
guard let self = self, !cancelled else {
guard
let self = self,
!migrationState.withLock({ $0.cancelled })
else {
return
}
autoreleasepool {
Internals.autoreleasepool {
do {
@@ -465,8 +482,14 @@ extension DataStack {
migrationError,
"Failed to migrate version model \"\(migrationType.sourceVersion)\" to version \"\(migrationType.destinationVersion)\"."
)
migrationResult = .failure(migrationError)
cancelled = true
migrationState.withLock { state in
if !state.cancelled {
state.migrationResult = .failure(migrationError)
state.cancelled = true
}
}
}
}
@@ -488,7 +511,10 @@ extension DataStack {
DispatchQueue.main.async {
progress.setProgressHandler(nil)
completion(migrationResult ?? .success(migrationTypes))
completion(
migrationState.withLock { $0.migrationResult }
?? .success(migrationTypes)
)
return
}
}
@@ -594,23 +620,29 @@ extension DataStack {
let estimatedTime: TimeInterval = 60 * 3 // 3 mins
let interval: TimeInterval = 1
let fakeTotalUnitCount: Float = 0.9 * Float(progress.totalUnitCount)
var fakeProgress: Float = 0
let fakeProgress: OSAllocatedUnfairLock<Float> = .init(initialState: 0)
var recursiveCheck: () -> Void = {}
recursiveCheck = { [weak timerQueue] in
@Sendable
func recursiveCheck() {
guard let timerQueue = timerQueue, fakeProgress < 1 else {
let enqueueNext = fakeProgress.withLock {
return
guard $0 < 1.0 else {
return false
}
progress.completedUnitCount = Int64(fakeTotalUnitCount * fakeProgress)
fakeProgress += Float(interval / estimatedTime)
progress.completedUnitCount = Int64(fakeTotalUnitCount * $0)
$0 += Float(interval / estimatedTime)
return true
}
if enqueueNext {
timerQueue.asyncAfter(
deadline: .now() + interval,
execute: recursiveCheck
)
}
}
timerQueue.async(execute: recursiveCheck)
_ = try withExtendedLifetime(NSPersistentStoreCoordinator(managedObjectModel: destinationModel)) { (coordinator: NSPersistentStoreCoordinator) in
@@ -626,7 +658,7 @@ extension DataStack {
}
timerQueue.sync {
fakeProgress = 1
fakeProgress.withLock({ $0 = 1.0 })
}
_ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel)
progress.completedUnitCount = progress.totalUnitCount
+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.
*/
public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ fetchClauses: FetchClause...
) {
@@ -145,7 +145,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ fetchClauses: [FetchClause]
) {
@@ -188,7 +188,7 @@ extension DataStack {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/
public func monitorList<B: FetchChainableBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B
) {
@@ -288,7 +288,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ sectionBy: SectionBy<O>,
_ fetchClauses: FetchClause...
@@ -311,7 +311,7 @@ extension DataStack {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ sectionBy: SectionBy<O>,
_ fetchClauses: [FetchClause]
@@ -356,7 +356,7 @@ extension DataStack {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/
public func monitorSectionedList<B: SectionMonitorBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ 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`.
*/
public struct ReactiveNamespace {
public struct ReactiveNamespace: Sendable {
// MARK: Public
@@ -99,6 +99,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.addStorage(
storage,
completion: { (result) in
@@ -179,6 +180,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform(
asynchronous: { (transaction) -> O? in
@@ -222,8 +224,10 @@ extension DataStack.ReactiveNamespace {
source: O.ImportSource
) -> Future<O?, CoreStoreError> {
nonisolated(unsafe) let object = object
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform(
asynchronous: { (transaction) -> O? in
@@ -273,6 +277,7 @@ extension DataStack.ReactiveNamespace {
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform(
asynchronous: { (transaction) -> O? in
@@ -316,16 +321,17 @@ extension DataStack.ReactiveNamespace {
- parameter preProcess: a closure that lets the caller tweak the internal `UniqueIDType`-to-`ImportSource` mapping to be used for importing. Callers can remove from/add to/update `mapping` and return the updated array from the closure.
- returns: A `Future` for the imported objects. The event values will be the object instances correctly associated for the `DataStack`.
*/
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence>(
public func importUniqueObjects<O: DynamicObject & ImportableUniqueObject, S: Sequence & Sendable>(
_ into: Into<O>,
sourceArray: S,
preProcess: @escaping (
preProcess: @escaping @Sendable (
_ mapping: [O.UniqueIDType: O.ImportSource]
) throws(any Swift.Error) -> [O.UniqueIDType: O.ImportSource] = { $0 }
) -> Future<[O], CoreStoreError> where S.Iterator.Element == O.ImportSource {
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform(
asynchronous: { (transaction) -> [O] in
@@ -370,14 +376,15 @@ extension DataStack.ReactiveNamespace {
- parameter task: the asynchronous closure where creates, updates, and deletes can be made to the transaction. Transaction blocks are executed serially in a background queue, and all changes are made from a concurrent `NSManagedObjectContext`.
- returns: A `Future` whose event value be the value returned from the `task` closure.
*/
public func perform<Output>(
_ asynchronous: @escaping (
public func perform<Output: Sendable>(
_ asynchronous: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> Output
) -> Future<Output, CoreStoreError> {
return .init { (promise) in
nonisolated(unsafe) let promise = promise
self.base.perform(
asynchronous: asynchronous,
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 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>(
asynchronous task: @escaping (
public func perform<T: Sendable>(
asynchronous task: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T,
sourceIdentifier: Any? = nil,
completion: @escaping (AsynchronousDataTransaction.Result<T>) -> Void
sourceIdentifier: (any Sendable)? = nil,
completion: @escaping @Sendable (AsynchronousDataTransaction.Result<T>) -> Void
) {
self.perform(
@@ -63,22 +63,22 @@ extension DataStack {
- parameter failure: the closure executed if the save fails or if any errors are thrown within `task`. Cancelled `task`s will be indicated by `CoreStoreError.userCancelled`. Custom errors thrown by the user will be wrapped in `CoreStoreError.userError(error: Error)`.
*/
public func perform<T>(
asynchronous task: @escaping (
asynchronous task: @escaping @Sendable (
_ transaction: AsynchronousDataTransaction
) throws(any Swift.Error) -> T,
sourceIdentifier: Any? = nil,
success: @escaping (T) -> Void,
failure: @escaping (CoreStoreError) -> Void
sourceIdentifier: (any Sendable)? = nil,
success: @escaping @Sendable (sending T) -> Void,
failure: @escaping @Sendable (CoreStoreError) -> Void
) {
let transaction = AsynchronousDataTransaction(
nonisolated(unsafe) let transaction = AsynchronousDataTransaction(
mainContext: self.rootSavingContext,
queue: self.childTransactionQueue,
sourceIdentifier: sourceIdentifier
)
transaction.transactionQueue.cs_async {
let userInfo: T
nonisolated(unsafe) let userInfo: T
do {
userInfo = try task(transaction)
@@ -125,7 +125,7 @@ extension DataStack {
_ transaction: SynchronousDataTransaction
) throws(any Swift.Error) -> T,
waitForAllObservers: Bool = true,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) throws(CoreStoreError) -> T {
let transaction = SynchronousDataTransaction(
@@ -172,7 +172,7 @@ extension DataStack {
*/
public func beginUnsafe(
supportsUndo: Bool = false,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) -> UnsafeDataTransaction {
return UnsafeDataTransaction(
+7 -3
View File
@@ -90,7 +90,8 @@ extension DataStack {
// 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
@@ -114,7 +115,7 @@ extension DataStack {
return
}
var progress: Progress? = nil
nonisolated(unsafe) var progress: Progress? = nil
progress = self.dataStack.addStorage(
self.storage,
completion: { [weak self] result in
@@ -150,7 +151,9 @@ extension DataStack {
)
if let progress = progress {
progress.setProgressHandler { [weak self] progress in
Internals.mainActorImmediate { @MainActor [weak self] in
progress.setProgressHandler { progress in
guard
let self = self,
@@ -168,6 +171,7 @@ extension DataStack {
}
}
}
}
// MARK: Cancellable
+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`.
*/
public final class DataStack: Equatable {
public final class DataStack: Equatable, @unchecked Sendable {
/**
The resolved application name, used by the `DataStack` as the default Xcode model name (.xcdatamodel filename) if not explicitly provided.
@@ -398,7 +398,7 @@ public final class DataStack: Equatable {
- parameter completion: the closure to execute after all persistent stores are removed
*/
public func unsafeRemoveAllPersistentStores(
completion: @escaping () -> Void = {}
completion: @escaping @MainActor () -> Void = {}
) {
let coordinator = self.coordinator
@@ -459,7 +459,7 @@ public final class DataStack: Equatable {
// MARK: Internal
internal static var defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME"
internal static let defaultConfigurationName = "PF_DEFAULT_CONFIGURATION_NAME"
internal let coordinator: NSPersistentStoreCoordinator
internal let rootSavingContext: NSManagedObjectContext
+13 -3
View File
@@ -95,7 +95,10 @@ extension DiffableDataSource {
- parameter dataStack: the `DataStack` instance that the dataSource will fetch objects from
- parameter cellProvider: a closure that configures and returns the `UITableViewCell` for the object
*/
public init(target: T, dataStack: DataStack) {
public init(
target: T,
dataStack: DataStack
) {
self.target = target
self.dataStack = dataStack
@@ -106,7 +109,10 @@ extension DiffableDataSource {
Clears the target.
- parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`.
*/
open func purge(animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) {
open func purge(
animatingDifferences: Bool = true,
completion: @escaping @Sendable () -> Void = {}
) {
self.dispatcher.purge(
target: self.target,
@@ -136,7 +142,11 @@ extension DiffableDataSource {
- parameter snapshot: the `ListSnapshot` used to reload the target with. This is typically from the `snapshot` property of a `ListPublisher`.
- parameter animatingDifferences: if `true`, animations may be applied accordingly. Defaults to `true`.
*/
open func apply(_ snapshot: ListSnapshot<O>, animatingDifferences: Bool = true, completion: @escaping () -> Void = {}) {
open func apply(
_ snapshot: ListSnapshot<O>,
animatingDifferences: Bool = true,
completion: @escaping @Sendable () -> Void = {}
) {
let diffableSnapshot = snapshot.diffableSnapshot
self.dispatcher.apply(
@@ -83,6 +83,7 @@ extension DiffableDataSource {
- parameter itemProvider: a closure that configures and returns the `NSCollectionViewItem` for the object
*/
@nonobjc
@MainActor
public init(
collectionView: NSCollectionView,
dataStack: DataStack,
@@ -102,6 +103,7 @@ extension DiffableDataSource {
// MARK: - NSCollectionViewDataSource
@objc
@MainActor
public dynamic func numberOfSections(
in collectionView: NSCollectionView
) -> Int {
@@ -110,6 +112,7 @@ extension DiffableDataSource {
}
@objc
@MainActor
public dynamic func collectionView(
_ collectionView: NSCollectionView,
numberOfItemsInSection section: Int
@@ -119,6 +122,7 @@ extension DiffableDataSource {
}
@objc
@MainActor
open dynamic func collectionView(
_ collectionView: NSCollectionView,
itemForRepresentedObjectAt indexPath: IndexPath
@@ -140,6 +144,7 @@ extension DiffableDataSource {
}
@objc
@MainActor
open dynamic func collectionView(
_ collectionView: NSCollectionView,
viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind,
@@ -181,60 +186,93 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool {
return MainActor.assumeIsolated {
return self.base?.window == nil
}
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteSections(indices)
}
}
public func insertSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.insertSections(indices)
}
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.reloadSections(indices)
}
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
MainActor.assumeIsolated {
self.base?.moveSection(index, toSection: newIndex)
}
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteItems(at: Set(indexPaths))
}
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.insertItems(at: Set(indexPaths))
}
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.reloadItems(at: Set(indexPaths))
}
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
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) {
MainActor.assumeIsolated {
self.base?.animator().performBatchUpdates(updates, completionHandler: { _ in completion() })
}
}
public func reloadData() {
MainActor.assumeIsolated {
self.base?.reloadData()
}
}
}
}
#endif
@@ -185,59 +185,92 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool {
return MainActor.assumeIsolated {
return self.base?.window == nil
}
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteSections(indices)
}
}
public func insertSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.insertSections(indices)
}
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.reloadSections(indices)
}
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
MainActor.assumeIsolated {
self.base?.moveSection(index, toSection: newIndex)
}
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteItems(at: indexPaths)
}
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
Internals.mainActorImmediate {
self.base?.insertItems(at: indexPaths)
}
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
Internals.mainActorImmediate {
self.base?.reloadItems(at: indexPaths)
}
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
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) {
Internals.mainActorImmediate {
self.base?.performBatchUpdates(updates, completion: { _ in completion() })
}
}
public func reloadData() {
Internals.mainActorImmediate {
self.base?.reloadData()
}
}
}
}
#endif
@@ -85,7 +85,7 @@ extension DiffableDataSource {
public init(
tableView: UITableView,
dataStack: DataStack,
cellProvider: @escaping (UITableView, IndexPath, O) -> UITableViewCell?
cellProvider: @escaping @MainActor (UITableView, IndexPath, O) -> UITableViewCell?
) {
self.cellProvider = cellProvider
@@ -97,6 +97,7 @@ extension DiffableDataSource {
/**
The target `UITableView`
*/
@MainActor
public var tableView: UITableView? {
return self.target.base
@@ -240,50 +241,79 @@ extension DiffableDataSource {
public var shouldSuspendBatchUpdates: Bool {
return MainActor.assumeIsolated {
return self.base?.window == nil
}
}
public func deleteSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteSections(indices, with: .automatic)
}
}
public func insertSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.insertSections(indices, with: .automatic)
}
}
public func reloadSections(at indices: IndexSet, animated: Bool) {
MainActor.assumeIsolated {
self.base?.reloadSections(indices, with: .automatic)
}
}
public func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool) {
MainActor.assumeIsolated {
self.base?.moveSection(index, toSection: newIndex)
}
}
public func deleteItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.deleteRows(at: indexPaths, with: .automatic)
}
}
public func insertItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.insertRows(at: indexPaths, with: .automatic)
}
}
public func reloadItems(at indexPaths: [IndexPath], animated: Bool) {
MainActor.assumeIsolated {
self.base?.reloadRows(at: indexPaths, with: .automatic)
}
}
public func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool) {
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) {
MainActor.assumeIsolated {
guard let base = self.base else {
@@ -291,12 +321,16 @@ extension DiffableDataSource {
}
base.performBatchUpdates(updates, completion: { _ in completion() })
}
}
public func reloadData() {
MainActor.assumeIsolated {
self.base?.reloadData()
}
}
}
}
#endif
+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.
*/
public protocol DiffableDataSourceTarget {
public protocol DiffableDataSourceTarget: Sendable {
// MARK: Public
@@ -98,7 +98,7 @@ public protocol DiffableDataSourceTarget {
/**
Animates multiple insert, delete, reload, and move operations as a group.
*/
func performBatchUpdates(updates: () -> Void, animated: Bool, completion: @escaping () -> Void)
func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void)
/**
Reloads all sections and items.
@@ -110,18 +110,24 @@ extension DiffableDataSource.Target {
// MARK: Internal
internal func reload<C, O>(
internal func reload<C: Sendable, O>(
using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
animated: Bool,
interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
setData: (C) -> Void,
setData: @escaping @Sendable (C) -> Void,
completion: @escaping () -> Void
) {
let group = DispatchGroup()
defer {
group.notify(queue: .main, execute: completion)
group.notify(
queue: .main,
execute: {
completion()
}
)
}
if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data {
+2 -2
View File
@@ -99,7 +99,7 @@ extension DispatchQueue {
@nonobjc @inline(__always)
internal func cs_async(
_ closure: @escaping () -> Void
_ closure: @escaping @Sendable () -> Void
) {
self.async { autoreleasepool(invoking: closure) }
@@ -115,7 +115,7 @@ extension DispatchQueue {
@nonobjc @inline(__always)
internal func cs_barrierAsync(
_ closure: @escaping () -> Void
_ closure: @escaping @Sendable () -> Void
) {
self.async(flags: .barrier) { autoreleasepool(invoking: closure) }
+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`.
*/
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`.
+5 -2
View File
@@ -31,7 +31,7 @@ import CoreData
/**
A storage interface that is backed only in memory.
*/
public final class InMemoryStore: StorageInterface {
public final class InMemoryStore: StorageInterface, @unchecked Sendable {
/**
Initializes an `InMemoryStore` for the specified configuration
@@ -66,7 +66,10 @@ public final class InMemoryStore: StorageInterface {
/**
The options dictionary for the `NSPersistentStore`. For `InMemoryStore`s, this is always set to `nil`.
*/
public let storeOptions: [AnyHashable: Any]? = nil
public var storeOptions: [AnyHashable: Any]? {
return nil
}
/**
Do not call directly. Used by the `DataStack` internally.
+1 -1
View File
@@ -95,7 +95,7 @@ extension Internals {
// MARK: FilePrivate
fileprivate static var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:]
fileprivate static nonisolated(unsafe) var cachedCoders: [NSValueTransformerName: AnyFieldCoder] = [:]
// MARK: - TransformableDefaultValueCodingBox
@@ -36,7 +36,7 @@ extension Internals {
// Bugfix for NSFetchRequest messing up memory management for `affectedStores`
// http://stackoverflow.com/questions/14396375/nsfetchedresultscontroller-crashes-in-ios-6-if-affectedstores-is-specified
internal final class CoreStoreFetchRequest<T: NSFetchRequestResult>: NSFetchRequest<NSFetchRequestResult> {
internal final class CoreStoreFetchRequest<T: NSFetchRequestResult>: NSFetchRequest<NSFetchRequestResult>, @unchecked Sendable {
@nonobjc
internal func safeAffectedStores() -> [NSPersistentStore]? {
@@ -33,7 +33,7 @@ extension Internals {
// MARK: - CoreStoreFetchedResultsController
internal final class CoreStoreFetchedResultsController: NSFetchedResultsController<NSManagedObject> {
internal final class CoreStoreFetchedResultsController: NSFetchedResultsController<NSManagedObject>, @unchecked Sendable {
// MARK: Internal
@@ -43,7 +43,7 @@ extension Internals {
// MARK: - DiffableDataSourceSnapshot
// Implementation based on https://github.com/ra1028/DiffableDataSources
internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol {
internal struct DiffableDataSourceSnapshot: DiffableDataSourceSnapshotProtocol, @unchecked Sendable {
// MARK: Internal
@@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - ChangeSet
// 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 sectionDeleted: [Int]
@@ -36,7 +36,7 @@ extension Internals.DiffableDataUIDispatcher {
// Implementation based on https://github.com/ra1028/DifferenceKit
@usableFromInline
internal struct DiffResult<Index> {
internal struct DiffResult<Index: Sendable>: Sendable {
@usableFromInline
internal let deleted: [Index]
@@ -213,7 +213,7 @@ extension Internals.DiffableDataUIDispatcher {
// Implementation based on https://github.com/ra1028/DifferenceKit
@usableFromInline
internal struct Trace<I> {
internal struct Trace<I: Sendable>: Sendable {
@usableFromInline
internal var reference: I?
@@ -35,7 +35,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - StagedChangeset
// 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
var changesets: ContiguousArray<Changeset<C>>
@@ -117,7 +118,8 @@ extension Internals.DiffableDataUIDispatcher {
// MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable
extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: Differentiable {
extension Internals.DiffableDataUIDispatcher.StagedChangeset
where C: RangeReplaceableCollection, C.Element: Differentiable {
@inlinable
internal init(source: C, target: C) {
@@ -225,7 +227,8 @@ extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeRepla
// MARK: - Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection
extension Internals.DiffableDataUIDispatcher.StagedChangeset where C: RangeReplaceableCollection, C.Element: DifferentiableSection {
extension Internals.DiffableDataUIDispatcher.StagedChangeset
where C: RangeReplaceableCollection & Sendable, C.Element: DifferentiableSection & Sendable {
@inlinable
internal init(source: C, target: C) {
@@ -32,6 +32,8 @@ import QuartzCore
#endif
import os
// MARK: - Internals
@@ -41,7 +43,7 @@ extension Internals {
// Implementation based on https://github.com/ra1028/DiffableDataSources
@usableFromInline
internal final class DiffableDataUIDispatcher<O: DynamicObject> {
internal final class DiffableDataUIDispatcher<O: DynamicObject>: @unchecked Sendable {
// MARK: Internal
@@ -55,10 +57,10 @@ extension Internals {
func purge<Target: DiffableDataSource.Target>(
target: Target?,
animatingDifferences: Bool,
performUpdates: @escaping (
performUpdates: @escaping @Sendable (
Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
@escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void
) {
@@ -74,14 +76,14 @@ extension Internals {
_ snapshot: DiffableDataSourceSnapshot,
target: Target?,
animatingDifferences: Bool,
performUpdates: @escaping (
performUpdates: @escaping @Sendable (
Target,
StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
@escaping ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
@escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
) -> Void
) {
self.dispatcher.dispatch { [weak self] in
self.dispatcher.dispatch { @MainActor [weak self] in
guard let self = self else {
@@ -97,13 +99,17 @@ extension Internals {
return
}
let performDiffingUpdates: () -> Void = {
let performDiffingUpdates: @MainActor () -> Void = {
let changeset = StagedChangeset(source: self.sections, target: newSections)
performUpdates(target, changeset) { sections in
performUpdates(
target,
changeset,
{ sections in
self.sections = sections
}
)
}
#if os(watchOS) || !canImport(QuartzCore)
@@ -212,7 +218,7 @@ extension Internals {
// MARK: - ElementPath
@usableFromInline
internal struct ElementPath: Hashable {
internal struct ElementPath: Hashable, Sendable {
@usableFromInline
var element: Int
@@ -231,18 +237,21 @@ extension Internals {
// MARK: - MainThreadSerialDispatcher
fileprivate final class MainThreadSerialDispatcher {
fileprivate final class MainThreadSerialDispatcher: Sendable {
// MARK: FilePrivate
fileprivate init() {}
fileprivate func dispatch(_ action: @escaping () -> Void) {
fileprivate func dispatch(_ action: @escaping @MainActor () -> Void) {
let count = self.executingCount.incrementAndGet()
if Thread.isMainThread && count == 1 {
MainActor.assumeIsolated {
action()
}
self.executingCount.decrement()
}
else {
@@ -267,36 +276,31 @@ extension Internals {
// MARK: - AtomicInt
fileprivate class AtomicInt {
fileprivate final class AtomicInt: Sendable {
// MARK: FilePrivate
fileprivate func incrementAndGet() -> Int {
self.lock.wait()
defer {
return self.value.withLock {
self.lock.signal()
$0 += 1
return $0
}
self.value += 1
return self.value
}
fileprivate func decrement() {
self.lock.wait()
defer {
self.value.withLock {
self.lock.signal()
$0 -= 1
}
self.value -= 1
}
// MARK: Private
private let lock = DispatchSemaphore(value: 1)
private var value = 0
private let value: OSAllocatedUnfairLock<Int> = .init(initialState: 0)
}
}
@@ -64,7 +64,7 @@ extension Internals {
// MARK: - FetchedResultsControllerDelegate
internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate, @unchecked Sendable {
// MARK: Internal
+1 -1
View File
@@ -42,7 +42,7 @@ extension Internals {
notificationName: Notification.Name,
object: Any?,
queue: OperationQueue? = nil,
closure: @escaping (_ note: Notification) -> Void
closure: @escaping @Sendable (_ note: Notification) -> Void
) {
self.observer = NotificationCenter.default.addObserver(
@@ -31,7 +31,7 @@ extension Internals {
// MARK: - SharedNotificationObserver
internal final class SharedNotificationObserver<T> {
internal final class SharedNotificationObserver<T>: @unchecked Sendable {
// MARK: Internal
@@ -39,7 +39,7 @@ extension Internals {
notificationName: Notification.Name,
object: Any?,
queue: OperationQueue? = nil,
sharedValue: @escaping (_ note: Notification) -> T
sharedValue: @escaping @Sendable (_ note: Notification) -> T
) {
self.observer = NotificationCenter.default.addObserver(
+45 -10
View File
@@ -122,6 +122,7 @@ internal enum Internals {
return closure()
}
@inline(__always)
internal static func autoreleasepool<T>(
_ closure: () -> T
@@ -139,29 +140,63 @@ internal enum Internals {
}
@inline(__always)
internal static func autoreleasepool<T>(
_ closure: () throws(CoreStoreError) -> T
) throws(CoreStoreError) -> T {
do {
internal static func autoreleasepool<T, E>(
_ closure: () throws(E) -> T
) throws(E) -> T {
return try ObjectiveC.autoreleasepool(invoking: closure)
}
catch {
throw CoreStoreError(error)
}
}
@inline(__always)
internal static func withCheckedThrowingContinuation<T>(
function: String = #function,
_ 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(
function: function,
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"))
```
*/
public struct Into<O: DynamicObject>: Hashable {
public struct Into<O: DynamicObject>: Hashable, @unchecked Sendable {
/**
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.
*/
public final class ListMonitor<O: DynamicObject>: Hashable {
public final class ListMonitor<O: DynamicObject>: Hashable, @unchecked Sendable {
// MARK: Public (Accessors)
@@ -648,7 +648,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
*/
public func refetch(
_ fetchClauses: FetchClause...,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) {
self.refetch(
@@ -668,7 +668,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
*/
public func refetch(
_ fetchClauses: [FetchClause],
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) {
self.refetch(
@@ -751,7 +751,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>,
sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: @escaping (ListMonitor<O>) -> Void
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void
) {
self.init(
@@ -786,7 +786,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>,
sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: @escaping (ListMonitor<O>) -> Void
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void
) {
self.init(
@@ -803,7 +803,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer,
name: Notification.Name,
toObserver observer: AnyObject,
callback: @escaping (_ monitor: ListMonitor<O>) -> Void
callback: @escaping @Sendable (_ monitor: ListMonitor<O>) -> Void
) {
Internals.setAssociatedRetainedObject(
@@ -828,7 +828,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer,
name: Notification.Name,
toObserver observer: AnyObject,
callback: @escaping (
callback: @escaping @Sendable (
_ monitor: ListMonitor<O>,
_ object: O,
_ indexPath: IndexPath?,
@@ -864,7 +864,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
_ notificationKey: UnsafeRawPointer,
name: Notification.Name,
toObserver observer: AnyObject,
callback: @escaping (
callback: @escaping @Sendable (
_ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo,
_ 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,
willChange: @escaping (
willChange: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>
) -> Void,
didChange: @escaping (
didChange: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>
) -> Void,
willRefetch: @escaping (
willRefetch: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>
) -> Void,
didRefetch: @escaping (
didRefetch: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>
) -> 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,
didInsertObject: @escaping (
didInsertObject: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>,
_ object: O,
_ toIndexPath: IndexPath
) -> Void,
didDeleteObject: @escaping (
didDeleteObject: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>,
_ object: O,
_ fromIndexPath: IndexPath
) -> Void,
didUpdateObject: @escaping (
didUpdateObject: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>,
_ object: O,
_ atIndexPath: IndexPath
) -> Void,
didMoveObject: @escaping (
didMoveObject: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<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,
didInsertSection: @escaping (
didInsertSection: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo,
_ toIndex: Int
) -> Void,
didDeleteSection: @escaping (
didDeleteSection: @escaping @Sendable (
_ observer: U,
_ monitor: ListMonitor<O>,
_ sectionInfo: NSFetchedResultsSectionInfo,
@@ -1127,7 +1127,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
internal func refetch(
_ applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
Internals.assert(
@@ -1302,7 +1302,7 @@ public final class ListMonitor<O: DynamicObject>: Hashable {
from: From<O>,
sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
createAsynchronously: ((ListMonitor<O>) -> Void)?
createAsynchronously: (@Sendable (ListMonitor<O>) -> Void)?
) {
self.isSectioned = (sectionBy != nil)
+1 -1
View File
@@ -39,7 +39,7 @@ import CoreData
monitor.addObserver(self)
```
*/
public protocol ListObserver: AnyObject {
public protocol ListObserver: AnyObject, Sendable {
/**
The `NSManagedObject` type for the observed list
@@ -90,7 +90,8 @@ extension ListPublisher {
// 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
@@ -114,6 +115,8 @@ extension ListPublisher {
return
}
Internals.mainActorImmediate { [self] in
self.publisher.addObserver(
self,
notifyInitial: self.emitInitialValue,
@@ -130,6 +133,7 @@ extension ListPublisher {
}
)
}
}
// MARK: Cancellable
@@ -138,17 +142,10 @@ extension ListPublisher {
self.subscriber = nil
if Thread.isMainThread {
Internals.mainActorImmediate {
self.publisher.removeObserver(self)
}
else {
DispatchQueue.main.async {
self.publisher.removeObserver(self)
}
}
}
+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 callback: the closure to execute when changes occur
*/
@MainActor
public func addObserver<T: AnyObject>(
_ observer: T,
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 callback: the closure to execute when changes occur
*/
@MainActor
public func addObserver<T: AnyObject>(
_ observer: T,
notifyInitial: Bool = false,
@@ -159,6 +161,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
- parameter observer: the object whose notifications will be unregistered
*/
@MainActor
public func removeObserver<T: AnyObject>(_ observer: T) {
Internals.assert(
@@ -185,7 +188,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
*/
public func refetch<B: FetchChainableBuilderType>(
_ clauseChain: B,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) throws(any Swift.Error) where B.ObjectType == O {
try self.refetch(
@@ -214,7 +217,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
*/
public func refetch<B: SectionMonitorBuilderType>(
_ clauseChain: B,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) throws(any Swift.Error) where B.ObjectType == O {
try self.refetch(
@@ -342,7 +345,7 @@ public final class ListPublisher<O: DynamicObject>: Hashable {
from: From<O>,
sectionBy: SectionBy<O>?,
applyFetchClauses: @escaping (_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObject>) -> Void,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) throws(any Swift.Error) {
let (newFetchedResultsController, newFetchedResultsControllerDelegate) = Self.recreateFetchedResultsController(
+35 -9
View File
@@ -23,9 +23,9 @@
// SOFTWARE.
//
#if canImport(Combine) && canImport(SwiftUI)
#if canImport(Observation) && canImport(SwiftUI)
import Combine
import Observation
import SwiftUI
@@ -65,11 +65,12 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter listPublisher: The `ListPublisher` that the `ListState` will observe changes for
*/
@MainActor
public init(
_ 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
*/
@MainActor
public init<B: FetchChainableBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
@@ -139,6 +141,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/
@MainActor
public init<B: SectionMonitorBuilderType>(
_ clauseChain: B,
in dataStack: DataStack
@@ -174,6 +177,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public init(
_ from: From<Object>,
_ fetchClauses: FetchClause...,
@@ -212,6 +216,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
- parameter from: a `From` clause indicating the entity type
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public init(
_ from: From<Object>,
_ 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 fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public init(
_ from: From<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 fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
@MainActor
public init(
_ from: From<Object>,
_ sectionBy: SectionBy<Object>,
@@ -316,11 +323,13 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
// MARK: @propertyWrapper
@MainActor
public var wrappedValue: ListSnapshot<Object> {
return self.observer.items
}
@MainActor
public var projectedValue: ListPublisher<Object> {
return self.observer.listPublisher
@@ -337,23 +346,40 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
// MARK: Private
@ObservedObject
@State
private var observer: Observer
// MARK: - Observer
private final class Observer: ObservableObject {
@MainActor
private final class Observer: Observation.Observable {
@Published
var items: ListSnapshot<Object>
private let registrar = ObservationRegistrar()
private var _items: ListSnapshot<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>) {
self.listPublisher = listPublisher
self.items = listPublisher.snapshot
self._items = listPublisher.snapshot
listPublisher.addObserver(self) { [weak self] (listPublisher) in
@@ -365,7 +391,7 @@ public struct ListState<Object: DynamicObject>: DynamicProperty {
}
}
deinit {
isolated deinit {
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.
*/
public enum MigrationType: Hashable {
public enum MigrationType: Hashable, Sendable {
/**
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 {
static var coreStoreObject: Void?
static nonisolated(unsafe) var coreStoreObject: Void?
}
}
@@ -192,8 +192,8 @@ extension NSManagedObjectContext {
private struct PropertyKeys {
static var observerForWillSaveNotification: Void?
static var shouldCascadeSavesToParent: Void?
static nonisolated(unsafe) var observerForWillSaveNotification: Void?
static nonisolated(unsafe) var shouldCascadeSavesToParent: Void?
}
+70 -123
View File
@@ -293,30 +293,23 @@ extension NSManagedObjectContext: FetchableSource, QueryableSource {
_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID>
) throws(CoreStoreError) -> [NSManagedObjectID] {
var fetchResults: [NSManagedObjectID]?
var fetchError: Error?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.dynamicCast())
return try self.performAndWait {
return try self.fetch(fetchRequest.staticCast())
}
}
catch {
fetchError = error
}
}
if let fetchResults = fetchResults {
return fetchResults
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
"Failed executing query request."
)
throw coreStoreError
}
}
// MARK: QueryableSource
@@ -443,89 +436,74 @@ extension NSManagedObjectContext {
_ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> O? {
var fetchResults: [O]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
let fetchResults = try self.performAndWait {
return try self.fetch(fetchRequest.staticCast())
}
return fetchResults.first
}
catch {
fetchError = error
}
}
if let fetchResults = fetchResults {
return fetchResults.first
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
)
throw coreStoreError
}
}
@nonobjc
internal func fetchAll<O: NSManagedObject>(
_ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> [O] {
var fetchResults: [O]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
return try self.performAndWait {
return try self.fetch(fetchRequest.staticCast())
}
}
catch {
fetchError = error
}
}
if let fetchResults = fetchResults {
return fetchResults
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
)
throw coreStoreError
}
}
@nonobjc
internal func fetchCount(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSNumber>
) throws(CoreStoreError) -> Int {
var count = 0
var countError: (any Swift.Error)?
self.performAndWait {
do {
count = try self.count(for: fetchRequest.staticCast())
let count = try self.performAndWait {
return try self.count(for: fetchRequest.staticCast())
}
guard count != NSNotFound else {
throw CoreStoreError(nil)
}
return count
}
catch {
countError = error
}
}
if count == NSNotFound {
let coreStoreError = CoreStoreError(countError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing count request."
)
throw coreStoreError
}
return count
}
@nonobjc
@@ -533,30 +511,24 @@ extension NSManagedObjectContext {
_ fetchRequest: Internals.CoreStoreFetchRequest<NSManagedObjectID>
) throws(CoreStoreError) -> NSManagedObjectID? {
var fetchResults: [NSManagedObjectID]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
let fetchResults = try self.performAndWait {
return try self.fetch(fetchRequest.staticCast())
}
return fetchResults.first
}
catch {
fetchError = error
}
}
if let fetchResults = fetchResults {
return fetchResults.first
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
)
throw coreStoreError
}
}
// MARK: Querying
@@ -567,35 +539,29 @@ extension NSManagedObjectContext {
fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> U? {
var fetchResults: [Any]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
}
catch {
let fetchResults = try self.performAndWait {
fetchError = error
return try self.fetch(fetchRequest.staticCast())
}
}
if let fetchResults = fetchResults {
if let rawResult = fetchResults.first as? NSDictionary,
if let rawResult = fetchResults.first,
let rawObject = rawResult[selectTerms.first!.keyPathString] as? U.QueryableNativeType {
return Select<O, U>.ReturnType.cs_fromQueryableNativeType(rawObject)
}
return nil
}
let coreStoreError = CoreStoreError(fetchError)
catch {
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
"Failed executing query request."
)
throw coreStoreError
}
}
@nonobjc
internal func queryValue<O>(
@@ -603,65 +569,53 @@ extension NSManagedObjectContext {
fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> Any? {
var fetchResults: [Any]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
}
catch {
let fetchResults = try self.performAndWait {
fetchError = error
return try self.fetch(fetchRequest.staticCast())
}
}
if let fetchResults = fetchResults {
if let rawResult = fetchResults.first as? NSDictionary,
if let rawResult = fetchResults.first,
let rawObject = rawResult[selectTerms.first!.keyPathString] {
return rawObject
}
return nil
}
let coreStoreError = CoreStoreError(fetchError)
catch {
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
"Failed executing query request."
)
throw coreStoreError
}
}
@nonobjc
internal func queryAttributes(
_ fetchRequest: Internals.CoreStoreFetchRequest<NSDictionary>
) throws(CoreStoreError) -> [[String: Any]] {
var fetchResults: [Any]?
var fetchError: (any Swift.Error)?
self.performAndWait {
do {
fetchResults = try self.fetch(fetchRequest.staticCast())
let fetchResults = try self.performAndWait {
return try self.fetch(fetchRequest.staticCast())
}
return NSDictionary.cs_fromQueryResultsNativeType(fetchResults)
}
catch {
fetchError = error
}
}
if let fetchResults = fetchResults {
return NSDictionary.cs_fromQueryResultsNativeType(fetchResults)
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing fetch request."
"Failed executing query request."
)
throw coreStoreError
}
}
// MARK: Deleting
@@ -671,32 +625,24 @@ extension NSManagedObjectContext {
_ fetchRequest: Internals.CoreStoreFetchRequest<O>
) throws(CoreStoreError) -> Int {
var numberOfDeletedObjects: Int?
var fetchError: (any Swift.Error)?
self.performAndWait {
Internals.autoreleasepool {
do {
return try self.performAndWait {
return try Internals.autoreleasepool {
let fetchResults = try self.fetch(fetchRequest.staticCast())
for object in fetchResults {
self.delete(object)
}
numberOfDeletedObjects = fetchResults.count
return fetchResults.count
}
}
}
catch {
fetchError = error
}
}
}
if let numberOfDeletedObjects = numberOfDeletedObjects {
return numberOfDeletedObjects
}
let coreStoreError = CoreStoreError(fetchError)
let coreStoreError = CoreStoreError(error)
Internals.log(
coreStoreError,
"Failed executing delete request."
@@ -704,3 +650,4 @@ extension NSManagedObjectContext {
throw coreStoreError
}
}
}
+19 -11
View File
@@ -65,7 +65,7 @@ extension NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.persistentStoreCoordinator = coordinator
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
context.undoManager = nil
context.setupForCoreStoreWithContextName("com.corestore.rootcontext")
@@ -76,16 +76,21 @@ extension NSManagedObjectContext {
object: coordinator,
closure: { [weak context] (note) -> Void in
context?.perform { () -> Void in
guard let context = context else {
return
}
nonisolated(unsafe) let note = note
context.perform {
if let updatedObjectIDs = (note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObjectID>) {
for objectID in updatedObjectIDs {
context?.registeredObject(for: objectID)?.willAccessValue(forKey: nil)
context.registeredObject(for: objectID)?.willAccessValue(forKey: nil)
}
}
context?.mergeChanges(fromContextDidSave: note)
context.mergeChanges(fromContextDidSave: note)
}
}
)
@@ -100,7 +105,7 @@ extension NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = rootContext
context.mergePolicy = NSRollbackMergePolicy
context.mergePolicy = NSMergePolicy(merge: .rollbackMergePolicyType)
context.undoManager = nil
context.setupForCoreStoreWithContextName("com.corestore.maincontext")
context.observerForDidSaveNotification = Internals.NotificationObserver(
@@ -108,8 +113,10 @@ extension NSManagedObjectContext {
object: rootContext,
closure: { [weak context] (note) -> Void in
guard let rootContext = note.object as? NSManagedObjectContext,
let context = context else {
guard
let rootContext = note.object as? NSManagedObjectContext,
let context = context
else {
return
}
@@ -117,7 +124,8 @@ extension NSManagedObjectContext {
let saveMetadata = rootContext.saveMetadata
context.saveMetadata = saveMetadata
let mergeChanges = { () -> Void in
nonisolated(unsafe) let note = note
let mergeChanges = { @Sendable () -> Void in
if let updatedObjects = (note.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>) {
@@ -147,9 +155,9 @@ extension NSManagedObjectContext {
private struct PropertyKeys {
static var parentStack: Void?
static var observerForDidSaveNotification: Void?
static var observerForDidImportUbiquitousContentChangesNotification: Void?
static nonisolated(unsafe) var parentStack: Void?
static nonisolated(unsafe) var observerForDidSaveNotification: Void?
static nonisolated(unsafe) var observerForDidImportUbiquitousContentChangesNotification: Void?
}
@nonobjc
@@ -142,15 +142,14 @@ extension NSManagedObjectContext {
@nonobjc
internal func saveSynchronously(
waitForMerge: Bool,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) -> (hasChanges: Bool, error: CoreStoreError?) {
var result: (hasChanges: Bool, error: CoreStoreError?) = (false, nil)
self.performAndWait {
return self.performAndWait {
guard self.hasChanges else {
return
return (false, nil)
}
do {
@@ -168,8 +167,7 @@ extension NSManagedObjectContext {
saveError,
"Failed to save \(Internals.typeName(NSManagedObjectContext.self))."
)
result = (true, saveError)
return
return (true, saveError)
}
if let parentContext = self.parent, self.shouldCascadeSavesToParent {
@@ -177,20 +175,19 @@ extension NSManagedObjectContext {
waitForMerge: waitForMerge,
sourceIdentifier: sourceIdentifier
)
result = (true, error)
return (true, error)
}
else {
result = (true, nil)
return (true, nil)
}
}
return result
}
@nonobjc
internal func saveAsynchronously(
sourceIdentifier: Any?,
completion: @escaping (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in }
sourceIdentifier: (any Sendable)?,
completion: @escaping @MainActor (_ hasChanges: Bool, _ error: CoreStoreError?) -> Void = { (_, _) in }
) {
self.perform {
@@ -259,11 +256,11 @@ extension NSManagedObjectContext {
// MARK: Internal
internal let isSavingSynchronously: Bool
internal let sourceIdentifier: Any?
internal let sourceIdentifier: (any Sendable)?
internal init(
isSavingSynchronously: Bool,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
self.isSavingSynchronously = isSavingSynchronously
@@ -276,9 +273,9 @@ extension NSManagedObjectContext {
private struct PropertyKeys {
static var parentTransaction: Void?
static var saveMetadata: Void?
static var isTransactionContext: Void?
static var isDataStackContext: Void?
static nonisolated(unsafe) var parentTransaction: Void?
static nonisolated(unsafe) var saveMetadata: Void?
static nonisolated(unsafe) var isTransactionContext: Void?
static nonisolated(unsafe) var isDataStackContext: Void?
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ extension NSPersistentStore {
private struct PropertyKeys {
static var storageInterface: Void?
static nonisolated(unsafe) var storageInterface: Void?
}
}
@@ -33,7 +33,7 @@ extension NSPersistentStoreCoordinator {
@nonobjc
internal func performAsynchronously(
_ closure: @escaping () -> Void
_ closure: @escaping @Sendable () -> Void
) {
self.perform(closure)
@@ -41,41 +41,29 @@ extension NSPersistentStoreCoordinator {
@nonobjc
internal func performSynchronously<T>(
_ closure: @escaping () -> T
_ closure: @Sendable () -> T
) -> T {
var result: T?
self.performAndWait {
result = closure()
}
return result!
return self.performAndWait(closure)
}
@nonobjc
internal func performSynchronously<T>(
_ closure: @escaping () throws(any Swift.Error) -> T
_ closure: @Sendable () throws(any Swift.Error) -> T
) throws(CoreStoreError) -> T {
var closureError: (any Swift.Error)?
var result: T?
self.performAndWait {
do {
result = try closure()
return try self.performAndWait {
return try closure()
}
}
catch {
closureError = error
throw CoreStoreError(error)
}
}
if let closureError = closureError {
throw CoreStoreError(closureError)
}
return result!
}
@nonobjc
internal func addPersistentStoreSynchronously(
@@ -85,6 +73,7 @@ extension NSPersistentStoreCoordinator {
options: [AnyHashable: Any]?
) throws(CoreStoreError) -> NSPersistentStore {
nonisolated(unsafe) let options = options
return try self.performSynchronously {
return try self.addPersistentStore(
+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.
*/
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.
@@ -71,7 +71,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
- 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.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]) ?? [:]
}
internal func registerObserver<U: AnyObject>(
internal func registerObserver<U: AnyObject & Sendable>(
_ observer: U,
willChangeObject: @escaping (
willChangeObject: @escaping @Sendable (
_ observer: U,
_ monitor: ObjectMonitor<O>,
_ object: O
) -> Void,
didDeleteObject: @escaping (
didDeleteObject: @escaping @Sendable (
_ observer: U,
_ monitor: ObjectMonitor<O>,
_ object: O
) -> Void,
didUpdateObject: @escaping (
didUpdateObject: @escaping @Sendable (
_ observer: U,
_ monitor: ObjectMonitor<O>,
_ object: O,
@@ -361,7 +361,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
_ notificationKey: UnsafeRawPointer,
name: Notification.Name,
toObserver observer: AnyObject,
callback: @escaping (_ monitor: ObjectMonitor<O>) -> Void
callback: @escaping @Sendable (_ monitor: ObjectMonitor<O>) -> Void
) {
Internals.setAssociatedRetainedObject(
@@ -386,7 +386,7 @@ public final class ObjectMonitor<O: DynamicObject>: Hashable, ObjectRepresentati
_ notificationKey: UnsafeRawPointer,
name: Notification.Name,
toObserver observer: AnyObject,
callback: @escaping (_ monitor: ObjectMonitor<O>, _ object: O) -> Void
callback: @escaping @Sendable (_ monitor: ObjectMonitor<O>, _ object: O) -> Void
) {
Internals.setAssociatedRetainedObject(
+2 -2
View File
@@ -36,12 +36,12 @@ import CoreData
monitor.addObserver(self)
```
*/
public protocol ObjectObserver: AnyObject {
public protocol ObjectObserver: AnyObject & Sendable {
/**
The `DynamicObject` type for the observed object
*/
associatedtype ObjectEntityType: DynamicObject
associatedtype ObjectEntityType: DynamicObject & Sendable
/**
Handles processing just before a change to the observed `object` occurs. (Optional)
@@ -90,7 +90,8 @@ extension ObjectPublisher {
// 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
@@ -114,6 +115,8 @@ extension ObjectPublisher {
return
}
Internals.mainActorImmediate { [self] in
self.publisher.addObserver(
self,
notifyInitial: self.emitInitialValue,
@@ -130,6 +133,7 @@ extension ObjectPublisher {
}
)
}
}
// MARK: Cancellable
@@ -138,17 +142,10 @@ extension ObjectPublisher {
self.subscriber = nil
if Thread.isMainThread {
Internals.mainActorImmediate {
self.publisher.removeObserver(self)
}
else {
DispatchQueue.main.async {
self.publisher.removeObserver(self)
}
}
}
+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 callback: the closure to execute when changes occur
*/
@MainActor
public func addObserver<T: AnyObject>(
_ observer: T,
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 callback: the closure to execute when changes occur
*/
@MainActor
public func addObserver<T: AnyObject>(
_ observer: T,
notifyInitial: Bool = false,
@@ -145,6 +147,7 @@ public final class ObjectPublisher<O: DynamicObject>: ObjectRepresentation, Hash
- parameter observer: the object whose notifications will be unregistered
*/
@MainActor
public func removeObserver<T: AnyObject>(_ observer: T) {
Internals.assert(
+30 -10
View File
@@ -23,9 +23,9 @@
// SOFTWARE.
//
#if canImport(Combine) && canImport(SwiftUI)
#if canImport(Observation) && canImport(SwiftUI)
import Combine
import Observation
import SwiftUI
@@ -62,19 +62,22 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
- parameter objectPublisher: The `ObjectPublisher` that the `ObjectState` will observe changes for
*/
@MainActor
public init(_ objectPublisher: ObjectPublisher<O>?) {
self.observer = .init(objectPublisher: objectPublisher)
self._observer = .init(wrappedValue: .init(objectPublisher: objectPublisher))
}
// MARK: @propertyWrapper
@MainActor
public var wrappedValue: ObjectSnapshot<O>? {
return self.observer.item
}
@MainActor
public var projectedValue: ObjectPublisher<O>? {
return self.observer.objectPublisher
@@ -91,19 +94,36 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
// MARK: Private
@ObservedObject
@State
private var observer: Observer
// MARK: - Observer
private final class Observer: ObservableObject {
@MainActor
private final class Observer: Observation.Observable {
@Published
var item: ObjectSnapshot<O>?
private let registrar = ObservationRegistrar()
private var _item: ObjectSnapshot<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>?) {
guard
@@ -112,12 +132,12 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
else {
self.objectPublisher = nil
self.item = nil
self._item = nil
return
}
self.objectPublisher = objectPublisher
self.item = objectPublisher.snapshot
self._item = objectPublisher.snapshot
objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
@@ -129,7 +149,7 @@ public struct ObjectState<O: DynamicObject>: DynamicProperty {
}
}
deinit {
isolated deinit {
self.objectPublisher?.removeObserver(self)
}
+11 -5
View File
@@ -36,7 +36,8 @@ extension Progress {
- parameter closure: the closure to execute on progress change
*/
@nonobjc
public func setProgressHandler(_ closure: ((_ progress: Progress) -> Void)?) {
@MainActor
public func setProgressHandler(_ closure: (@MainActor (_ progress: Progress) -> Void)?) {
self.progressObserver.progressHandler = closure
}
@@ -46,15 +47,19 @@ extension Progress {
private struct PropertyKeys {
static var progressObserver: Void?
static nonisolated(unsafe) var progressObserver: Void?
}
@nonobjc
@MainActor
private var progressObserver: ProgressObserver {
get {
let object: ProgressObserver? = Internals.getAssociatedObjectForKey(&PropertyKeys.progressObserver, inObject: self)
let object: ProgressObserver? = Internals.getAssociatedObjectForKey(
&PropertyKeys.progressObserver,
inObject: self
)
if let observer = object {
return observer
@@ -76,11 +81,12 @@ extension Progress {
// MARK: - ProgressObserver
@objc
private final class ProgressObserver: NSObject {
private final class ProgressObserver: NSObject, @unchecked Sendable {
private unowned let progress: Progress
fileprivate var progressHandler: ((_ progress: Progress) -> Void)? {
@MainActor
fileprivate var progressHandler: (@MainActor (_ progress: Progress) -> Void)? {
didSet {
+5 -2
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.
*/
public final class SQLiteStore: LocalStorage {
public final class SQLiteStore: LocalStorage, @unchecked Sendable {
/**
Initializes an SQLite store interface from the given SQLite file URL. When this instance is passed to the `DataStack`'s `addStorage()` methods, a new SQLite file will be created if it does not exist.
@@ -165,10 +165,13 @@ public final class SQLiteStore: LocalStorage {
[NSSQLitePragmasOption: ["journal_mode": "WAL"]]
```
*/
public let storeOptions: [AnyHashable: Any]? = [
public var storeOptions: [AnyHashable: Any]? {
return [
NSSQLitePragmasOption: ["journal_mode": "WAL"],
NSBinaryStoreInsecureDecodingCompatibilityOption: true
]
}
/**
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.
*/
public protocol StorageInterface: AnyObject {
public protocol StorageInterface: AnyObject, Sendable {
/**
The string identifier for the `NSPersistentStore`'s `type` property. This is the same string CoreStore will use to create the `NSPersistentStore` from the `NSPersistentStoreCoordinator`'s `addPersistentStoreWithType(...)` method.
@@ -68,7 +68,7 @@ public protocol StorageInterface: AnyObject {
/**
The `LocalStorageOptions` provides settings that tells the `DataStack` how to setup the persistent store for `LocalStorage` implementers.
*/
public struct LocalStorageOptions: OptionSet, ExpressibleByNilLiteral {
public struct LocalStorageOptions: OptionSet, ExpressibleByNilLiteral, Sendable {
/**
Tells the `DataStack` that the store should not be migrated or recreated, and should simply fail on model mismatch
+1 -1
View File
@@ -164,7 +164,7 @@ public final class SynchronousDataTransaction: BaseDataTransaction {
internal init(
mainContext: NSManagedObjectContext,
queue: DispatchQueue,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
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.
*/
public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ fetchClauses: FetchClause...
) {
@@ -135,7 +135,7 @@ extension UnsafeDataTransaction {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ fetchClauses: [FetchClause]
) {
@@ -174,7 +174,7 @@ extension UnsafeDataTransaction {
- parameter clauseChain: a `FetchChainableBuilderType` built from a chain of clauses
*/
public func monitorList<B: FetchChainableBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ clauseChain: B
) {
@@ -265,7 +265,7 @@ extension UnsafeDataTransaction {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ sectionBy: SectionBy<O>,
_ fetchClauses: FetchClause...
@@ -283,7 +283,7 @@ extension UnsafeDataTransaction {
- parameter fetchClauses: a series of `FetchClause` instances for fetching the object list. Accepts `Where`, `OrderBy`, and `Tweak` clauses.
*/
public func monitorSectionedList<O>(
createAsynchronously: @escaping (ListMonitor<O>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<O>) -> Void,
_ from: From<O>,
_ sectionBy: SectionBy<O>,
_ fetchClauses: [FetchClause]
@@ -323,7 +323,7 @@ extension UnsafeDataTransaction {
- parameter clauseChain: a `SectionMonitorBuilderType` built from a chain of clauses
*/
public func monitorSectionedList<B: SectionMonitorBuilderType>(
createAsynchronously: @escaping (ListMonitor<B.ObjectType>) -> Void,
createAsynchronously: @escaping @Sendable (ListMonitor<B.ObjectType>) -> Void,
_ 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.
*/
public final class UnsafeDataTransaction: BaseDataTransaction {
public final class UnsafeDataTransaction: BaseDataTransaction, @unchecked Sendable {
// MARK: -
@@ -42,7 +42,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
- parameter completion: the block executed after the save completes. Success or failure is reported by the optional `error` argument of the block.
*/
public func commit(
_ completion: @escaping (_ error: CoreStoreError?) -> Void
_ completion: @escaping @Sendable (_ error: CoreStoreError?) -> Void
) {
self.context.saveAsynchronously(
@@ -142,7 +142,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
*/
public func beginUnsafe(
supportsUndo: Bool = false,
sourceIdentifier: Any? = nil
sourceIdentifier: (any Sendable)? = nil
) -> UnsafeDataTransaction {
return UnsafeDataTransaction(
@@ -160,7 +160,7 @@ public final class UnsafeDataTransaction: BaseDataTransaction {
mainContext: NSManagedObjectContext,
queue: DispatchQueue,
supportsUndo: Bool,
sourceIdentifier: Any?
sourceIdentifier: (any Sendable)?
) {
super.init(