I'm developing an iOS app with SwiftData and modern Swift concurrency using ModelActors. In my app I have two distinct model containers:
Main Container: Holds UI‑relevant models. Queue Container: Holds only pending tasks (i.e. “PendingTask” objects) that represent deferred updates.
I’ve created two separate actors that conform to the ModelActor protocol to isolate these concerns:
ApiQueue: Processes API calls and updates the UI container. TasksQueue: Manages the pending tasks (adding, fetching, removing, processing) using the queue container. The idea is that by keeping the queue container completely separate from the UI, changes (inserts, deletes, saves) on the queue context should not trigger any UI updates or merge events.
In one of my views:
// Save routine: creates main object and dependencies, then queues them for API processing.
private func saveMainObject() async {
isSaving = true
var queueObjects: [any DependencyResolvable] = []
// 1) Create main object and dependencies.
let mainObject = createLocalMainObject()
queueObjects.append(mainObject)
let history = createLocalHistory(for: mainObject)
queueObjects.append(history)
guard let group = createLocalGroup(for: mainObject) else {
isSaving = false
return
}
queueObjects.append(group)
// 2) Save in main container.
do {
try mainContext.save()
} catch {
print("Final save error: \(error)")
isSaving = false
return
}
// 3) Enqueue objects in TasksQueue.
if let queue = TasksQueue.shared {
for obj in queueObjects {
Task {
await queue.addTask(object: obj, actionType: "create")
}
}
}
isSaving = false
// Update UI...
}
And here’s the simplified version of our TasksQueue actor that manages the pending tasks using its own container (completely isolated from the UI):
public actor TasksQueue: ModelActor {
public static var shared: TasksQueue!
public nonisolated let modelContainer: ModelContainer
public nonisolated let modelExecutor: any ModelExecutor
public init(modelContainer: ModelContainer) {
self.modelContainer = modelContainer
let context = ModelContext(modelContainer)
self.modelExecutor = DefaultSerialModelExecutor(modelContext: context)
}
public static func initialize(queueContainer: ModelContainer) {
let tasksQueue = TasksQueue(modelContainer: queueContainer)
Self.shared = tasksQueue
Task.detached(priority: .background) {
await tasksQueue.processQueue()
}
}
internal func addTask<T: DependencyResolvable>(object: T, actionType: String) async {
guard let localId = object.localIdForQueue else {
print("TasksQueue: No local ID found – aborting.")
return
}
let task = PendingTask(
modelType: String(reflecting: type(of: object)),
localObjectId: localId,
actionType: actionType
)
do {
modelContext.insert(task)
try modelContext.save()
print("TasksQueue: New PendingTask saved: \(task)")
} catch {
print("TasksQueue: Error saving task: \(error)")
}
Task.detached(priority: .background) {
await self.processQueue()
}
}
public func fetchNextTask() async -> PendingTask? {
do {
let descriptor = FetchDescriptor<PendingTask>(
sortBy: [SortDescriptor(\.createdAt, order: .forward)]
)
return try modelContext.fetch(descriptor).first
} catch {
print("TasksQueue: Error fetching task: \(error)")
return nil
}
}
public func removeTask(_ task: PendingTask) async {
do {
modelContext.delete(task)
try modelContext.save()
print("TasksQueue: Task removed: \(task)")
} catch {
print("TasksQueue: Error removing task: \(error)")
}
}
public func processQueue() async {
guard !isProcessing else { return }
isProcessing = true
defer { isProcessing = false }
while let task = await fetchNextTask() {
let success = await handleTask(task)
if success {
await removeTask(task)
} else {
try? await Task.sleep(nanoseconds: 10 * 1_000_000_000)
}
}
}
public func handleTask(_ task: PendingTask) async -> Bool {
task.retryCount += 1
if task.retryCount > 3 {
// Removed error logging.
}
var result = false
switch task.actionType {
case "create":
switch task.modelType {
case let mt where mt.hasSuffix("MainObject"):
result = await ApiQueue.shared?.handleCreateMainObject(task.localObjectId) ?? false
default:
print("TasksQueue: Unknown model type (create): \(task.modelType)")
}
case "update":
switch task.modelType {
case let mt where mt.hasSuffix("MainObject"):
result = await ApiQueue.shared?.handleUpdateMainObject(task.localObjectId) ?? false
default:
print("TasksQueue: Unknown model type (update): \(task.modelType)")
}
case "delete":
switch task.modelType {
case let mt where mt.hasSuffix("MainObject"):
result = await ApiQueue.shared?.handleDeleteMainObject(task.localObjectId) ?? false
default:
print("TasksQueue: Unknown model type (delete): \(task.modelType)")
}
default:
print("TasksQueue: Unknown action type: \(task.actionType)")
}
return result
}
}
And here the ApiQueue, which handles the API Calls and works on the main container:
public func handleCreateMainObject(_ localId: Int) async -> Bool {
do {
// Fetch local MainObject by temporary ID.
guard let mainObject = try fetchMainObject(by: localId) else {
print("handleCreateMainObject: No MainObject found with ID \(localId)")
return false
}
// Check if dependencies are resolved.
guard mainObject.areDependenciesResolved() else {
print("handleCreateMainObject: Dependencies not resolved for MainObject with ID \(localId)")
return false
}
// Upload image if needed.
var updatedImageKey = mainObject.imageKey ?? ""
if updatedImageKey.isEmpty, let imageData = mainObject.imageData {
let uuid = UUID().uuidString
let newPath = "\(await authenticationService.privateDataPath)/\(uuid)"
let uploadSuccess = await storageService.uploadWithResult(imageData, path: newPath)
if !uploadSuccess {
print("handleCreateMainObject: Image upload failed for MainObject ID \(localId); retrying...")
try? await Task.sleep(nanoseconds: 5_000_000_000) // wait 5 seconds
let retrySuccess = await storageService.uploadWithResult(imageData, path: newPath)
if !retrySuccess {
print("handleCreateMainObject: Retry image upload failed. Aborting creation.")
return false
}
}
updatedImageKey = newPath
}
mainObject.imageKey = updatedImageKey
// Create MainObject remotely via API.
guard let newMainObjectId = await apiService.createMainObject(mainObject: mainObject) else {
print("handleCreateMainObject: API call failed for MainObject ID \(localId)")
return false
}
// Update MainObject with new ID and creation date.
let oldTempId = mainObject.id
mainObject.id = newMainObjectId
mainObject.createdAt = Date()
// Update related histories with new MainObject ID.
if let relatedHistories = try? fetchHistories(forMainObjectId: oldTempId) {
for history in relatedHistories {
history.mainObjectId = newMainObjectId
}
}
// Save changes.
try saveContext()
print("handleCreateMainObject: MainObject updated successfully with new ID \(newMainObjectId)")
return true
} catch {
print("handleCreateMainObject: Error processing MainObject with ID \(localId): \(error)")
return false
}
}
My situation: Even though we use two separate model containers (one for UI data and one solely for background pending tasks), we still see that each removeTask operation in the TasksQueue triggers many fetch operations and some of them are executed on the main thread. This is unexpected because the queue container is not bound to the UI at all.
My question is: Why do these background operations (especially removeTask and fetchNextTask) still run on the main thread and cause long delays—even though they use a separate model container that should not trigger UI updates? What changes can I make to ensure that these tasks execute entirely in the background, without impacting the UI?
Any insights or suggestions to eliminate these delays are appreciated!
EDIT: Here are the Configurations for the two containers.
private let mainModelContainer: ModelContainer = {
do {
return try ModelContainer(
for: LocalUser.self,
...
migrationPlan: nil,
configurations: ModelConfiguration("MainStore")
)
} catch {
fatalError("Could not create main ModelContainer: \(error)")
}
}()
private let queueModelContainer: ModelContainer = {
do {
return try ModelContainer(
for: PendingApiTask.self,
migrationPlan: nil,
configurations: ModelConfiguration("QueueStore")
)
} catch {
fatalError("Could not create Queue ModelContainer: \(error)")
}
}()
init() {
// Initialize ApiQueue (processes API calls and UI data.)
ApiQueue.initialize(
mainContainer: mainModelContainer,
apiService: apiService,
storageService: storageService,
authenticationService: authenticationService
)
// Initialize TasksQueue (processes PendingApiTasks)
TasksQueue.initialize(queueContainer: queueModelContainer)
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744926716a4601480.html
评论列表(0条)