Skip to content

Commit feacc6a

Browse files
committed
Add PersistentContainerStorage
1 parent bd3c583 commit feacc6a

File tree

2 files changed

+169
-1
lines changed

2 files changed

+169
-1
lines changed

Sources/CoreDataModel/NSManagedObjectContext.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ extension NSManagedObjectContext: ModelStorage {
5959
}
6060
}
6161

62+
// MARK: - ManagedObjectViewContext
63+
6264
@MainActor
6365
public final class ManagedObjectViewContext: ViewContext, ObservableObject {
6466

@@ -69,7 +71,7 @@ public final class ManagedObjectViewContext: ViewContext, ObservableObject {
6971
assert(context.concurrencyType == .mainQueueConcurrencyType)
7072
}
7173

72-
public init(persistentContainer: NSPersistentContainer) {
74+
public nonisolated init(persistentContainer: NSPersistentContainer) {
7375
self.context = persistentContainer.viewContext
7476
}
7577

@@ -94,6 +96,8 @@ public final class ManagedObjectViewContext: ViewContext, ObservableObject {
9496
}
9597
}
9698

99+
// MARK: - Extensions
100+
97101
internal extension NSManagedObjectContext {
98102

99103
func fetchObjects(_ fetchRequest: FetchRequest) throws -> [NSManagedObject] {

Sources/CoreDataModel/NSPersistentContainer.swift

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,159 @@ extension NSPersistentContainer: ModelStorage {
5858
}
5959
}
6060

61+
// MARK: - PersistentContainerStorage
62+
63+
/// Concurrency safe persistent storage
64+
///
65+
/// Write will happen on a single isolation context using a single background managed object context
66+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
67+
public actor PersistentContainerStorage: ModelStorage, ObservableObject {
68+
69+
// MARK: Initialization
70+
71+
public init(name: String, model: Model) {
72+
let managedObjectModel = NSManagedObjectModel(model: model)
73+
let persistentContainer = NSPersistentContainer(
74+
name: name,
75+
managedObjectModel: managedObjectModel
76+
)
77+
self.persistentContainer = persistentContainer
78+
self._viewContext = ManagedObjectViewContext(persistentContainer: persistentContainer)
79+
}
80+
81+
// MARK: Properties
82+
83+
internal let persistentContainer: NSPersistentContainer
84+
85+
internal let _viewContext: ManagedObjectViewContext
86+
87+
@MainActor
88+
public var viewContext: ManagedObjectViewContext {
89+
get throws {
90+
try loadStores()
91+
return _viewContext
92+
}
93+
}
94+
95+
private nonisolated(unsafe) var state: State = .notLoaded
96+
97+
private lazy var backgroundContext = persistentContainer.newBackgroundContext()
98+
99+
// MARK: Methods
100+
101+
public func fetch(_ entity: EntityName, for id: ObjectID) async throws -> ModelData? {
102+
return try await performBackgroundTask { (context, model) in
103+
try context.fetch(entity, for: id)
104+
}
105+
}
106+
107+
public func fetch(_ fetchRequest: FetchRequest) async throws -> [ModelData] {
108+
try await performBackgroundTask { (context, model) in
109+
try context.fetch(fetchRequest)
110+
}
111+
}
112+
113+
public func count(_ fetchRequest: FetchRequest) async throws -> UInt {
114+
try await performBackgroundTask { (context, model) in
115+
try context.count(fetchRequest)
116+
}
117+
}
118+
119+
public func insert(_ value: ModelData) async throws {
120+
try await performBackgroundTask { (context, model) in
121+
try context.insert(value, model: model)
122+
}
123+
}
124+
125+
public func insert(_ values: [ModelData]) async throws {
126+
try await performBackgroundTask { (context, model) in
127+
try context.insert(values, model: model)
128+
}
129+
}
130+
131+
public func delete(_ entity: EntityName, for id: ObjectID) async throws {
132+
try await performBackgroundTask { (context, model) in
133+
try context.delete(entity, for: id)
134+
}
135+
}
136+
137+
public func fetchID(_ fetchRequest: FetchRequest) async throws -> [ObjectID] {
138+
try await performBackgroundTask { (context, model) in
139+
try context.fetchID(fetchRequest)
140+
}
141+
}
142+
143+
private func performBackgroundTask<T>(
144+
schedule: NSManagedObjectContext.ScheduledTaskType = .immediate,
145+
_ task: @escaping (NSManagedObjectContext, NSManagedObjectModel) throws -> T
146+
) async throws -> T {
147+
try await loadStores()
148+
let model = persistentContainer.managedObjectModel
149+
let context = backgroundContext
150+
return try await context.perform(schedule: schedule) {
151+
try task(context, model)
152+
}
153+
}
154+
155+
@MainActor
156+
private func loadStores() async throws {
157+
// lazily load stores
158+
guard state == .notLoaded else {
159+
// already loaded or loading
160+
return
161+
}
162+
state = .loading
163+
do {
164+
for try await store in persistentContainer.loadPersistentStores() {
165+
// continue
166+
_ = store
167+
}
168+
}
169+
catch {
170+
state = .notLoaded
171+
throw error
172+
}
173+
state = .loaded
174+
}
175+
176+
private nonisolated func loadStores() throws {
177+
// lazily load stores
178+
guard state == .notLoaded else {
179+
// already loaded or loading
180+
return
181+
}
182+
state = .loading
183+
do {
184+
try persistentContainer.syncLoadPersistentStores()
185+
}
186+
catch {
187+
return
188+
}
189+
state = .loaded
190+
}
191+
}
192+
193+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
194+
internal extension PersistentContainerStorage {
195+
196+
enum State: Equatable, Hashable, Sendable {
197+
198+
case notLoaded
199+
case loading
200+
case loaded
201+
}
202+
}
203+
204+
// MARK: - Extensions
205+
61206
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
62207
public extension NSPersistentContainer {
63208

64209
func loadPersistentStores() -> AsyncThrowingStream<NSPersistentStoreDescription, Error> {
65210
assert(self.persistentStoreDescriptions.isEmpty == false)
211+
for store in persistentStoreDescriptions {
212+
store.shouldAddStoreAsynchronously = true
213+
}
66214
return AsyncThrowingStream<NSPersistentStoreDescription, Error>.init(NSPersistentStoreDescription.self, bufferingPolicy: .unbounded, { continuation in
67215
self.loadPersistentStores { [unowned self] (description, error) in
68216
continuation.yield(description)
@@ -76,6 +224,22 @@ public extension NSPersistentContainer {
76224
}
77225
})
78226
}
227+
228+
func syncLoadPersistentStores() throws {
229+
assert(self.persistentStoreDescriptions.isEmpty == false)
230+
for store in persistentStoreDescriptions {
231+
store.shouldAddStoreAsynchronously = false
232+
}
233+
var caughtError: Error?
234+
self.loadPersistentStores { (description, error) in
235+
if let error {
236+
caughtError = error
237+
}
238+
}
239+
if let error = caughtError {
240+
throw error
241+
}
242+
}
79243
}
80244

81245
#endif

0 commit comments

Comments
 (0)