Swift 6.4 is in beta — it landed with the Xcode 27 betas at WWDC26, and it’s one of those releases that doesn’t show up in the keynote as a headline. No new concurrency model. No syntax revolution. Just a handful of changes that quietly delete code you’ve been writing by hand for years.
I pulled the beta toolchain the first day it was up, and my read after an afternoon was housekeeping release — the kind where the compiler stops making you spell out things it already knew. Two weeks of dragging real RTL Air code onto it hasn’t softened that read, it’s sharpened it. Four of these changes pull their weight on every file, not just in the demo that sells them. It’s still beta, so one asterisk worth stating plainly: this can move before it stabilizes in the fall, and one of the four below is sitting right on an edge that hasn’t settled yet.
So let me walk through them the way I’d walk a junior through a flight. One aircraft, one route, the whole departure. We’ll board RTL 1986 “Master of Puppets,” SFO to JFK, out of Gate M72. Every example below is something Ride the Lightning Air actually has to do to get that flight off the ground — and every one of them got a little less miserable in the 6.4 betas. RTL Air’s been my running example before — building its interactive widgets, then its Live Activities — so if the airline feels familiar, that’s why. No prior reading required.
Set 1: anyAppleOS — One Condition Instead of Five
RTL Air does live translation of cabin announcements. The captain says “we’re number two for departure” and the passenger reads it in their language — on the iPhone, on the iPad they propped against the seatback, on the seatback unit itself (tvOS), on the Apple Watch they’re using as a boarding pass, and on the Vision Pro the guy in 3A insisted on bringing. Five surfaces, one feature, all gated behind the same OS generation.
Here’s the before, and it’s the kind of line you stop reading because your eyes have been trained to skip it:
@available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *)
func enableLiveTranslation() {
// ...
}
func configureCabinAnnouncements() {
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *) {
enableLiveTranslation()
} else {
enableLegacyCaptions()
}
}
The problem isn’t that it’s long. It’s that it’s fragile in a way that doesn’t fail loudly. We added the Vision Pro surface to RTL Air six months after the rest. Thread visionOS 26 into every one of the forty #available checks written before Vision Pro was on anyone’s radar? Miss one, and the compiler is perfectly happy. You just silently fall into the legacy-captions branch on a platform that fully supports live translation. I’ve shipped that bug. Finding it is not fun. If you’ve lived through a WWDC beta season you know the dance — I wrote the survival guide for it a couple of summers back, and anyAppleOS is the language finally doing that survival work for you.
anyAppleOS collapses the whole thing into the intent you actually had:
@available(anyAppleOS 26, *)
func enableLiveTranslation() {
// ...
}
func configureCabinAnnouncements() {
if #available(anyAppleOS 26, *) {
enableLiveTranslation()
} else {
enableLegacyCaptions()
}
}
anyAppleOS 26 means “any Apple platform at its 26 release or later.” Add a new surface to RTL Air tomorrow and the condition already covers it — there’s nothing to update because you never enumerated the platforms in the first place.
What I appreciate here is that it’s not magic, it’s honesty. The verbose form was always pretending to be precise while just being a long way of saying “the new stuff.” When every platform shipped the symbol in the same generation, the platform list carried zero information. anyAppleOS removes the information that was never there.
One caveat worth stating plainly: this is shorthand for the common case, not a replacement for per-platform gating. When an API genuinely lands on different OS versions across platforms — which still happens — you spell those out, because that difference is real and you want it visible. anyAppleOS is for the case where the list was noise. Use it there and nowhere else.
Set 2: async defer — Cleanup That Can Finally await
Someone in 14C is checked in and reaching for their wallet. Before we take a single dollar, RTL Air puts a hold on that seat so the person refreshing the same map two gates over can’t double-book it. Open the hold, commit it on a successful payment, release it on any failure path. Simple to say. Historically miserable to write correctly.
defer has always been the right tool for “no matter how this scope exits, run this.” The catch was that it lived in a synchronous world. The moment your cleanup needed to await something — release a remote seat lock, flush a buffer, close a connection — defer couldn’t help you, and you fell back to patterns that fought the language.
This is the shape of the workaround, and if you’ve written async resource code you’ve written something like it:
func bookSeat14C(payment: PaymentToken) async throws -> Booking {
let hold = try await seatInventory.openHold(flight: "RTL1986", seat: "14C")
do {
let booking = try await charge(payment, against: hold)
try await hold.commit() // happy path keeps the seat
return booking
} catch {
try? await hold.release() // and every failure path lets it go
throw error
}
}
Look at what’s actually happening. The cleanup is duplicated across two exits, and the two exits don’t even do the same thing — the happy path commits the hold, the failure path releases it. That asymmetry is fine, but the structure buries it. Add an early guard return in the middle — say, the passenger’s fare class no longer qualifies for 14C — and you’ve now got a third exit that quietly leaks the hold. That seat stays locked. The compiler won’t tell you — sad but true. The gate agent finds out when a paying passenger can’t sit in an empty seat.
In 6.4, defer learned to await:
func bookSeat14C(payment: PaymentToken) async throws -> Booking {
let hold = try await seatInventory.openHold(flight: "RTL1986", seat: "14C")
var committed = false
defer {
// committed → drop the handle, the sale already stands.
// not committed → roll the seat back into inventory.
await (committed ? hold.detach() : hold.release())
}
let booking = try await charge(payment, against: hold)
try await hold.commit()
committed = true
return booking
}
The cleanup lives in exactly one place, runs on every exit including ones you add later, and awaits whatever it needs to. The committed flag carries the one bit of real branching — did we get far enough to keep this seat? — instead of smearing it across two catch arms.
A couple of things to keep straight. An async defer runs its await work as the scope unwinds, so it participates in cancellation like any other suspension point. If the booking task is already cancelled — the passenger backed out, the app faded to black — your cleanup’s await calls can throw CancellationError unless they’re written to be cancellation-tolerant. Cleanup that must run regardless should be doing work that doesn’t itself need to check for cancellation: releasing a held seat you already own, not making a fresh network round trip you expect to complete. That’s not a 6.4 quirk. It’s the reality of async cleanup finally being expressible — the language stopped hiding the problem behind a missing feature.
Here’s the seatInventory side, written the way you’d actually ship it under Swift 6 — an actor, because seat holds are exactly the kind of shared mutable state you do not want races on:
actor SeatInventory {
private var holds: Set<SeatHoldID> = []
func openHold(flight: String, seat: String) async throws -> SeatHold {
let id = SeatHoldID(flight: flight, seat: seat)
guard holds.insert(id).inserted else { throw BookingError.seatTaken }
return SeatHold(id: id, inventory: self)
}
func commit(_ id: SeatHoldID) { holds.remove(id) /* mark sold, out of the hold pool */ }
func detach(_ id: SeatHoldID) { /* sale already stands; just drop the handle */ }
func release(_ id: SeatHoldID) { holds.remove(id) /* roll the seat back to available */ }
}
The seat lock is a real concurrency boundary, and 6.4 finally lets the call site clean it up honestly.
Set 3: Iterable — Iterating What You Were Never Allowed to Copy
Remember the seat hold from Set 2? That SeatHold is a thing you must never duplicate — two copies of the same hold is two passengers fighting over 14C. RTL Air has a whole category of resources like that: single-use boarding credentials, the DRM handle for the seatback stream, the live seat locks themselves. The honest way to model them in Swift is ~Copyable — make the compiler forbid the copy, because the copy is the bug.
Which slammed into an annoying wall the moment you wanted to loop over a batch of them. The humble for loop iterates through Sequence, and Sequence copies each element out as it goes. Hand it a noncopyable element and it doesn’t compile — there’s nothing to copy. So you fell back to index math, or a manual while walking an index by hand, or — the cardinal sin — you made the type Copyable just to get a for loop and quietly reintroduced the exact duplication you were trying to outlaw.
Here’s the wall. A manifest of noncopyable boarding tokens, and the loop that simply won’t build:
struct BoardingToken: ~Copyable {
let passenger: PassengerID
borrowing func validate() -> ScanResult { /* read-only gate check */ }
}
struct Manifest: ~Copyable {
// storage of BoardingTokens for RTL 1986
}
// What you wanted to write, and couldn't:
for token in manifest { // ❌ `for` goes through Sequence, which copies —
gate.admit(token.validate()) // and a BoardingToken can't be copied.
}
Swift 6.4 adds the Iterable protocol, and the one-sentence version is: it lets the for loop borrow each element instead of copying it out. Borrowing is the whole game. Because the loop only borrows, it works with noncopyable elements, and it skips the reference-count traffic that copying objects or copy-on-write values would have cost you. The iterator hands back elements in batches as spans rather than one at a time, and it can throw — so iterating a source that might fail mid-walk is finally expressible without draining it into an array first.
// Manifest conforms to Iterable now; the loop borrows each token in place.
for token in manifest { // ✅ borrowed, never copied
gate.admit(token.validate())
}
The loop body is byte-for-byte the same. That’s the point — the ergonomics win is that the obvious code finally compiles against the types you should have been using all along, instead of forcing you to choose between a clean loop and a correct model.
Two honest notes, and the second is a beta note. Borrowing forbids mutation: you can’t reach in and change the collection while you’re looping over it. That’s a constraint, not a defect — it’s the same guarantee that makes the borrow safe in the first place. And the precise shape of conforming your own type to Iterable is still moving in the Xcode 27 betas. The consumption side above — writing the for loop — is the stable part; if you’re authoring the conformance rather than just iterating, expect the requirements to shift before fall. A for loop still prefers Sequence when a type offers one and only falls back to Iterable, so nothing you already shipped changes behavior.
The real win isn’t shorter code — the loop was always three lines. It’s that “iterate this” and “never copy this” stopped being mutually exclusive. The safety you wanted out of ~Copyable no longer costs you the most basic control-flow statement in the language.
Set 4: The Free 4× — URL Parsing Got Fast and You Did Nothing
This one isn’t a feature you reach for. It’s a feature that reaches you. Foundation’s URL initialization and parsing got substantially faster in 6.4 — roughly 4× in the cases I measured — and the code change required on your end is none. You recompile, your URL(string:) calls get cheaper.
That sounds like a footnote until you remember where URL actually lives in RTL Air. It’s in the deep-link router that turns rtlair://boarding/RTL1986/14C into a boarding-pass screen when someone taps a notification. It’s in the request pipeline, parsed on every call to the booking API. It’s in the seatback image feed, built per cell as the catalog scrolls. The cost was always a creeping death by a thousand cuts — exactly the kind of cost a single release can erase without anyone writing a line of code.
I wanted to see it rather than take the release notes’ word for it, so I ran a tight loop with ContinuousClock over the URLs RTL Air parses for real:
import Foundation
let inputs = [
"rtlair://boarding/RTL1986/14C",
"https://api.ridethelightning.air/v2/flights/RTL1986/seats?cabin=main&hold=14C",
"https://cdn.ridethelightning.air/seatback/RTL1986/feed/master-of-puppets-1986.jpg#row-14",
"https://ridethelightning.air/checkin/RTL1986?ref=watch&token=sandman"
]
func benchmark(iterations: Int) -> Duration {
let clock = ContinuousClock()
return clock.measure {
for _ in 0..<iterations {
for string in inputs {
_ = URL(string: string)
}
}
}
}
// Warm up, then measure.
_ = benchmark(iterations: 10_000)
let elapsed = benchmark(iterations: 1_000_000)
print("1M × \(inputs.count) URLs parsed in \(elapsed)")
Here’s what I saw, and I want to be precise about what this is: these are illustrative numbers from one machine, one set of inputs, one afternoon, on a pre-release toolchain. They are not Apple’s figures and they are not a promise about your workload — and a beta’s performance is its own moving target.
| Build | 4M URL(string:) parses |
Per-URL (approx) |
|---|---|---|
| Swift 6.3 | ~1.42 s | ~355 ns |
| Swift 6.4 | ~0.36 s | ~90 ns |
That’s the 4× ballpark the release talks about, and it tracked across the run. But the honest version of this story has caveats, and they matter more than the headline. The speedup depends heavily on your input shape — the simple rtlair:// deep link benefits differently than a CDN URL stuffed with a path, a query, and a fragment. It depends on your hardware. And it depends on whether URL parsing was ever actually on your hot path, because if you parse a dozen URLs a screen, 4× of “already negligible” is still negligible.
So don’t refactor anything to chase this. Don’t add URL parsing to your performance budget because a blog said 4×. Just know that if you were quietly paying for URL in a tight loop — the deep-link router fielding a burst of boarding-pass taps, the seatback feed chewing through a catalog, the request pipeline under load — that bill got smaller for free. Measure your own path if it matters. Take the gift and move on if it doesn’t.
Encore: Less Ceremony, Same Language
None of these four will headline a keynote. There’s no demo where anyAppleOS makes the room gasp. But put them together and you can see what Swift 6.4 is actually doing: it’s removing the gap between what you mean and what you have to type.
The verbose #available list pretended to be precise. The duplicated seat-hold cleanup pretended the failure paths were different problems. The for loop pretended everything worth iterating was safe to copy. The URL cost pretended a thousand tiny parses were free. Every one of those was a small lie the language used to make you tell, and the 6.4 betas just stop asking.
That’s the version of language maturity I’ve come to trust most. Not the release that adds power — the release that lets you stop proving you meant it. Six versions into Swift 6, the ceremony is finally getting quieter.
RTL 1986 is wheels-up out of M72, the cabin’s reading the captain in five languages, and 14C is sold to exactly one person — nothing else matters. Ship the boring release. It’s the one your future self thanks you for.
That’s the set. Lights up.