Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

179
Views
¿Cómo ejecutar una tarea vinculada a la CPU en segundo plano usando Swift Concurrency sin bloquear las actualizaciones de la interfaz de usuario?

Tengo un ObservableObject que puede hacer un trabajo pesado vinculado a la CPU:

 import Foundation import SwiftUI @MainActor final class Controller: ObservableObject { @Published private(set) var isComputing: Bool = false func compute() { if isComputing { return } Task { heavyWork() } } func heavyWork() { isComputing = true sleep(5) isComputing = false } }

Uso una Task para hacer el cálculo en segundo plano usando las nuevas funciones de concurrencia. Esto requiere el uso del atributo @MainActor para garantizar que todas las actualizaciones de la interfaz de usuario (aquí vinculadas a la propiedad isComputing ) se ejecuten en el actor principal.

Luego tengo la siguiente vista que muestra un contador y un botón para iniciar el cálculo:

 struct ContentView: View { @StateObject private var controller: Controller @State private var counter: Int = 0 init() { _controller = StateObject(wrappedValue: Controller()) } var body: some View { VStack { Text("Timer: \(counter)") Button(controller.isComputing ? "Computing..." : "Compute") { controller.compute() } .disabled(controller.isComputing) } .frame(width: 300, height: 200) .task { for _ in 0... { try? await Task.sleep(nanoseconds: 1_000_000_000) counter += 1 } } } }

El problema es que el cálculo parece bloquear toda la interfaz de usuario: el contador se congela.

¿Por qué se congela la interfaz de usuario y cómo implementar .compute() de tal manera que no bloquee las actualizaciones de la interfaz de usuario?


lo que probé

  • Hacer heavyWork y el método asíncrono y la dispersión await Task.yield() cada vez que se actualiza una propiedad publicada parece funcionar, pero esto es engorroso y propenso a errores. Además, permite algunas actualizaciones de la interfaz de usuario, pero no entre llamadas posteriores Task.yield() .
  • La eliminación del atributo @MainActor parece funcionar si ignoramos las advertencias moradas que indican que las actualizaciones de la interfaz de usuario deben realizarse en el actor principal (aunque no es una solución válida).

Editar 1

Gracias a la respuesta propuesta por @Bradley, llegué a esta solución que funciona como se esperaba (y está muy cerca de la forma habitual de DispatchQueue ):

 @MainActor final class Controller: ObservableObject { @Published private(set) var isComputing: Bool = false func compute() { if isComputing { return } Task.detached { await MainActor.run { self.isComputing = true } await self.heavyWork() await MainActor.run { self.isComputing = false } } } nonisolated func heavyWork() async { sleep(5) } }
over 4 years ago · Santiago Trujillo
1 answers
Answer question

0

El problema es que heavyWork hereda el aislamiento de MainActor de Controller , lo que significa que el trabajo se realizará en el subproceso principal. Esto se debe a que anotó Controller con @MainActor , por lo que todas las propiedades y métodos de esta clase heredarán, de manera predeterminada, el aislamiento de MainActor . Pero también cuando crea una nueva Task { } , esta hereda la prioridad actual de la tarea actual (es decir, el MainActor ) y el aislamiento del actor, lo que obliga a heavyWork a ejecutarse en el actor/hilo principal.

Necesitamos asegurarnos de (1) que ejecutamos el trabajo pesado con una prioridad más baja, por lo que es menos probable que el sistema lo programe en el subproceso de la interfaz de usuario. Esto también debe ser una tarea separada, lo que evitará la herencia predeterminada que realiza la Task { } . Podemos hacer esto usando Task.detached con una prioridad baja (como .background o .low ).

Luego (2), nos aseguramos de que heavyWork esté nonisolated , por lo que no heredará el contexto @MainActor del Controller . Sin embargo, esto significa que ya no puede mutar ningún estado en el Controller directamente. Todavía puede leer/modificar el estado del actor si await accesos para leer operaciones o await llamadas a otros métodos en el actor que modifican el estado. En este caso, necesitaría hacer que heavyWork una función async .

Luego (3), esperamos a que se calcule el value utilizando la propiedad de valor devuelta por el "manejador de tareas". Esto nos permite acceder a un valor de retorno de la función heavyWork , si corresponde.

 @MainActor final class Controller: ObservableObject { @Published private(set) var isComputing: Bool = false func compute() { if isComputing { return } Task { isComputing = true // (1) run detached at a non-UI priority let work = Task.detached(priority: .low) { self.heavyWork() } // (3) non-blocking wait for value let result = await work.value print("result on main thread", result) isComputing = false } } // (2) will not inherit @MainActor isolation nonisolated func heavyWork() -> String { sleep(5) return "result of heavy work" } }
over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!