swift - ModelActor causes Hangs and many Fetches - Stack Overflow

I'm developing an iOS app with SwiftData and modern Swift concurrency using ModelActors. In my app

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

相关推荐

  • swift - ModelActor causes Hangs and many Fetches - Stack Overflow

    I'm developing an iOS app with SwiftData and modern Swift concurrency using ModelActors. In my app

    1天前
    80

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信