12 Swift 6.4 Features, Ranked by How Useful They Are
Every year Apple’s “What’s new in Swift” session drops thirty minutes of language features, and ever 2026-9-30 08:2:16 Author: hackernoon.com(查看原文) 阅读量:2 收藏

Every year Apple’s “What’s new in Swift” session drops thirty minutes of language features, and every year the recaps list them in the order they appeared on stage. That order is optimized for a keynote, not for your sprint.

This one is sorted differently. The question for every feature is the same: how far is it from a diff I can open tomorrow? Some of these are one-line changes that delete a hack you’ve been apologizing for since 2021. Others are powerful, well-designed, and will not touch your app code for a year. Both deserve to be understood. Only one deserves a ticket.

One thing to fix in your mental model before we start: this year’s session covers two releases, Swift 6.3 and 6.4. Some of what follows is already in your toolchain.

Each feature gets three lines: Lands in, Reach for it when, and a Verdict. Let’s go.

Tier 1: Monday-morning changes

1. XCTest and Swift Testing finally speak the same language

Lands in: Swift 6.4

If you’ve stalled on migrating to Swift Testing, it was almost certainly because of this: every test target has a pile of helpers built on XCTAssert*, and rewriting them all at once was never going to get prioritized.

That blocker is gone. XCTest assertion failures, when called from a Swift Testing test, are now reported as test issues. And it works the other way too: #expect runs correctly inside an XCTestCase.

// Written in 2019. Still fine.
func assertValidCart(_ cart: Cart) {
    XCTAssertFalse(cart.items.isEmpty, "cart must not be empty")
    XCTAssertGreaterThan(cart.total, 0)
}
// A brand-new Swift Testing test can call it directly.
@Test func checkoutBuildsCart() throws {
    let cart = try CartBuilder()
        .add(.sku("SNK-001"), quantity: 2)
        .build()    assertValidCart(cart)          // XCTest assertion, reported as an issue
    #expect(cart.total == 3_998)   // Swift Testing expectation
}// And the reverse: a legacy XCTestCase using #expect.
final class LegacyCartTests: XCTestCase {
    func testWelcomeCouponApplies() throws {
        let cart = try CartBuilder()
            .add(.sku("SNK-001"))
            .apply(coupon: "WELCOME10")
            .build()        #expect(cart.discount == 0.10)
    }
}

The practical consequence: you can write one set of assertion helpers and have them behave identically regardless of which runner calls them. Migration becomes a per-file decision instead of a per-target big bang.

Reach for it when: you have more than a handful of XCTest-based helpers and want to move to Swift Testing incrementally.

Verdict: Ship it. This is the single highest-ROI change in the session for any team with a real test suite. There’s a trap here, though. See the pitfalls section before you flip the switch.

2. @diagnose: warnings you control per declaration, not per target

Lands in: the 6.4 cycle

Deprecation warnings have a lifecycle problem. The API gets deprecated. You can’t migrate yet, because the replacement is a different shape and the migration is a quarter of work. So you either live with yellow triangles in the build log until everyone stops reading them, or you disable the warning for the whole module and lose it forever.

@diagnose gives you a third option: change how a specific warning group behaves inside a specific declaration

@diagnose(DeprecatedDeclaration, as: ignored,
          reason: "PaymentsKit v3 lands next quarter, tracked in PAY-4821")
func tokenize(_ card: Card) -> Token {
    LegacyPayments.tokenize(card)   // deprecated, and we know
}
// Turn ON a check that's off by default, only where it matters.
@diagnose(StrictMemorySafety, as: warning)
func decodeFrame(_ bytes: UnsafeRawBufferPointer) -> Frame {
    // every unsafe access here now gets audited by the compiler
}// Promote future errors to errors today, so nobody adds new instances.
@diagnose(ErrorInFutureSwiftVersion, as: error)
func parsePrice(_ raw: String) -> Decimal {
    // ...
}

Three modes, three very different uses: ignored for scoped suppression, warning for opting into stricter checks in security-sensitive code, error for ratcheting a migration forward one function at a time.

Reach for it when: you’re mid-migration to Swift 6 language mode, or you have a security boundary you’d like the compiler to be paranoid about.

Verdict: Ship it. Then add a lint rule that rejects as: ignored without a reason:. That parameter exists for a reason of its own.

3. Concurrency stops nagging you about the wrong things

Lands in: Swift 6.4

This is a bundle of small fixes, and each one deletes a workaround you’ve probably written.

Silently swallowed Task errors now warn. This one catches real bugs. A fire-and-forget task whose closure throws would just... drop the error. Now the compiler tells you.

// Before 6.4: compiles, error vanishes into the void.
// 6.4: warning — you're ignoring a thrown error.
Task { try await syncEngine.push(pendingChanges) }
// Option A: handle it inside the task.
Task {
    do {
        try await syncEngine.push(pendingChanges)
    } catch {
        logger.error("push failed: \(error)")
    }
}// Option B: keep the handle and check later.
let push = Task { try await syncEngine.push(pendingChanges) }
defer { await telemetry.flush() }   // async calls in defer are allowed now
try await push.value

Note the defer in Option B. Calling async functions from a defer block was previously forbidden. Not anymore.

weak let exists, and it unblocks Sendable. Any class holding a weak var delegate couldn't be Sendable without @unchecked. That was a lie you told the compiler to make it stop talking. Now you can make the weak reference an immutable binding, and Sendable checking is fine with it.

// Before: a lie.
final class ImagePrefetcher: @unchecked Sendable {
    weak var delegate: PrefetchDelegate?
}
// After: honest.
final class ImagePrefetcher: Sendable {
    weak let delegate: PrefetchDelegate?    init(delegate: PrefetchDelegate?) {
        self.delegate = delegate
    }
}

The reference can still go nil when the delegate deallocates. What changed is that the binding is immutable, and that's what Sendable cares about.

~Sendable says the quiet part out loud. You can now declare a type explicitly non-Sendable. Subclasses are free to opt back in.

class Screen: ~Sendable { }
final class SplashScreen: Screen, @unchecked Sendable { }

A second memberwise initializer for mixed-visibility structs. A struct with internal stored properties and a private one with a default used to get a single private memberwise init, which meant hand-writing the public one. Now the compiler generates both.

swift

struct OrderDraft {
    var items: [LineItem]
    var address: Address
    private var validationCache: [String: Bool] = [:]
}
// From another file in the same module — this now just works:
let draft = OrderDraft(items: items, address: address)

And some/any next to an optional no longer needs parentheses. Small. Satisfying.

Reach for it when: you grep for @unchecked Sendable and find more than zero results.

Verdict: Ship it. Run the 6.4 compiler over your codebase and treat every new “ignored error” warning as a bug report you didn’t have to write.

4. withTaskCancellationShield: finish the write before you bail

Lands in: the 6.4 cycle (standard library)

Checking Task.isCancelled before expensive work is the right default. Except for the moment when you're halfway through writing a file, committing a transaction, or flushing an analytics journal. Bailing there doesn't save work. It corrupts state.

Inside the shield, every cancellation check reads false, all the way down the call stack.

swift:

extension AnalyticsJournal {
    func flush(_ batch: [Event]) {
        // Everything before this point may honor cancellation.
        withTaskCancellationShield {
            // Inside: Task.isCancelled == false, so lower layers
            // won't abort mid-write.
            storage.append(batch)
            storage.commit()
        }
    }
}

Apple’s guidance is worth repeating: keep the shielded region short. This is for “finish or roll back what I already started,” not for “ignore cancellation because it’s inconvenient.”

Reach for it when: you have a write path that’s already had a bug caused by cancellation landing between two dependent operations.

Verdict: Ship it, surgically. One shield per write path, none anywhere else.

5. ProgressManager: Progress for the async/await era

Lands in: the 6.3/6.4 cycle (Foundation)

Foundation’s old Progress was designed around KVO and implicit parent/child registration via thread-local state, which is exactly the kind of thing that stops making sense once your code is structured concurrency. ProgressManager separates composition (who owns which share of the work) from reporting (who's watching), and it does it with types instead of magic.

let manager = ProgressManager(totalCount: 100)
// Hand out a slice of the total to a child operation.
try await exporter.exportOrders(manager.subprogress(assigningCount: 100))extension OrderExporter {
    func exportOrders(_ progress: consuming Subprogress? = nil) async throws {
        let steps = progress?.start(totalCount: 3)        try await fetch();   steps?.complete(count: 1)
        try await encode();  steps?.complete(count: 1)
        try await upload();  steps?.complete(count: 1)
    }
}// Observe from the UI side.
Task {
    for await fraction in Observations({ manager.fractionCompleted }) {
        exportButton.title = "Exporting \(Int(fraction * 100))%"
    }
}

Two details to notice. The child function takes consuming Subprogress?, so the caller can pass nil when nobody's watching and the function doesn't care. And the manager supports typed metadata, so you can attach domain-specific values (bytes transferred, items processed) and summarize them across the tree without stringly-typed user-info dictionaries.

Reach for it when: any multi-stage async flow that shows a progress bar. Uploads, exports, media processing, initial sync.

Verdict: Ship it for new flows. Don’t rewrite working Progress code just to use it.

6. anyAppleOS: one availability line instead of five

Lands in: the 6.3/6.4 cycle

Apple aligned OS version numbers last year (everything is 26, then 27). This year Swift cashes that in: a single platform name covers all of them.

// Before
@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showLiveActivityGlance() { }
// After
@available(anyAppleOS 27, *)
func showLiveActivityGlance() { }// Set the default, then carve out exceptions.
@available(anyAppleOS 27, *)
@available(tvOS, unavailable)
func startCheckoutHandoff() { }// Works in conditional compilation too.
#if os(anyAppleOS)
import WidgetKit
#endif

Reach for it when: you ship a framework or app extension across three or more Apple platforms.

Verdict: Ship it if your minimum versions line up across platforms. If they don’t, this does nothing for you.

7. Module selectors (::): name collisions, resolved for real

Lands in: Swift 6.3

Two modules export a type with the same name. You disambiguate with ModuleName.TypeName. This works until the module also contains a type with the same name as the module, at which point Swift prefers the type, looks inside it, finds nothing, and errors.

The new :: syntax makes the left side always a module name.

import SwiftUI
import DesignSystem   // also exports a `View` type
struct ProductCard: SwiftUI::View {
    var body: some SwiftUI::View {
        DesignSystem::Card { /* ... */ }
    }
}

It works on members too, which matters when two modules add same-named methods to one type via extensions:

// Module Fulfillment adds `cancel()` to Order.
// Module Payments also adds `cancel()` to Order.
order.Payments::cancel()   // refund, don't un-ship

Apple’s own note on this: it’s for collisions between modules you don’t control, and for macro expansions where you can’t predict what else got imported. Do not design APIs that rely on it.

Reach for it when: the View-vs-View problem, or you write macros.

Verdict: Ship it as a fix, never as a design.

Lands in: Swift 6.4

Beyond the XCTest bridge, Swift Testing picked up three things that address real CI pain:

swift:

@Test(arguments: Storefront.all)
func priceFormatting(storefront: Storefront) throws {
    // Skip *this argument*, not the whole test.
    if storefront.currency == nil {
        try Test.cancel("no currency configured for \(storefront.id)")
    }
    let text = PriceFormatter(storefront: storefront).string(from: 1_999)    // Worth a look, not worth blocking the merge queue.
    if text.count > 12 {
        Issue.record("\(storefront.id) renders unusually long", severity: .warning)
    }    #expect(text.contains("19"))
}

Test.cancel is the interesting one in parameterized tests: you cancel individual arguments instead of either running them to a meaningless completion or failing the whole test. Issue.record(severity: .warning) surfaces things you want a human to notice without turning the pipeline red.

And swift test gained a repeat mode: rerun until pass or until fail, with a cap on repetitions. In "until pass" mode only the failing tests rerun. Flaky-test hunting, built in.

Verdict: Ship it. Test.cancel alone will clean up a lot of guard ... else { return } at the top of parameterized tests.

Tier 3: Performance, with receipts

Everything here is real, well-designed, and will not show up in most app code this year. It will show up in the frameworks your app depends on. Know it exists; reach for it when the profiler tells you to.

8. borrow / mutate accessors: computed properties without the copy

Lands in: Swift 6.4

A computed property with get/set moves the value by copy. For an Int, who cares. For an InlineArray of 256 Ints, that's a two-kilobyte struct copied out and back just to touch one element.

borrow gives read access to the storage in place. mutate gives exclusive write access in place.

struct FrameBuffer: ~Copyable {
    private let storage: UnsafeMutablePointer<[1024 of UInt8]>
    var pixels: [1024 of UInt8] {
        borrow { storage.pointee }
        mutate { &storage.pointee }
    }
}buffer.pixels[512] = 0xFF   // in place. No 1 KB round trip.

As a bonus, the property can now hold non-copyable values, which get/set fundamentally couldn't.

Verdict: Reach for it in wrappers around large value types. This is the most practical thing in the ownership block.

9. Continuation: resume-once, checked at compile time

Lands in: the 6.3/6.4 cycle (standard library)

Every codebase bridging callbacks to async has the same two options: CheckedContinuation (safe, runtime cost) or UnsafeContinuation (fast, and one double-resume away from a crash in production). The new Continuation type verifies at compile time that you resume it exactly once. Safer than checked, as cheap as unsafe.

The session names the type and the guarantee without walking through the call site, so check the standard library docs for the exact spelling before you write it in a code review.

Verdict: Adopt it in new bridging code. Sweep the old UnsafeContinuation sites when you have a quiet week.

10. MutableRef: hoist the dictionary lookup out of the loop

Lands in: the 6.3/6.4 cycle (standard library)

You have a loop that increments counts[key] on every iteration. The lookup is the same every time. Until now, holding a dictionary slot "open" across a loop required moving the loop into a nested function and passing the slot as inout. Obscure, and everyone who's found it has felt slightly dirty.

func countMentions(of tag: Tag, in posts: [Post], into counts: inout [Tag: Int]) {
    var slot = MutableRef(&counts[tag, default: 0])   // one lookup
    for post in posts where post.tags.contains(tag) {
        slot.value += 1                               // no re-hash
    }
}

Ref (read) and MutableRef (write) are non-escapable, so the compiler knows the access ends when the variable leaves scope. They can be stored, passed, returned, and used in generics, which means APIs that hand out a live reference to one of their properties are now expressible.

Verdict: Reach for it in hot loops the profiler has already pointed at.

11. Iterable: for loops that borrow instead of copy

Lands in: Swift 6.4

Sequence copies each element out to the loop variable. Iterable lets the loop borrow them, in batches, via Span. No retain/release for reference types or copy-on-write containers, non-copyable elements are supported, and the loop can throw.

The constraints matter: you can’t mutate the collection while iterating (exclusivity is enforced), and for prefers Sequence when a type conforms to both. So adopting Iterable on a type that already has Sequence changes nothing.

Verdict: Know it exists. Author it in collection-heavy library code. Don’t expect free speedups elsewhere.

12. @inline(always) and @specialized: telling the optimizer what you know

Lands in: @inline(always) in 6.3, @specialized in 6.3

@inline(never) has existed forever. It now has a twin that forces inlining even when the optimizer's heuristics say no. For class methods, pair it with final or dynamic dispatch will still get in the way.

@specialized asks the compiler to emit a concrete version of a generic function for a type you know is hot, which matters most in libraries where the compiler can't see the call sites.

@inline(always)
func clamp01(_ x: Float) -> Float { min(max(x, 0), 1) }
@specialized(where Bytes == [UInt8])
func checksum<Bytes: Sequence<UInt8>>(_ bytes: Bytes) -> UInt32 {
    // ...
}

Verdict: Measure first. Both can make a binary bigger and slower when applied on a hunch.

The traps nobody puts in the recap

  1. The XCTest bridge reports failures as warnings by default. An XCTAssert that fails inside a Swift Testing test does not fail the test until you opt in via the Xcode build settings. Flip that switch on day one, or you'll have "green" runs that are quietly losing signal.
  2. @diagnose(..., as: ignored) is debt with a nicer syntax. Require reason: in code review. Grep for it quarterly.
  3. @inline(always) and @specialized are not free. Apple describes them as tools you "usually won't need." Believe that. Profile, apply, re-profile.
  4. Iterable won't replace Sequence automatically. If your type has both, for picks Sequence. No silent speedup.
  5. weak let doesn't make the reference permanent. It can still go nil. What you gain is that the immutable binding no longer blocks Sendable.
  6. anyAppleOS requires aligned minimums. Different minimum versions per platform means you're back to the long form.

What I’d do this week

  • Open a branch. Compile with 6.4. Fix every new “ignored thrown error” warning. That’s your free bug sweep.
  • Replace @unchecked Sendable on delegate-holding classes with weak let.
  • Turn on XCTest-failure promotion in Swift Testing, then migrate one test file.
  • Put a cancellation shield around your one scariest write path.
  • Bookmark borrow/mutate and MutableRef for the next time Instruments shows you a copy you didn't ask for.

Everything else in this session (Swift-to-C via @C, the official Android SDK, Wasm at 35–40x faster bridging, embedded Swift growing its subset) is genuinely exciting and belongs in a different article, one about where Swift is going rather than what lands in your app.


文章来源: https://hackernoon.com/12-swift-64-features-ranked-by-how-useful-they-are?source=rss
如有侵权请联系:admin#unsafe.sh