diff --git a/CoreStoreTests/ConvenienceTests.swift b/CoreStoreTests/ConvenienceTests.swift index 49590d3..0080a6f 100644 --- a/CoreStoreTests/ConvenienceTests.swift +++ b/CoreStoreTests/ConvenienceTests.swift @@ -33,7 +33,42 @@ import CoreStore // MARK: - ConvenienceTests class ConvenienceTests: BaseTestCase { - + + @objc + dynamic func test_ThatUserInfoLazyInitializer_CanReadAnotherKey() { + + let keys = UnsafeMutableRawPointer.allocate(byteCount: 2, alignment: 1) + defer { keys.deallocate() } + + let userInfo = UserInfo() + let value = userInfo[UnsafeRawPointer(keys), lazyInit: { + + let nested = userInfo[ + UnsafeRawPointer(keys.advanced(by: 1)), + lazyInit: { "nested" } + ] as! String + return "outer \(nested)" + }] + XCTAssertEqual(value as? String, "outer nested") + } + + func test_ThatUndoManagerIsReadyWhenUnsafeTransactionStartsOffMain() async { + + let hasUndoManager = await Task.detached { + + let parent = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + let transaction = UnsafeDataTransaction( + mainContext: parent, + queue: DispatchQueue(label: "com.corestore.tests.unsafeTransactionQueue"), + supportsUndo: true, + sourceIdentifier: nil + ) + let context = transaction.context + return context.performAndWait { context.undoManager != nil } + }.value + XCTAssertTrue(hasUndoManager) + } + @objc @MainActor dynamic func test_ThatDataStacks_CanCreateFetchedResultsControllers() { diff --git a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift index 99cf696..afac7f1 100644 --- a/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Classic/ColorsDemo/Classic.ColorsDemo.MainView.swift @@ -52,21 +52,22 @@ extension Classic.ColorsDemo { .toolbar { ToolbarItemGroup(placement: .topBarLeading) { EditButton() - Button("Clear") { + .labelStyle(.iconOnly) + Button("Clear", systemImage: "trash") { self.clearColors() } } ToolbarItemGroup(placement: .topBarTrailing) { - Button(self.filter.rawValue) { + Button(self.filter.rawValue, systemImage: "line.3.horizontal.decrease.circle") { self.changeFilter() } - Button("Shuffle") { + Button("Shuffle", systemImage: "shuffle") { self.shuffleColors() } - Button("Add") { + Button("Add", systemImage: "plus") { self.addColor() } diff --git a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift index 5b0b5ee..eee5c87 100644 --- a/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/ColorsDemo/Modern.ColorsDemo.MainView.swift @@ -55,7 +55,8 @@ extension Modern.ColorsDemo { ToolbarItemGroup(placement: .topBarLeading) { EditButton() - Button("Clear") { + .labelStyle(.iconOnly) + Button("Clear", systemImage: "trash") { self.clearColors() } @@ -63,15 +64,15 @@ extension Modern.ColorsDemo { ToolbarItemGroup(placement: .topBarTrailing) { - Button(self.filter.rawValue) { + Button(self.filter.rawValue, systemImage: "line.3.horizontal.decrease.circle") { self.changeFilter() } - Button("Shuffle") { + Button("Shuffle", systemImage: "shuffle") { self.shuffleColors() } - Button("Add") { + Button("Add", systemImage: "plus") { self.addColor() } diff --git a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift index de920bf..ee539d3 100644 --- a/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift +++ b/Demo/Sources/Demos/Modern/PlacemarksDemo/Modern.PlacemarksDemo.MainView.swift @@ -109,7 +109,12 @@ extension Modern.PlacemarksDemo { return } let geocoded = await self.geocoder.geocode(place: place) - guard self.place?.persistentID() == place.persistentID() else { + guard + !Task.isCancelled, + self.place?.persistentID() == place.persistentID(), + self.place?.$latitude == place.$latitude, + self.place?.$longitude == place.$longitude + else { return } @@ -126,7 +131,7 @@ extension Modern.PlacemarksDemo { .navigationTitle("Placemarks") .toolbar { - Button("Random") { + Button("Random", systemImage: "dice") { self.demoSynchronousTransaction() } diff --git a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift index a696f4d..97f48e4 100644 --- a/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift +++ b/Demo/Sources/Demos/Modern/PokedexDemo/Modern.PokedexDemo.Service.swift @@ -195,7 +195,7 @@ extension Modern.PokedexDemo { if let species = details.$species?.snapshot { self.fetchFormsIfNeeded( - key: String(species.$id), + key: pokedexEntry.$id, detailsPersistentID: details.persistentID(), species: species ) @@ -219,7 +219,6 @@ extension Modern.PokedexDemo { self.detailTasks.removeValue(forKey: key) } await self.fetchSpecies( - key: key, detailsPersistentID: detailsPersistentID, speciesURL: speciesURL ) @@ -288,7 +287,6 @@ extension Modern.PokedexDemo { } private func fetchSpecies( - key: String, detailsPersistentID: PersistentID, speciesURL: URL ) async { diff --git a/Sources/BaseDataTransaction.swift b/Sources/BaseDataTransaction.swift index 18168f0..c5f6c87 100644 --- a/Sources/BaseDataTransaction.swift +++ b/Sources/BaseDataTransaction.swift @@ -554,11 +554,18 @@ public /*abstract*/ class BaseDataTransaction { context.undoManager = nil } else if context.undoManager == nil { - - Internals.mainActorImmediate { - - context.undoManager = UndoManager() + let undoManager: UndoManager + if Thread.isMainThread { + + undoManager = MainActor.assumeIsolated { UndoManager() } } + else { + + undoManager = DispatchQueue.main.sync { + MainActor.assumeIsolated { UndoManager() } + } + } + context.undoManager = undoManager } } diff --git a/Sources/ListSnapshot.SectionInfo.swift b/Sources/ListSnapshot.SectionInfo.swift index a508616..f3976e1 100644 --- a/Sources/ListSnapshot.SectionInfo.swift +++ b/Sources/ListSnapshot.SectionInfo.swift @@ -38,7 +38,7 @@ extension ListSnapshot { public let sectionID: SectionID - public let itemIDs: [NSManagedObjectID] + public let itemIDs: [ItemID] // MARK: RandomAccessCollection @@ -79,7 +79,7 @@ extension ListSnapshot { public subscript(position: Int) -> ObjectPublisher { let itemID = self.itemIDs[position] - return self.context.objectPublisher(managedObjectID: itemID) + return self.context.objectPublisher(managedObjectID: itemID.managedObjectID) } public func index(_ i: Index, offsetBy distance: Int) -> Index { @@ -100,7 +100,7 @@ extension ListSnapshot { public subscript(bounds: Range) -> ArraySlice { let itemIDs = self.itemIDs[bounds] - return ArraySlice(itemIDs.map(self.context.objectPublisher(managedObjectID:))) + return ArraySlice(itemIDs.map({ self.context.objectPublisher(managedObjectID: $0.managedObjectID) })) } @@ -126,7 +126,6 @@ extension ListSnapshot { } self.sectionID = sectionID self.itemIDs = listSnapshot.itemIDs(inSectionWithID: sectionID) - .map({ $0.managedObjectID }) self.context = context } } diff --git a/Sources/UserInfo.swift b/Sources/UserInfo.swift index 05dfc57..34db8de 100644 --- a/Sources/UserInfo.swift +++ b/Sources/UserInfo.swift @@ -38,7 +38,7 @@ import Foundation ``` - Important: Do not use this class to store thread-sensitive data. */ -public final class UserInfo: Sendable { +public final class UserInfo: @unchecked Sendable { /** Allows external libraries to store custom data. App code should rarely have a need for this. @@ -55,17 +55,15 @@ public final class UserInfo: Sendable { get { - return self.data.withLock { (info: inout _) in - - return info[key] - } + self.lock.lock() + defer { self.lock.unlock() } + return self.data[key] } set { - return self.data.withLock { (info: inout _) in - - info[key] = newValue - } + self.lock.lock() + defer { self.lock.unlock() } + self.data[key] = newValue } } @@ -84,16 +82,15 @@ public final class UserInfo: Sendable { */ public subscript(key: UnsafeRawPointer, lazyInit closure: () -> any Sendable) -> any Sendable { - return self.data.withLock { (info: inout _) in - - if let value = info[key] { - - return value - } - let value = closure() - info[key] = value + self.lock.lock() + defer { self.lock.unlock() } + if let value = self.data[key] { + return value } + let value = closure() + self.data[key] = value + return value } @@ -104,5 +101,6 @@ public final class UserInfo: Sendable { // MARK: Private - private let data: Internals.Mutex<[UnsafeRawPointer: any Sendable]> = .init([:]) + private var data: [UnsafeRawPointer: any Sendable] = [:] + private let lock = NSRecursiveLock() }