@@ -58,11 +58,159 @@ extension NSPersistentContainer: ModelStorage {
58
58
}
59
59
}
60
60
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
+
61
206
@available ( macOS 12 , iOS 15 , watchOS 8 , tvOS 15 , * )
62
207
public extension NSPersistentContainer {
63
208
64
209
func loadPersistentStores( ) -> AsyncThrowingStream < NSPersistentStoreDescription , Error > {
65
210
assert ( self . persistentStoreDescriptions. isEmpty == false )
211
+ for store in persistentStoreDescriptions {
212
+ store. shouldAddStoreAsynchronously = true
213
+ }
66
214
return AsyncThrowingStream < NSPersistentStoreDescription , Error > . init ( NSPersistentStoreDescription . self, bufferingPolicy: . unbounded, { continuation in
67
215
self . loadPersistentStores { [ unowned self] ( description, error) in
68
216
continuation. yield ( description)
@@ -76,6 +224,22 @@ public extension NSPersistentContainer {
76
224
}
77
225
} )
78
226
}
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
+ }
79
243
}
80
244
81
245
#endif
0 commit comments