Oct 30 2026
, Swift 6.4, iOS 27, Xcode 27Swift 6.4, iOS 27, Xcode 27
Learn what’s new in Swift 6.4, including syntax and ergonomics improvements, concurrency updates like async defer and task cancellation shields, and updates to Swift’s libraries and build system.
By Mani Ramezan.
Since Swift 2, Swift has supported the defer statement, which lets you schedule code to run when the current scope exits. This helps ensure cleanup code runs even if a function returns early or throws. But defer has always run synchronously. Often, cleanup code is simple and doesn’t need to await anything, so synchronous defer works fine — for example, to close a file handle or a database connection:
func processTransaction(id: String) async throws {
let session = try await databaseManager.openSession(for: id)
// ❌ error: 'async' call cannot occur in a defer body
defer {
await session.close()
}
try await session.writeLogs()
try await session.commit()
}
As a workaround, you could spin up a Task inside the defer block:
Looks correct, right? This works for the most part, but on closer inspection it trades one problem for another: now it’s unstructured, and processTransaction can return before session.close() ever finishes. There’s no guarantee the cleanup happens before the enclosing function returns.
Swift 6.4 closes this gap by letting you await inside a regular defer block, so you can now write:
Now the function genuinely waits for session.close() before it returns — no stray background task, no race.
Explicit non-Sendable
As part of Swift concurrency, Swift added Sendable to specify types that can be safely transferred across concurrency domains. This helps the compiler enforce safe usage of types in concurrent contexts. However, there are cases where you want to explicitly mark a type as non-Sendable, indicating that it’s not safe to transfer across concurrency domains. Also, when the developer does not mark a type as Sendable, there is some ambiguity: is that intentional, or is the conformance simply overlooked?
One older workaround was to add an unavailable Sendable conformance. This does not make the type Sendable; it documents that Sendable conformance is intentionally unavailable:
However, this workaround has limitations. For example, let’s say you now want to subclass this class and create a thread-safe buffer using locks:
// Subclass guarantees atomic additions using an unfair lock.
final class LockedEventBuffer: BaseEventBuffer, @unchecked Sendable {
private let lock = OSAllocatedUnfairLock()
override func add(event: String) {
lock.withLock {
MainActor.assumeIsolated {
super.add(event: event)
}
}
}
override func flush() -> [String] {
lock.withLock {
MainActor.assumeIsolated {
super.flush()
}
}
}
}
Now when you try to compile, you’ll see the following warning:
To help with this limitation, Swift 6.4 introduces ~Sendable. This allows you to explicitly mark a class as non-Sendable without relying on the availability attribute hack:
class BaseEventBuffer: ~Sendable { ... }
final class LockedEventBuffer: BaseEventBuffer, @unchecked Sendable { ... }
BaseEventBuffer now explicitly identifies itself as not thread-safe, while LockedEventBuffer is thread-safe. This lets you mark your types as non-Sendable directly, without the limitations of the unavailable-conformance workaround.
Task cancellation shields
As part of modern concurrency, you can spin off asynchronous tasks and cancel them when they’re no longer needed. Consider a case where you start downloading a file and cache it locally:
class DiskCache {
func deleteTempFile(at path: String) {
guard !Task.isCancelled else { return }
try? FileManager.default.removeItem(atPath: path)
}
}
class ImageDownloader {
let cache = DiskCache()
func downloadImage(from url: URL) async throws {
let tempPath = URL.temporaryDirectory.appending(path: url.lastPathComponent).path()
do {
try await networkClient.download(from: url, to: tempPath)
} catch {
cache.deleteTempFile(at: tempPath)
throw error
}
}
}
This is a simplified example of what you might have in production code:
currTask is a local task property that lets you cancel the download later.
A button the user taps to start a download or cancel one.
If a download is already in progress, cancel it; otherwise, start a new one.
Store the new task so it can be canceled later.
Always reset isDownloading back to false when the download finishes, fails, or is cancelled.
Now, this might look fine at first glance, but it has a subtle issue related to task cancellation. Can you spot what could go wrong here? 🤔
The issue is hidden inside deleteTempFile(at:), the first line:
guard !Task.isCancelled else { return }
Here’s what happens:
User taps on the cancel button which cancels the task
Task.isCancelled is now set to true
The catch block in downloadImage(from:) is called with CancellationError
And then when deleteTempFile(at:) is called, it returns early before cleaning up the file
To fix this before, you had to spin off an unstructured task to perform the cleanup:
Task {
cache.deleteTempFile(at: tempPath)
}
This operates as a workaround but it introduces complexity and potential race conditions. The cleanup task is now running independently of the original task’s lifecycle. Swift 6.4 gives you a better way to handle this with task cancellation shields. This feature allows you to protect critical sections of code from premature cancellation without resorting to unstructured tasks. Use the withTaskCancellationShield function to wrap the critical section:
The new withTaskCancellationShield function ensures that the enclosed code block runs to completion even if something cancels the surrounding task. This uses no unstructured tasks, and doesn’t generate any race conditions. Inside the shielded block, Task.isCancelled always returns false, so the critical section runs uninterrupted.
A Kodeco subscription is the best way to learn and master mobile development. Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.