Skip to main content
Be Native Be Native

Articles

Swift 6.3 and 6.4: the new features you will code every day

What news does Swift 6.4 bring us, and what were the last ones that incorporated version 6.3, that was released in spring? This year, Apple has condensed both in its canonical video "What's New in Swift?" for the WWDC, and we bring you an article on every important novelty, what it brings, what changes, its why (the most important thing), and how it makes our lives easier.

· · 13 min read

🤯 Some Swift releases arrive with fireworks, and others arrive with something far more valuable: less friction. The kind that makes that little annoyance you ran into every morning… simply disappear. And it turns out the language's two latest releases are packed with exactly that. So much so that this year Apple decided to make its canonical "What's New in Swift?" video for WWDC 2026 cover both versions and everything they bring.

🤔 Before we dive in, let's get our bearings, because there are two versions playing at once here. Swift 6.3 has been available since March 2026 (we're on 6.3.2), it's stable, and you can ship it in production today. Swift 6.4 was just unveiled at WWDC 2026 and is in beta, inside the developer builds of Xcode 27. In other words: 6.3 is "now"; 6.4 is "almost now, but still in the oven." I'll flag it change by change, because leaning your build on one or the other tomorrow is not the same thing.

👨🏻‍💻 And here's an idea you'll see running through the whole article: almost none of these improvements ask you to write more code. Quite the opposite. Nearly all of them exist so you can say exactly what you mean, without noise around it. Which is, in the end, what understanding the code is all about.

Goodbye to the parentheses that didn't belong

✨ (Swift 6.4 — beta) Let's start with the smallest one, the kind you'll only notice as an annoyance going away.

When you wanted to use some or any with an optional type, the compiler forced you to add parentheses. Not out of spite, but because it fell out of the operator precedence rules. Still, it was pedantic.

swift
// Before
func delegate() -> (any MissionDelegate)?
// Now
func delegate() -> any MissionDelegate?

🫵🏻 What does it give you? Nothing spectacular — and that's precisely why it's good: the compiler now understands the only thing you could have possibly meant. One less pebble in your shoe.

The error that used to slip out of a Task

🤯 (Swift 6.4 — beta) This one matters, because it's about real safety.

Until now, if you launched a Task with throwing code and didn't store it or handle the error, that error vanished in silence. Your code looked correct. It wasn't.

swift
// This silently swallowed the error, no warning
Task {
    try lander.fly(to: moon)
}

From now on, the compiler warns you and points you to the two correct ways out: handle the error inside the task itself, or save the task to check its result later.

swift
// Option 1: handle it inside
Task {
    do {
        try lander.fly(to: moon)
    } catch {
        lander.abort()
    }
}
// Option 2: save the task and check it later
let landingTask = Task {
    try lander.fly(to: moon)
}

👨🏻‍🏫 What does it give you? It stops the language from letting you fail in silence. An error you don't handle isn't an error that doesn't exist: it's a time bomb that goes off in production, far away from your Xcode. This surfaces it at compile time.

async inside a defer

🔦 (Swift 6.4 — beta) There was an old, unintuitive restriction: you couldn't call async functions from a defer block. Which was odd, because defer is exactly where you want your cleanup to run no matter what — whether the function returns or throws.

swift
// This used to fail to compile
func rendezvous() async throws {
    let landingTask = Task { try lander.fly(to: moon) }
    defer {
        await orbiter.rendezvous(with: lander) // ❌ not allowed
    }
    try await orbiter.justHangOut(waitingFor: landingTask)
}

🙏🏻 Now it's allowed. What does it give you? Your asynchronous cleanup logic lives where it belongs, guaranteed to run whether you exit cleanly or through an exception. Fewer artificial do/catch blocks written just to clean up.

weak let, ~Sendable, and the end of fighting @unchecked

🤖 (Swift 6.4 — beta, with a caveat) How many times have you ended up slapping @unchecked Sendable on a class just because it had a weak var property and the compiler wouldn't leave you alone? That @unchecked is a promise you make to the compiler: "trust me." And "trust me" is exactly what we don't want in serious code.

The catch is that weak var is mutable, and that's what breaks Sendable checking. But if you don't need to reassign that reference, you can now declare it weak let: it can still become nil if the object is deallocated, but it's immutable and no longer blocks Sendable.

swift
final class Spacecraft: Sendable {
    weak let dockedAt: SpaceStation?   // immutable and Sendable-friendly
}

And if what you want is the opposite — to make it explicit that a type should NOT be Sendable — there's now the tilde syntax:

swift
class Mission: ~Sendable { ... }
// As a bonus: this does NOT stop subclasses from being Sendable
class CrewedMission: Mission, @unchecked Sendable { ... }

🫵🏻 What does it give you? Two things I care about a lot. First, weak let takes an @unchecked Sendable off your shoulders, and every @unchecked you delete is one less lie in your codebase (note: weak let has existed since Swift 6.2; what's being sharpened now is how it fits with Sendable). Second, ~Sendable lets you state your intent clearly, without tricks. Once again, the language rewards telling the truth.

A second memberwise initializer

😎 (Swift 6.4 — beta) A struct mixing internal and private properties only gave you a private memberwise initializer, so you couldn't build it from another file in your own project without writing the init by hand.

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

Now Swift generates an additional second memberwise initializer that exposes only the internal properties, usable from the rest of your module:

swift
// Generated automatically, accessible from other files in the project:
// init(topic: String, scheduledAt: Date)
let briefing = Briefing(topic: "Landing", scheduledAt: .now)

🙏🏻 What does it give you? Fewer hand-written inits just to work around visibility. The compiler does the boring work for you.

anyAppleOS: the end of the @available tower

🤯 (Swift 6.4 — beta) You'll notice this one. A year ago Apple aligned the version numbers across all its systems (iOS, macOS, watchOS, tvOS, and visionOS now share numbering). Swift 6.4 takes the logical next step: condensing all those platform names into one.

swift
// Before: the usual tower
@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showStatus() { ... }
// Now
@available(anyAppleOS 27, *)
func showStatus() { ... }

And when there are carve-outs, you use anyAppleOS as the baseline and add the exceptions on top:

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

It even works in #if os(anyAppleOS) conditions to compile out entire blocks.

👩🏼‍💻 What does it give you? Your availability attributes stop taking up three lines and stop being a source of typos (did you write watchOS and forget visionOS?). More readable, less fragile.

@diagnose: you decide what counts as a warning

🤯 (Swift 6.4 — beta) Here's one for those of us who, like me, get nervous over a single warning in the project. Until now you controlled warnings with a sledgehammer: at the project or package level. All or nothing.

The new @diagnose attribute lets you change the behavior of a diagnostic group within a specific declaration. Three uses, three real-world scenarios:

swift
// 1. Silence a deprecated-API warning ONLY here, without touching the rest
@diagnose(DeprecatedDeclaration, as: ignored, reason: "New engine migration pending")
func makeLegacyMission() -> Mission { ... }
// 2. Turn on a warning that's off by default, in security-critical code
@diagnose(StrictMemorySafety, as: warning)
func uplinkCommand(from receiver: inout Receiver, to computer: inout Computer) { ... }
// 3. Promote to an error right now a warning that will be an error in the future
@diagnose(ErrorInFutureSwiftVersion, as: error)
func fetchPosition() -> (x: Double, y: Double, z: Double) { ... }

🫵🏻 What does it give you? Surgical control. You can temporarily silence that migration warning you've already got on your radar without turning it off across the whole project and without something else sneaking past underneath. And the other way around: you can harden a critical function to audit its use of unsafe APIs yourself. This is the difference between "I have zero warnings because I turned them all off" and "I have zero warnings because I know exactly what each one does."

Module selectors: when two packages share a name

🔑 (Swift 6.3 — stable) Heads up — this one you already have. What happens when you import two modules that expose a type with the same name? Swift can't tell which one you mean and gives you an error. Until now you'd fix it with dot syntax, Rocket.SaturnV, but that has a hole: Swift prefers type names over module names. If the Rocket module also has a type called Rocket, it looks for the member inside the type and doesn't find it.

swift
import Rocket
import GiftShopToys
let r1 = SaturnV()          // ❌ ambiguous: the real rocket or the toy one?
let r2 = Rocket.SaturnV()   // ⚠️ may be confused with the type Rocket

Swift 6.3's solution is elegant: swap the dot for a double colon (::). It's the module selector, and whatever is on the left is always treated as a module name.

swift
let r3 = Rocket::SaturnV()  // ✅ unambiguous: the SaturnV from the Rocket module

It also works on methods and properties, handy when a type gets identically-named methods from extensions in two different modules:

swift
launchPadTechnician.HumanResources::fire()  // fire from the job, not set on fire 🔥

😊 What does it give you? It saves the day when, say, both SwiftUI and a database package define a type called View. That said, Apple's advice is clear and I second it: use it to resolve conflicts between modules you don't control, not to design APIs that clash on purpose and then lean on this to tell them apart. Code compiling doesn't mean code reads well.

The standard library sharpens up: dictionaries and paths

👨🏻‍💻 (Swift 6.4 — beta) Two small gems in the standard library.

The first, mapKeyedValues. Until now mapValues only passed the value into the closure. If you needed the key to compute the new value, you had to rebuild the dictionary by hand. A pain.

swift
// Before: by hand, because mapValues doesn't give me the key
let names = [Mission: String](uniqueKeysWithValues:
    missions.lazy.map { mission, window in
        (mission, makeDisplayName(for: mission, in: window))
    }
)
// Now: key and value, straight away
let names = missions.mapKeyedValues { mission, window in
    makeDisplayName(for: mission, in: window)
}

The second, a new FilePath type in the standard library itself, based on the one from Swift System. Manipulating paths has subtle cross-platform differences that are easy to get wrong.

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

🙏🏻 What do they give you? Less glue code and fewer silly mistakes when dealing with paths. Small things that add up.

The task cancellation shield

🦾 (Swift 6.4 — beta) Checking whether a task was cancelled before starting expensive work is good practice. But there's a case where you want to finish the work even if it was cancelled: for example, finishing a write to disk so you don't corrupt a file.

That's what withTaskCancellationShield is for. Inside that block, cancellation checks always return false.

swift
func sendSOS() {
    withTaskCancellationShield {
        radio.send(makeSOSPacket())   // this goes out no matter what
    }
}

🫵🏻 What does it give you? A precise tool for those critical moments. The advice: keep the region short, focused only on finishing or rolling back what you'd already started. It's not an excuse to ignore cancellation in general.

Swift Testing keeps growing

🎯 Here the news is split across both versions, so watch the tags.

(Swift 6.3 — stable) You can now set the severity of an issue. An Issue.record with severity: .warning shows up in the results but does not mark the test as failed. Perfect for surfacing something suspicious that you don't want bringing down your CI.

swift
if remaining < 0.10 {
    Issue.record("Fuel below the 10% reserve target", severity: .warning)
}

(Swift 6.3 — stable) And you can cancel tests dynamically with Test.cancel(). It shines with parameterized tests: you cancel the argument that shouldn't run instead of failing or running it to completion.

swift
@Test(arguments: allRockets)
func testBurn(rocket: Rocket) throws {
    if rocket.engineType == .solid {
        try Test.cancel("\(rocket.name) uses solid fuel, not applicable")
    }
    // ...
}

(Swift 6.4 — beta) The headline feature: two-way interoperability with XCTest. XCTest assertion failures are reported as issues when called from Swift Testing, and Swift Testing macros like #expect work inside an XCTestCase. By default, those cross-framework cases are reported as warnings; you decide in Xcode's settings whether to promote them to failures.

🤔 What does it give you? Migrating from XCTest to Swift Testing stops being a leap of faith. You can coexist, move utilities over little by little, and not lose coverage along the way. As a bonus, the swift test command can now repeat a test until it passes (or until it fails), re-running only the ones that failed: the perfect tool against flaky tests.

Progress done right, built for async/await

📅 (Swift 6.4 — beta) Foundation introduces ProgressManager, a progress-reporting type designed for modern concurrency. It cleanly separates progress composition from progress reporting, and lets you attach metadata in a type-safe way.

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)
    }
}

👀 What does it give you? Progress reporting that genuinely fits the async/await world, without the old workarounds and with composition handled by the API itself.

So, what do I take home?

🧠 Notice that none of these features asks you to learn a new paradigm. They all point the same way: getting the language out of your way so you can say what you already wanted to say. Errors that stop hiding, @uncheckeds that disappear, availability attributes that shrink, warnings you control with a scalpel.

🤷🏻‍♂️ And here's the detail that separates whoever uses these things from whoever understands them: all these improvements only pay off if you know why the annoyances existed in the first place. Whoever didn't understand why weak var broke Sendable won't appreciate weak let. Whoever turned off every warning at once won't get any mileage out of @diagnose. The tool multiplies whoever understands; whoever doesn't, it leaves exactly where they were.

🫵🏻 So the question isn't "which version should I set?" It's: how many of these little workarounds were you living with without even noticing? Tell me. And tell me which one you'll try first.