Saltar al contenido principal
Be Native Be Native

Artículo

Swift 6.3 y 6.4: las novedades que vas a tocar cada día

¿Qué novedades nos trae Swift 6.4 y cuáles fueron las últimas que incorporó la versión 6.3 que se lanzó en primavera? Este año Apple ha condensado ambas en su vídeo canónico "What's New in Swift?" para la WWDC, y os traemos un artículo cada importante novedad, qué aporta, qué cambia, su por qué (lo más importante) y cómo nos hace la vida más fácil.

· · 13 min de lectura

🤯 Hay versiones de Swift que llegan con fuegos artificiales y hay otras que llegan con algo mucho más valioso: menos fricción. Esas que hacen que ese pequeño detalle que te molestaba cada mañana… simplemente desaparezca. Y resulta que las dos últimas entregas del lenguaje vienen cargadas precisamente de eso. Por eso mismo Apple este año ha decidido hacer su vídeo canónico: "What's New in Swift?" para la WWDC 2026, abarcando ambas versiones y sus novedades.

🤔 Antes de entrar, conviene poner orden, porque aquí hay dos versiones jugando a la vez. Swift 6.3 ya está disponible desde marzo de 2026 (vamos por la 6.3.2), es estable y la puedes usar en producción hoy mismo. Swift 6.4 acaba de presentarse en la WWDC 2026 y está en beta, dentro de los Xcode 27 de desarrollo. Es decir: lo de la 6.3 es "ya"; lo de la 6.4 es "ya casi, pero todavía cocinándose". Te lo voy a ir marcando cambio a cambio, porque no es lo mismo apoyar tu build de mañana en una cosa que en la otra.

👨🏻‍💻 Y una idea que vas a ver repetirse en todo el artículo: casi ninguna de estas mejoras te obliga a escribir más código. Al contrario. Casi todas existen para que escribas exactamente lo que quieres decir, sin ruido alrededor. Que es, al final, de lo que va entender el código.

Adiós a los paréntesis que sobraban

✨ (Swift 6.4 — beta) Empecemos por la más pequeña, esa que solo notarás como una molestia que se va.

Cuando querías usar some o any con un tipo opcional, el compilador te obligaba a poner paréntesis. No por capricho, sino porque caía de las reglas de precedencia de operadores. Pero era pedante.

swift
// Antes
func delegate() -> (any MissionDelegate)?
// Ahora
func delegate() -> any MissionDelegate?

🫵🏻 ¿Qué aporta? Nada espectacular, y precisamente por eso es bueno: el compilador ya entiende lo único que tenía sentido que quisieras decir. Una piedrecita menos en el zapato.

El error que se te escapaba en una Task

🤯 (Swift 6.4 — beta) Esta sí es importante, porque va de seguridad real.

Hasta ahora, si lanzabas una Task con código que lanzaba (throws) y no la guardabas ni gestionabas el error, ese error desaparecía en silencio. Tu código parecía correcto. No lo era.

swift
// Esto se "tragaba" el error sin avisar
Task {
    try lander.fly(to: moon)
}

A partir de ahora el compilador te avisa con un warning y te ofrece las dos salidas correctas: gestionar el error dentro de la propia tarea, o guardar la tarea para comprobar su resultado después.

swift
// Opción 1: lo gestiono dentro
Task {
    do {
        try lander.fly(to: moon)
    } catch {
        lander.abort()
    }
}
// Opción 2: me guardo la tarea y lo compruebo luego
let landingTask = Task {
    try lander.fly(to: moon)
}

👨🏻‍🏫 ¿Qué aporta? Que el lenguaje deje de dejarte fallar en silencio. Un error que no gestionas no es un error que no existe: es una bomba de relojería que estalla en producción, lejos de tu Xcode. Esto lo destapa en compilación.

async dentro de un defer

🔦 (Swift 6.4 — beta) Había una limitación vieja y poco intuitiva: no podías llamar a funciones async desde un bloque defer. Lo cual era raro, porque el defer es justo donde quieres tu limpieza pase lo que pase, devuelva la función o lance.

swift
// Antes esto no compilaba
func rendezvous() async throws {
    let landingTask = Task { try lander.fly(to: moon) }
    defer {
        await orbiter.rendezvous(with: lander) // ❌ prohibido
    }
    try await orbiter.justHangOut(waitingFor: landingTask)
}

🙏🏻 Ahora sí se permite. ¿Qué aporta? Que tu lógica de limpieza asíncrona viva donde tiene que vivir, garantizando que se ejecuta tanto si sales bien como si sales por una excepción. Menos do/catch artificiales solo para limpiar.

weak let, ~Sendable y dejar de pelearte con @unchecked

🤖 (Swift 6.4 — beta, con matiz) ¿Cuántas veces has acabado poniendo @unchecked Sendable en una clase solo porque tenía una propiedad weak var y el compilador no te dejaba en paz? Ese @unchecked es una promesa que le haces al compilador: "confía en mí". Y "confía en mí" es justo lo que no queremos en código serio.

El truco está en que weak var es mutable, y por eso rompe el chequeo de Sendable. Pero si esa referencia no necesitas reasignarla, ahora la declaras weak let: sigue pudiendo volverse nil si el objeto se libera, pero es inmutable y deja de bloquearte el Sendable.

swift
final class Spacecraft: Sendable {
    weak let dockedAt: SpaceStation?   // inmutable y compatible con Sendable
}

Y si lo que quieres es lo contrario —dejar explícito que un tipo NO debe ser Sendable— ahora tienes la sintaxis con tilde:

swift
class Mission: ~Sendable { ... }
// Y de regalo: esto NO impide que las subclases sí lo sean
class CrewedMission: Mission, @unchecked Sendable { ... }

🫵🏻 ¿Qué aporta? Dos cosas que me importan mucho. Una, que weak let te quita un @unchecked Sendable de encima, y cada @unchecked que borras es una mentira menos en tu base de código (ojo: weak let existe desde Swift 6.2, lo que afina ahora es su encaje con Sendable). Y dos, que ~Sendable te deja decir tu intención de forma clara, sin trucos. El lenguaje premia, otra vez, decir la verdad.

Un segundo inicializador por miembros

😎 (Swift 6.4 — beta) Un struct que mezcla propiedades internal y private solo te daba un inicializador por miembros private, así que no podías construirlo desde otro fichero de tu propio proyecto sin escribirte el init a mano.

swift
struct Briefing {
    internal var topic: String
    internal var scheduledAt: Date
    private  var attendees: [Person] = []
}

Ahora Swift genera además un segundo inicializador por miembros que solo expone las propiedades internal, usable desde el resto de tu módulo:

swift
// Generado automáticamente, accesible desde otros ficheros del proyecto:
// init(topic: String, scheduledAt: Date)
let briefing = Briefing(topic: "Aterrizaje", scheduledAt: .now)

🙏🏻 ¿Qué aporta? Menos init escritos a mano solo para sortear la visibilidad. El compilador hace el trabajo aburrido por ti.

anyAppleOS: el fin de la torre de @available

🤯 (Swift 6.4 — beta) Esta la vas a notar. Apple alineó hace un año los números de versión de todos sus sistemas (iOS, macOS, watchOS, tvOS, visionOS comparten numeración). Swift 6.4 da el siguiente paso lógico: condensar todos esos nombres en uno.

swift
// Antes: la torre de siempre
@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showStatus() { ... }
// Ahora
@available(anyAppleOS 27, *)
func showStatus() { ... }

Y si hay excepciones, usas anyAppleOS como base y añades los matices encima:

swift
@available(anyAppleOS 27, *)
@available(tvOS, unavailable)
func launch() { ... }

Incluso funciona en las condiciones #if os(anyAppleOS) para compilar fuera bloques enteros.

👩🏼‍💻 ¿Qué aporta? Que tus atributos de disponibilidad dejen de ocupar tres líneas y dejen de ser una fuente de erratas (¿pusiste watchOS y olvidaste visionOS?). Más legible, menos frágil.

@diagnose: tú decides qué warning es un warning

🤯 (Swift 6.4 — beta) Y aquí va una para los que, como yo, nos ponemos nerviosos con un solo warning en el proyecto. Hasta ahora controlabas los warnings a lo bruto: a nivel de proyecto o de paquete. Todo o nada.

El nuevo atributo @diagnose te deja cambiar el comportamiento de un grupo de diagnósticos dentro de una declaración concreta. Tres usos, tres escenarios reales:

swift
// 1. Silenciar un warning de API deprecada SOLO aquí, sin tocar el resto
@diagnose(DeprecatedDeclaration, as: ignored, reason: "Migración pendiente del nuevo motor")
func makeLegacyMission() -> Mission { ... }
// 2. Activar un warning que viene apagado por defecto, en código sensible
@diagnose(StrictMemorySafety, as: warning)
func uplinkCommand(from receiver: inout Receiver, to computer: inout Computer) { ... }
// 3. Convertir en error YA un warning que será error en el futuro
@diagnose(ErrorInFutureSwiftVersion, as: error)
func fetchPosition() -> (x: Double, y: Double, z: Double) { ... }

🫵🏻 ¿Qué aporta? Control quirúrgico. Puedes silenciar temporalmente ese warning de migración que ya tienes fichado, sin apagarlo en todo el proyecto y sin que se te cuele otra cosa por debajo. Y al revés: puedes endurecer una función crítica para auditar tú mismo su uso de APIs inseguras. Esto es la diferencia entre "tengo cero warnings porque los he apagado todos" y "tengo cero warnings porque sé exactamente qué hace cada uno".

Selectores de módulo: cuando dos paquetes se llaman igual

🔑 (Swift 6.3 — estable) Ojo, que esta ya la tienes disponible. ¿Qué pasa cuando importas dos módulos que exponen un tipo con el mismo nombre? Que Swift no sabe a cuál te refieres y te da error. Hasta ahora lo resolvías con la sintaxis de punto, Rocket.SaturnV, pero eso tiene un agujero: Swift prefiere los nombres de tipo sobre los de módulo. Si el módulo Rocket tiene además un tipo Rocket, te busca el miembro dentro del tipo y no lo encuentra.

swift
import Rocket
import GiftShopToys
let r1 = SaturnV()          // ❌ ambiguo: ¿el cohete real o el de juguete?
let r2 = Rocket.SaturnV()   // ⚠️ puede confundirse con el tipo Rocket

La solución de Swift 6.3 es elegante: cambias el punto por dos puntos (::). Es el selector de módulo, y lo que va a la izquierda siempre se trata como nombre de módulo.

swift
let r3 = Rocket::SaturnV()  // ✅ inequívoco: el SaturnV del módulo Rocket

También funciona sobre métodos y propiedades, útil cuando un tipo recibe métodos con el mismo nombre desde extensiones de dos módulos distintos:

swift
launchPadTechnician.HumanResources::fire()  // despedir, no prender fuego 🔥

😊 ¿Qué aporta? Te salva el día cuando, por ejemplo, SwiftUI y un paquete de base de datos definen ambos un tipo View. Eso sí, el consejo de Apple es claro y lo suscribo: úsalo para resolver conflictos entre módulos que no controlas, no para diseñar APIs que chocan a propósito y luego apoyarte en esto. Que tu código compile no significa que se lea bien.

El estándar afina: diccionarios y rutas

👨🏻‍💻 (Swift 6.4 — beta) Dos pequeñas joyas de la librería estándar.

La primera, mapKeyedValues. Hasta ahora mapValues solo te pasaba el valor al closure. Si para calcular el nuevo valor necesitabas también la clave, te tocaba reconstruir el diccionario a mano. Un dolor.

swift
// Antes: a mano, porque mapValues no me da la clave
let names = [Mission: String](uniqueKeysWithValues:
    missions.lazy.map { mission, window in
        (mission, makeDisplayName(for: mission, in: window))
    }
)
// Ahora: la clave y el valor, directos
let names = missions.mapKeyedValues { mission, window in
    makeDisplayName(for: mission, in: window)
}

La segunda, un nuevo tipo FilePath en la propia librería estándar, basado en el de Swift System. Manipular rutas tiene diferencias sutiles entre plataformas que es fácil hacer mal.

swift
var path: FilePath = "/var/www/static"
path.components.append("WWDC")
print(path.components)   // ["var", "www", "static", "WWDC"]

🙏🏻 ¿Qué aportan? Menos código de pegamento y menos errores tontos al tratar rutas. Cosas pequeñas que sumadas se notan.

El "escudo" de cancelación de tareas

🦾 (Swift 6.4 — beta) Comprobar si una tarea fue cancelada antes de empezar un trabajo caro es buena práctica. Pero hay un caso en el que quieres terminar el trabajo aunque la hayan cancelado: por ejemplo, acabar de escribir en disco para no corromper un fichero.

Para eso llega withTaskCancellationShield. Dentro de ese bloque, las comprobaciones de cancelación siempre devuelven false.

swift
func sendSOS() {
    withTaskCancellationShield {
        radio.send(makeSOSPacket())   // esto se manda sí o sí
    }
}

🫵🏻 ¿Qué aporta? Una herramienta precisa para esos momentos críticos. El consejo: mantén la región corta, dedicada solo a terminar o revertir lo que ya habías empezado. No es una excusa para ignorar la cancelación en general.

Swift Testing sigue creciendo

🎯 Aquí hay novedades repartidas entre las dos versiones, así que atención al marcado.

(Swift 6.3 — estable) Ya puedes graduar la severidad de un issue. Un Issue.record con severity: .warning aparece en los resultados del test pero no lo marca como fallido. Perfecto para sacar a la luz algo sospechoso que no quieres que tumbe tu CI.

swift
if remaining < 0.10 {
    Issue.record("Combustible por debajo del 10% de reserva", severity: .warning)
}

(Swift 6.3 — estable) Y puedes cancelar tests dinámicamente con Test.cancel(). Brilla con los tests parametrizados: cancelas el argumento que no debe ejecutarse en lugar de fallar o de correrlo entero.

swift
@Test(arguments: allRockets)
func testBurn(rocket: Rocket) throws {
    if rocket.engineType == .solid {
        try Test.cancel("\(rocket.name) usa combustible sólido, no aplica")
    }
    // ...
}

(Swift 6.4 — beta) La gran novedad: interoperabilidad con XCTest en los dos sentidos. Los fallos de aserciones de XCTest se reportan como issues cuando los llamas desde Swift Testing, y las macros de Swift Testing como #expect funcionan dentro de un XCTestCase. Por defecto, esos casos cruzados se reportan como warnings; tú decides en los ajustes de Xcode si los promocionas a fallo.

🤔 ¿Qué aporta? Que migrar de XCTest a Swift Testing deje de ser un salto al vacío. Puedes convivir, mover utilidades poco a poco y no perder cobertura por el camino. Y como bonus, el comando swift test ahora puede repetir un test hasta que pase (o hasta que falle), reejecutando solo los que fallan: la herramienta perfecta contra los tests inestables.

Progreso bien hecho, pensado para async/await

📅 (Swift 6.4 — beta) Foundation estrena ProgressManager, un tipo de reporte de progreso diseñado para la concurrencia moderna. Separa con limpieza la composición del progreso de su reporte, y te deja adjuntar metadatos de forma tipada.

swift
let manager = ProgressManager(totalCount: 100)
try await rocket.launch(manager.subprogress(assigningCount: 100))
extension Rocket {
    func launch(_ progress: consuming Subprogress? = nil) async throws {
        let stage = progress?.start(totalCount: 3)
        try await ignite();          stage?.complete(count: 1)
        try await liftoff();         stage?.complete(count: 1)
        try await stageSeparation(); stage?.complete(count: 1)
    }
}

👀 ¿Qué aporta? Reporte de progreso que encaja de verdad con el mundo async/await, sin los apaños de toda la vida y con la composición resuelta por la propia API.

Entonces, ¿qué me llevo a casa?

🧠 Si te fijas, ninguna de estas novedades te pide que aprendas un paradigma nuevo. Todas van en la misma dirección: que el lenguaje deje de estorbarte para decir lo que ya querías decir. Errores que dejan de esconderse, @unchecked que desaparecen, atributos de disponibilidad que se encogen, warnings que controlas con bisturí.

🤷🏻‍♂️ Y aquí está el detalle que separa a quien usa estas cosas de quien las entiende: todas estas mejoras solo valen si sabes por qué existían las molestias. El que no entendía por qué weak var rompía Sendable no va a apreciar weak let. El que apagaba todos los warnings a la vez no va a sacarle jugo a @diagnose. La herramienta multiplica a quien comprende; al que no, lo deja igual.

🫵🏻 Así que la pregunta no es "¿qué versión pongo?". Es: ¿de cuántos de estos pequeños apaños vivías sin darte cuenta? Cuéntamelo. Y dime cuál de todos vas a estrenar primero.