DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Pasos para migrar a Swift 6 y verificación estricta de concurrencia

Comprensión durante la lectura

¿Por qué se recomienda comenzar la migración con un fragmento de código aislado?

Entre menos dependencias tenga un fragmento de código más fácil es hacer la migración. Que haya dependencias significa que aparecerán casos donde, luego de arreglar 50 errores, aparecen 80 más.

¿Qué ventaja ofrece actualizar las dependencias de terceros antes de habilitar el "strict concurrency checking"?

Puede ser que el código que planeo migrar dependa de otras bibliotecas de terceros. En este caso, vale la pena primero actualizar dichas dependencias, en caso de que estas estén al día con el uso de Swift Concurrency.

De hecho, si las dependencias de terceros ya introdujeron cambios para estar al día con strict concurrency checking, muy probablemente se le exigirá a mi código que introduzca algunos cambios incluso antes de habilitar el "strict concurrency checking".

¿Vale la pena usar @MainActor por defecto?

Si se está trabajando en una aplicación móvil, es buena idea activar @MainActor por defecto. Esto va a resolver muchas advertencias y errores.

En un proyecto de iOS buscar "Default Actor Isolation". En un paquete de SPM:

.target(
    name: "DefaultActorIsolationPackage",
    swiftSettings: [
        .defaultIsolation(MainActor.self)
    ]
)
Enter fullscreen mode Exit fullscreen mode

¿Qué diferencia hay entre los niveles Minimal, Targeted y Complete de strict concurrency checking?

  • Minimal Solo revisa código que tenga palabras clave de concurrencia explícita: async/await, @Sendable, @MainActor. Todo lo demás se ignora.
  • Targeted revisa todo tipo que conforma Sendable, aunque no se async directamente.
  • Complete revisa absolutamente todo el código base, incluso si no toca concurrencia. Este nivel pregunta: "¿Podría este código, algún día, cruzar un isolation domain".

Consideremos los siguientes escenarios:

class UserCache {
  var name: String = ""
  var age: Int = 0
}
func loadUser() {
  let cache = UserCache()
  cache.name = "Ana"
}
Enter fullscreen mode Exit fullscreen mode
  • Minimal: No hay warning porque no hay async, @Sendable ni @MainActor.
  • Targeted: No hay warning porque no conforma Sendable ni usa concurrencia.
  • Complete: warning - UserCache se un class con propiedades mutables y no es Sendable.
class UserCache {
  var name: String = ""
}
func doWork(completion: @escaping @Sendable (UserCache) -> Void) {
  let cache = UserCache()
  completion(cache) // captura UserCache dentro de @Sendable
}
Enter fullscreen mode Exit fullscreen mode
  • Minimal: warning - El parámetro es @Sendable, pero UserCache no conforma Sendable.
  • Targeted: warning - Igual que minimal.
  • Complete: error - Convierte en error el warning de Sendable.
struct Config: Sendable {
  var timeout: Int
  var logger: Logger // Logger es una class, no Sendable
}
class Logger {
  var prefix: String = ""
}
func run(config: Config) {
  // código síncrono, sin async
}
Enter fullscreen mode Exit fullscreen mode
  • Minimal: nada - run es síncrono, así que no valida nada.
  • Targeted: error - Config es Sendable, pero su propiedad logger no.
  • Complete: error - Igual que Targeted. Además, advierte que Logger no es Sendable y se puede usar en cualquier otro lado.

En SPM se activa así:

.target(
  name: "CoreExtensions",
  dependencies: ["Logging"],
  path: "CoreExtensions/Sources",
  swiftSettings: [
    /// Used to be like this in Xcode 14:
    SwiftSetting.unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]),
    /// Xcode 15, 16, 26, and up. Remove `=targeted` to use the default `complete`. Potentially isolate to a platform to further reduce scope.
    .enableExperimentalFeature("StrictConcurrency=targeted", .when(platforms: [.macOS]))
  ]
)
Enter fullscreen mode Exit fullscreen mode

¿En qué situación tendría sentido quedarse en un paso intermedio como agregar alternativas async en lugar de migrar por completo?

Puede ser que el fragmento de código aislado sobre el que estemos trabajando sea una dependencia de algún otro módulo. En este caso, se puede proveer una alternativa async/await del API que permita a otro desarrollador migrar los clientes a Swift Concurrency.

Al proveer una alternativa async/await se puede agregar un "Async wrapper" desde el menú "Refactor" y poner la siguiente etiqueta en el código que recibe closures:

@available(*, deprecated, renamed: "fetchImage(urlRequest:)", message: "Consider using the async/await alternative.")
Enter fullscreen mode Exit fullscreen mode

La etiqueta anterior va a levantar una advertencia en todos los puntos donde se use la versión con closures, lo que permitirá identificar con rapidez donde se debe reemplazar con la versión que usa async/await.

Usar un "Async wrapper" puede ser útil porque permite a los otros desarrolladores avanzar en la migración mientras yo me enfoco en construir la solución final de Swift Concurrency por detrás.

¿Por qué es útil conformar Sendable en los tipos de datos de un paquete?

Un paquete puede ser ampliamente usado por algún cliente. En este caso, conviene conformar Sendable para que se pueda usar con tranquilidad en lugares donde sí hay cambios de dominio de aislamiento.

¿Qué relación existe entre el swift-tools-version y el modo de lenguaje Swift 6?

swift-tools-version permite elegir la versión del lenguaje de un paquete (de SPM). Al elegir // swift-tools-version: 6.0 luego se puede poner en versión 5 a algunas dependencias específicas con .swiftLanguageMode(.v5).

.target(
    name: "CoreNetworking",
    swiftSettings: [
        .swiftLanguageMode(.v5)
    ]
)
Enter fullscreen mode Exit fullscreen mode

Recordar sin releer

Sin mirar el artículo, ¿puedes enumerar los 9 pasos de la migración en orden?

¿Cómo explicarías con tus propias palabras la diferencia entre .swiftLanguageMode(.v5) y cambiar el swift-tools-version a 6.0?

¿Qué hace exactamente el atributo @available(*, deprecated, renamed:) y por qué se prefiere sobre la versión sin deprecated?


Revisión y síntesis

¿Qué pasos del proceso considera el autor como opcionales y por qué?

¿De qué manera el concepto de Default Actor Isolation en Swift 6.2 cambia la estrategia de migración respecto a versiones anteriores?

Si tuvieras que migrar un proyecto con muchas dependencias entrelazadas, ¿qué ajustes harías al orden propuesto en el artículo?

¿Qué relación existe entre Approachable Concurrency y los "upcoming features" mencionados al final?


Bibliografía

Top comments (0)