Escribir [weak self] suele transmitir una intención muy clara: el bloque no debe prolongar la vida del objeto. Sin embargo, cuando esa captura aparece dentro de otro bloque, el resultado puede ser justo el contrario. El bloque interior utiliza una captura de referencia débil, pero el exterior puede haber capturado self fuertemente para poder crearla.
Swift 6.4 incorpora un diagnóstico específico para este caso. Al compilar con Xcode 27, el aviso indica que la propiedad de la captura débil no coincide con la referencia fuerte capturada implícitamente en el ámbito exterior:
'weak' ownership of capture 'self' differs from implicitly-captured
strong reference in outer scope
No se trata de una nueva regla de ARC ni de un cambio de comportamiento en tiempo de ejecución. El código ya tenía esa semántica en versiones anteriores de Swift; la diferencia es que ahora el compilador hace visible una relación de propiedad que resultaba muy fácil pasar por alto.
Cómo puede una captura weak provocar una captura fuerte
Los bloques capturan por defecto las referencias que necesitan con propiedad fuerte. Una lista de captura como [weak self] modifica esa propiedad para el bloque en el que está escrita, pero no reescribe automáticamente todos los ámbitos que lo envuelven.
Imaginemos un controlador que contiene un observador de regiones. Dicho monitor almacena un bloque que solicita una previsión meteorológica y, cuando llega la respuesta, actualiza el controlador:
final class ForecastController {
private let monitor: RegionMonitor
private let service: ForecastService
private(set) var headline = ""
init(monitor: RegionMonitor, service: ForecastService) {
self.monitor = monitor
self.service = service
}
func start() {
monitor.onRegionChange = { [service] region in
service.fetchForecast(for: region) { [weak self] forecast in
self?.headline = forecast.summary
}
}
}
deinit {
print("ForecastController liberado")
}
}
A primera vista, self solo aparece junto a weak. Aun así, para construir la captura débil del bloque interior, el bloque exterior necesita acceder al self original. Como no declara qué propiedad quiere utilizar, Swift hace una captura de forma fuerte implícitamente.
El ciclo se forma antes incluso de que termine la petición:
ForecastControllerconservamonitor.monitorconservaonRegionChange.- El bloque exterior conserva implícitamente
ForecastController.
La captura débil del bloque interior no puede romper ese ciclo porque la referencia fuerte ya existe un nivel por encima. Por eso deinit no se ejecutará aunque desaparezcan las demás referencias al controlador.
Solución 1: mover la captura débil al bloque exterior
Si el bloque exterior no necesita mantener vivo al controlador, la corrección más sencilla consiste en declarar allí [weak self]:
func start() {
monitor.onRegionChange = { [weak self, service] region in
service.fetchForecast(for: region) { forecast in
self?.headline = forecast.summary
}
}
}
Ahora el observador conserva el bloque, pero el bloque solo mantiene una referencia débil al controlador. El bloque interior captura esa referencia procedente del ámbito exterior, no una referencia fuerte nueva al objeto.
Capturar service por separado también hace explícito que la operación puede continuar aunque el controlador ya no exista. El resultado podrá llegar, pero la actualización de headline simplemente se omitirá si self ya es nil.
Solución 2: usar una captura débil en ambos niveles
En ocasiones el bloque exterior sí necesita utilizar varias propiedades o ejecutar lógica del controlador. Se puede convertir temporalmente la referencia débil en una fuerte durante esa ejecución y volver a capturarla débilmente para el trabajo interior:
func start() {
monitor.onRegionChange = { [weak self] region in
guard let self else { return }
self.recordRegionChange(region)
self.service.fetchForecast(for: region) { [weak self] forecast in
self?.headline = forecast.summary
}
}
}
La primera captura evita el ciclo entre el controlador y el monitor. guard let self mantiene el controlador vivo únicamente mientras se atiende el cambio de región. La segunda captura impide que la petición prolongue su vida hasta que el servidor responda.
Este patrón también aclara dos decisiones distintas: el controlador debe existir para iniciar la operación, pero no necesita seguir existiendo para que la respuesta pueda completarse.
Solución 3: declarar una captura fuerte intencionada
Una captura fuerte no siempre implica un ciclo de retención. Si el bloque exterior pertenece a un objeto independiente y tiene una vida breve, mantener self hasta que termine puede ser exactamente lo que se busca.
func refreshInBackground() {
workerQueue.async { [self] in
let region = selectedRegion
service.fetchForecast(for: region) { [weak self] forecast in
self?.headline = forecast.summary
}
}
}
La lista [self] informa al compilador y a quien lea el código de que la captura fuerte exterior es deliberada. La cola libera ese bloque después de ejecutarlo, mientras que el bloque de respuesta no obliga al controlador a sobrevivir hasta el final de la petición.
Esta opción exige revisar el grafo de propiedad real. Si workerQueue o cualquier intermediario termina siendo retenido por self y conserva el bloque indefinidamente, la captura fuerte vuelve a ser problemática. El diagnóstico señala una posible incoherencia; no puede decidir por sí solo cuál debe ser el ciclo de vida de la aplicación.
Silenciar el aviso no siempre corrige el problema
La documentación del diagnóstico contempla una forma explícita de conservar la semántica anterior:
performWork {
requestValue { [weak self = self] value in
self?.consume(value)
}
}
Escribir [weak self = self] indica que la asignación se ha hecho conscientemente y silencia el aviso. Sin embargo, el bloque exterior sigue necesitando capturar fuertemente el self de la derecha. Esta sintaxis puede ser una vía de escape cuando se ha demostrado que no existe un ciclo, pero no debe utilizarse como arreglo automático para dejar la compilación sin advertencias.
En la mayoría de los casos resulta más legible declarar la decisión en el bloque exterior: [weak self] si no debe conservar el objeto o [self] si la captura fuerte es segura e intencionada.
El aviso también importa fuera de self
Aunque self sea el caso más habitual, el problema afecta a cualquier referencia fuerte que se vuelva a capturar como weak o unowned dentro de un bloque anidado. El compilador analiza la propiedad de la referencia, no su nombre.
unowned merece especial cuidado. No retiene el objeto y evita la opcionalidad, pero presupone que seguirá vivo cada vez que se acceda a él. Cambiar una captura a unowned solo para eliminar una advertencia puede transformar una posible fuga en un fallo durante la ejecución, el correspondiente crash.
El diagnóstico se centra además en bloques exteriores que pueden escapar. Un bloque que no escapa termina antes de que la función retorne y no queda almacenado para formar una relación duradera. Cuando el bloque sí escapa —porque se guarda como propiedad, se entrega a una API asíncrona o se conserva como observador— su lista de captura pasa a formar parte del diseño de memoria de la aplicación.
Cómo revisar el código al adoptar Swift 6.4
Ante esta advertencia conviene evitar la corrección mecánica y seguir la traza de las referencias. Hay que identificar quién conserva el bloque exterior, durante cuánto tiempo y si existe una ruta de vuelta hacia el objeto capturado.
Si el objeto es propietario directo o indirecto del bloque, lo normal será hacer débil la captura exterior. Si el bloque solo realiza una operación finita y lo conserva una entidad independiente, una captura fuerte explícita puede ser correcta. Cuando cada nivel tiene una duración diferente, declarar capturas débiles en ambos bloques permite expresar esa diferencia sin prolongar accidentalmente el ciclo de vida.
El Memory Graph Debugger de Xcode ayuda a confirmar el resultado, y un mensaje temporal en deinit continúa siendo una comprobación sencilla durante el desarrollo. El nuevo aviso de Swift 6.4 añade una defensa a priori de ambas: obliga a que una decisión de propiedad que antes era implícita quede escrita y pueda revisarse junto al resto del código.
Conclusión
[weak self] solo describe la relación de propiedad del bloque en el que aparece. En una estructura anidada, cada bloque tiene su propia captura y su propio ciclo de vida. La advertencia de Swift 6.4 no cambia ARC, pero expone el punto exacto en el que una captura aparentemente débil puede haber introducido una retención fuerte en el ámbito exterior.
La solución correcta depende de quién conserve cada bloque: mover la captura débil hacia fuera, repetirla en ambos niveles o hacer explícita una captura fuerte segura. Lo importante es que la lista de captura refleje el grafo de propiedad real y no se utilice únicamente como una fórmula para hacer desaparecer el aviso.