Swift Group Lab
Join us online for a deep dive into WWDC26 with Apple engineers and designers to ask questions, get advice, and follow the discussion about the week's biggest Swift announcements. Conducted in English.
Transcript
Engineers from the Swift and Foundation teams introduce themselves and set up a session spanning Swift concurrency and the Swift 6 migration, the ownership model, performance and build times, Swift Package Manager, and lesser-known language features.
Use region-based isolation, which lets you transfer non-Sendable data between actors as long as the original actor no longer accesses it after the transfer. In many cases the compiler can prove this is safe from your usage; the sending keyword makes the transfer explicit when you need to hand data off and relinquish access.
Lean into structured concurrency as fully as you can — trouble tends to come from adding escape hatches out of it. A practical approach is to refactor smaller, self-contained parts of your codebase into structured concurrency as units, rather than sprinkling unstructured tasks throughout, so the structure and cancellation guarantees actually hold.
There is some cost. Conformances like Equatable and Hashable generate the associated equality/hashing code and metadata to support them, adding to binary size and compile work even when unused. Add conformances deliberately where you need them rather than reflexively to every type; Sendable is a compile-time marker with less runtime weight, but value conformances carry real generated code.
When you mark a type @MainActor, everything using it must also be main-actor-isolated, which spreads. Rather than annotating individual types, consider setting main-actor isolation as the module's default so most of your app code is implicitly on the main actor, and carve out concurrency only where you deliberately go off it. That inverts the contagion and keeps legacy UI code simple while preserving safety.
Start by profiling — Instruments shows exactly where time goes, and the new flame graph view breaks down where work happens, so you optimize what actually matters rather than guessing. Build performance intuition from measurement first, then adopt the specific language features that address the hot spots you find.
It's completely fine to leave redundant annotations — they don't hurt, and some developers prefer keeping isolation explicit for clarity. There's no need to churn your codebase removing them; remove them if you value the reduced noise, but treating them as harmless no-ops is a perfectly good choice.
When Sendable was introduced, Foundation was audited into three buckets: clearly Sendable, clearly not, and a middle group complicated by class hierarchies where a superclass might be Sendable but a subclass isn't (like NSString being immutable while a mutable subclass exists). UserDefaults falls into that nuanced category; it's thread-safe in practice but not yet marked Sendable pending that resolution.
Borrow and mutate are part of Swift's evolving ownership model: borrow gives a read-only reference to data held elsewhere (avoiding a copy), and mutate gives a mutable reference. They're valuable for performance-sensitive code handling large or non-copyable values, but they're not a blanket replacement for get/set — reach for them where avoiding copies matters, not everywhere by default.
Swift is designed around progressive disclosure — you shouldn't need the advanced, low-level features for typical app development. Many additions target systems, embedded, or performance-critical code; reach for them only when a specific need arises. Don't feel obligated to learn everything at once; the core language remains approachable and the specialized tools are there when you need them.
Language features like type inference and generics can hurt build performance in the extreme, but usually not project-wide — once in a while a particular expression takes a long time to type-check. But when it's specifically the module-emission phase that's slow, that's more likely related to all the other modules being imported. Diagnose it the way you'd performance-tune code: use explicit module builds (rolled out in Xcode and Swift over the last couple of years, now on by default) together with the build timeline in Xcode to see where the compiler is actually spending its time. You'll often find excess dependencies causing large rebuilds that are easy to prune once you can see them.
Yes — through concurrency's evolution there were two changes to how async functions behave, and in hindsight the team would have started with the latest behavior for where a non-isolated async function runs. That final model (a nonisolated async function runs in the caller's context rather than hopping off) is what they'd have chosen from the outset.
The biggest Swift Package Manager change in the 6.4 release is one you don't opt into: Xcode's SPM and the open-source SPM used in other IDEs (like VS Code) now share a single build-system implementation, the Swift Build package, rather than two separate ones. That brings consistency across environments and a single place to land bug fixes and improvements. It also carries the Swift build system's performance work into ordinary package builds — including explicit modules and better splitting of a module's build into parallel pieces. It was a preview in 6.3 and is on by default in 6.4, so you benefit just by updating.
A favorite is a combination of two: the annotations that control a function body's visibility across module boundaries together with the optimizer's inlining decisions (such as @inlinable and @inline). Used well, they let the optimizer inline across modules for performance-critical code — powerful, but to be applied deliberately since they expose implementation details.
It's an evolution of parameter packs. The base concept now exists in the language; what's still needed is the ability to write an extension over a tuple type whose element types are represented by a parameter pack (since each element can be a different concrete type). Once that's possible, conditional conformances of tuples to Equatable, Hashable, and so on can be expressed.
Start by pinning down what "high frequency" actually means — if it's still well below your UI's update rate, there may be no work to do. When it's genuinely high, the goal is to cross from the background actor to your MainActor @Observable model as infrequently as possible: coalesce or accumulate samples rather than sending one update per change, and decide how much data loss is acceptable (a UI rarely needs every intermediate value). Debouncing helps here — Swift Async Algorithms has a Debounce you can layer on an async sequence — and SwiftUI's @Observable already coalesces updates for efficiency. Keep the heavy ingestion asynchronous, and drive the UI only with what the user actually needs to see.
They're handled differently by the compiler. A struct is generally passed as a single entity, whereas a tuple is often "exploded" — each element passed as if it were a separate parameter. For a tuple with many elements that per-element handling can cost more than moving one struct, which is why returning a struct can be cheaper than an equivalent tuple.
A forward-looking favorite is the new work on iterable protocols mentioned in What's New in Swift, which connects to the performance thread around non-copyable and non-escapable types. It points toward writing expressive iteration while keeping the strong performance and ownership guarantees the language has been building out.