Swift 6.4の新機能5つをSwift 6.3と比べて検証|これまで見逃していたテスト失敗と、iOS 27以降に限られる機能

Swift 6.4がXcode 27に入り、テスト結果の報告と非同期処理の書き方が変わった

Swiftの開発チームは2026年9月15日、Swift 6.4を公開しました。手元のXcode 27.0に入っているSwiftも6.4で、Xcode 26.5のSwiftは6.3.2です。Xcodeを27へ上げると、iOSアプリのコードもテストもSwift 6.4でビルドされます。

Swift 6.4には、言語、標準ライブラリ、テスト、ビルドツールにまたがる変更が入っています。この記事では、そのうちアプリ開発で日常的に書くコードに関わる5つ(Swift TestingとXCTestの相互運用、deferの中のawait、キャンセルを無視して後片付けする仕組み、some P?の書き方、警告の扱いを関数ごとに変える@diagnose)を取り上げます。それぞれを最小のサンプルで書き、Swift 6.4とSwift 6.3.2で実際にビルド・実行して結果を比べました。

読み終えると、Xcode 27へ上げたときにテスト結果がどう変わるか、どの新機能をすぐ使えて、どれがiOS 27以降に限られるかを判断できます。

検証で分かった3つのこと

Swift 6.4では、これまで素通りしていたテストの失敗が報告される

テスト

@Test内のXCTAssert失敗が出る

Swift 6.3.2では失敗しても成功扱いだった。6.4では、プロジェクトの種類と設定に応じて警告か失敗になる。

非同期処理

deferの中でawaitできる

エラーで関数を抜けるときも、非同期の後片付けが終わってから呼び出し元へ戻る。

使えるOS

withTaskCancellationShieldはiOS 27以降

キャンセルを無視して後片付けする関数だけ、iOS 27・macOS 27以降が必要。文法の変更はiOS 17向けでもビルドできた。

5つの変更は、Swift 6.3.2ではビルドエラーか、テスト失敗の見逃しになった

検証は2026年10月20日、MacBook(Apple M4、macOS 27.0.1)で行いました。Swift 6.4はXcode 27.0(ビルド27A266a)、Swift 6.3.2はXcode 26.5(17F42)に入っているものを、環境変数DEVELOPER_DIRで切り替えて使っています。外部のライブラリは使っていません。

同じコードをSwift 6.3.2と6.4でビルドした結果

@Test内のXCTAssert

6.3.2: 失敗しても成功扱い

6.4: 警告またはテスト失敗

XCTest内の#expect

6.3.2: 失敗しても成功扱い

6.4: テスト失敗

deferの中のawait

6.3.2: ビルドエラー

6.4: 動く(iOS 17向けも可)

withTaskCancellationShield

6.3.2: 関数が見つからない

6.4: 動く(iOS 27以降)

some Shape? と any Shape?

6.3.2: ビルドエラー

6.4: 動く(iOS 17向けも可)

@diagnose

6.3.2: 未知の属性でエラー

6.4: 関数ごとに警告の扱いを変更

最初の2枚は、1つの変更(Swift TestingとXCTestの相互運用)の両方向。「iOS 17向けも可」は、iOSシミュレータ用SDK 27.0で配布対象をiOS 17.0にして型検査し、エラーが出なかったことを指す。

Swift 6.3.2で書けなかったものは、ビルドエラーになるのですぐ気づけます。注意が必要なのは比較カードの最初の2つ(@Test内のXCTAssertとXCTest内の#expect)で、Swift 6.3.2ではテストの失敗が何も表示されずに成功扱いになっていました。Xcode 27へ上げたとき、これまで成功していたテストが警告や失敗に変わる可能性があります。

Swift Testingの@Testの中で失敗したXCTAssertが、報告されるようになった

XCTestからSwift Testingへ移行している途中のプロジェクトでは、XCTestのXCTAssertEqualを使った補助関数を、Swift Testingの@Testから呼んでいることがあります。Swift 6.3.2まではこの組み合わせでXCTAssertが失敗しても無視され、テストは成功と表示されていました。逆に、XCTestのテストケースの中で#expectが失敗した場合も同じでした。

Swift 6.4では、提案ST-0021により、両方の失敗が報告されるようになりました。次の小さなパッケージで確かめています。

import Testing
import XCTest
@testable import PriceKit

// 税込価格(10%、切り捨て)を返す taxIncluded() のテスト
@Test func intentionalFailure() {
    XCTAssertEqual(taxIncluded(999), 1100)   // 正しくは1098なので失敗する
}

final class LegacyFailureTests: XCTestCase {
    func testExpectFailureInsideXCTest() {
        #expect(taxIncluded(999) == 1100)    // こちらも失敗する
    }
}

swift testの結果は、Swift 6.4かどうかに加えて、Package.swiftの先頭に書くswift-tools-versionによって変わりました。

# Swift 6.3.2(swift-tools-version: 6.2)
✔ Test intentionalFailure() passed after 0.001 seconds.

# Swift 6.4(swift-tools-version: 6.2)
⚠︎ Test intentionalFailure() recorded a warning at PriceTests.swift:13:1: Issue recorded
⚠︎ Test intentionalFailure() recorded a warning at PriceTests.swift:13:1: An API was misused
↳ Replace XCTest API such as 'XCTAssert' with a Swift Testing equivalent such as '#expect'.
✔ Test intentionalFailure() passed after 0.001 seconds with 2 warnings.

# Swift 6.4(swift-tools-version: 6.4)
✘ Test intentionalFailure() recorded an issue at PriceTests.swift:13:1: Issue recorded
⚠︎ Test intentionalFailure() recorded a warning at PriceTests.swift:13:1: An API was misused
✘ Test intentionalFailure() failed after 0.001 seconds with 2 issues (including 1 warning).

出力のPriceTests.swift:13:1は、検証に使ったファイル内のXCTAssertEqualの行です。XCTestの中の#expectの失敗は、Swift 6.4で試した3つの条件(tools-version 6.2、6.4、6.4で環境変数SWIFT_TESTING_XCTEST_INTEROP_MODEをlimitedに指定)のすべてで、次のようにテスト失敗として報告されました。Swift 6.3.2では同じテストがpassedでした。Xcodeのプロジェクトをxcodebuild testで実行した場合も、同じテストは失敗になりました。

error: -[PriceKitTests.LegacyFailureTests testExpectFailureInsideXCTest] : Expectation failed: taxIncluded(999) == 1100
Test Case '-[PriceKitTests.LegacyFailureTests testExpectFailureInsideXCTest]' failed (0.001 seconds).

警告になるか失敗になるかを切り替える仕組みを、ST-0021は「相互運用モード」と呼んでいます。モードは4つあり、違いが出るのは@Testの中で使ったXCTestのAPI(XCTAssertなど)の扱いです。limitedは失敗を警告として表示し、completeはテスト失敗にし、strictはXCTestのAPIを使った時点でテストを止めます。noneは相互運用をしない設定で、Swift 6.3.2までと同じように失敗を見逃します。XCTestの中の#expectの失敗は、none以外のモードでテスト失敗になります。既定のモードは、Swiftパッケージではswift-tools-versionで決まります。Xcodeのプロジェクトについては、Appleによるとテストプランの設定で決まり、Xcode 27より前に作ったテストプランはlimited、Xcode 27で新しく作ったプロジェクトはcompleteが既定です。筆者がテストプランのない既存形式のプロジェクトで試した結果も、警告(limited)でした。

@Test内のXCTAssert失敗が、警告と失敗のどちらになるか(Swift 6.4で実測)

Swiftパッケージ:tools-version 6.2

警告(limited)

Package.swiftのswift-tools-versionが6.2のまま。警告2件が出て、テスト自体は成功。

Swiftパッケージ:tools-version 6.4

テスト失敗(complete)

swift-tools-versionを6.4へ上げると、同じ箇所がテスト失敗になる。

Xcodeのプロジェクト(テストプランなし、既存形式)

警告(limited)

テストプランのないmacOS向けプロジェクトをxcodebuild testで実行。既定では警告で、テスト自体は成功。Appleによると、Xcode 27で新しく作ったプロジェクトの既定はcomplete。

環境変数で上書き

SWIFT_TESTING_XCTEST_INTEROP_MODE

swift testではlimitedで警告、completeで失敗になった。xcodebuild testでは先頭にTEST_RUNNER_を付けた環境変数でcompleteを指定し、失敗になった。Xcodeではテストプランの設定でも変えられる(Apple)。

警告・失敗の結果は、いずれもXcode 27.0(Swift 6.4)で筆者が確認。Swiftパッケージではtools-version 6.4以上でcompleteになること、Xcodeのテストプランごとの既定は、AppleのWWDC26セッション「Migrate to Swift Testing」の説明による。ST-0021はオープンソース版のXCTestを対象にした提案で、Xcodeに入っているXCTestの扱いはAppleの説明に従う。

Swiftパッケージでtools-versionを6.4へ上げたり、環境変数でcompleteを指定したりすると、これまで隠れていた失敗がCIで一斉にテスト失敗になることがあります。今回の検証では、成功したXCTAssertには警告が出ず、失敗した箇所に「Issue recorded」の警告が出ました。この警告を探すと、失敗しているXCTAssertを見つけられます。

deferの中でawaitでき、エラーで抜けても後片付けが終わるまで待つ

deferは、関数を抜けるときに必ず実行する処理を書く構文です。Swift 6.3.2まではdeferの中でawaitを書けなかったため、ログの書き出しや接続の切断のような非同期の後片付けは、Task { }で投げっぱなしにするか、すべてのreturnとthrowの前に手で書く必要がありました。Swift 6.4では、async関数の中であればdeferでawaitできます(提案SE-0493)。

func upload(fail: Bool) async throws {
    print("upload(fail: \(fail)) 開始")
    defer { await flushLog(fail ? "失敗時" : "成功時") }   // 0.1秒かけてログを書き出す
    try await Task.sleep(for: .milliseconds(50))
    if fail { throw UploadError() }
    print("  本体: アップロード完了")
}
upload(fail: false) 開始
  本体: アップロード完了
  [defer] 成功時: ログを書き出しました
upload(fail: true) 開始
  [defer] 失敗時: ログを書き出しました
呼び出し元: エラーを受け取りました

エラーで抜けた場合も、deferの中の非同期処理が終わってから呼び出し元のcatchへ進みました。後片付けの完了を前提に次の処理を書けるので、Task { }で投げっぱなしにする書き方を置き換えられます。Swift 6.3.2では同じコードが'async' call cannot occur in a defer bodyというエラーになります。

キャンセル済みのタスクでも後片付けを走らせるwithTaskCancellationShieldは、iOS 27以降で使える

画面を閉じたときなどにタスクがキャンセルされると、その中のTask.sleepやネットワーク処理はキャンセルを検知して途中で止まります。下書きの保存のように、キャンセルされても最後まで終わらせたい処理では、この動きが保存漏れの原因になります。Swift 6.4のwithTaskCancellationShield(提案SE-0504)で囲んだ部分では、タスクのキャンセル状態が見えなくなります。

let task = Task {
    withUnsafeCurrentTask { $0?.cancel() }            // タスクをすぐキャンセル
    print("シールドなし:", await saveDraft())          // 中で Task.sleep を待つ関数
    let shielded = await withTaskCancellationShield {
        print("シールド内のキャンセル済み:", Task.isCancelled)
        return await saveDraft()
    }
    print("シールドあり:", shielded)
    print("シールドを出たあとのキャンセル済み:", Task.isCancelled)
}
シールドなし: 保存できなかった(CancellationError)
シールド内のキャンセル済み: false
シールドあり: 保存できた
シールドを出たあとのキャンセル済み: true

シールドの中ではTask.isCancelledがfalseになり、保存が最後まで進みました。シールドを出ると、キャンセル済みの状態に戻ります。前の節のdeferの中で使えば、キャンセルされても途中で止めたくない後片付けを書けます。

この関数には、使える配布対象のOSに条件があります。配布対象を変えて型検査した結果は次のとおりです。

配布対象のiOSを変えて型検査した結果

some Shape? / deferのawait / @diagnose

iOS 17.0でもOK

iOS 17.0・26.0・27.0のどれを配布対象にしてもエラーなし。

withTaskCancellationShield

iOS 27.0以上が必要

iOS 17.0・26.0では「only available in iOS 27.0 or newer」。macOSでも27.0以上が必要。

Xcode 27.0のiOSシミュレータ用SDK 27.0で、配布対象(-target)だけを変えて型検査した結果。端末やシミュレータでの実行はしておらず、ビルドが通るかどうかだけを確かめている。

iOS 26以前も対象にするアプリでは、if #available(iOS 27, *)で分けて使います。

「見つからなければnil」を返すsome Shape?と、関数ごとに警告を変える@diagnose

残りの2つは、戻り値の型を短く書ける変更と、警告の扱いを関数ごとに決められる変更です。

1つ目は、プロトコル型の戻り値をオプショナル型にする書き方です。例のShapeはサンプル用に定義した独自のプロトコルで、SquareとCircleはそれに準拠する構造体です。Swift 6.3.2では(some Shape)?や(any Shape)?と括弧が必要で、括弧なしのsome Shape?はエラーでした。Swift 6.4では括弧なしで書け、意味は括弧付きと同じです(提案SE-0521)。

func findSquare(side: Double) -> some Shape? {    // Swift 6.3.2 では (some Shape)? と書く必要があった
    side > 0 ? Square(side: side) : nil
}
func pickShape(_ key: String) -> any Shape? { ... }

2つ目の@diagnoseは、警告をエラーとして扱う設定(-warnings-as-errorsなど)をモジュール全体に入れているときに、特定の関数や型の中だけ扱いを変える属性です(提案SE-0522)。非推奨APIの使用をエラーにする-Werror DeprecatedDeclarationを付けてビルドすると、次の結果になりました。

@diagnose(DeprecatedDeclaration, as: warning, reason: "旧サーバー対応が終わるまで残す")
func bridgeToLegacy() -> Int { oldAPI() }          // → 警告

@diagnose(DeprecatedDeclaration, as: ignored, reason: "テスト用の比較")
func compareForTest() -> Bool { oldAPI() != newAPI() }   // → 何も出ない

func elsewhere() -> Int { oldAPI() }               // → エラー(モジュール全体の設定どおり)

reason:に理由を書けるので、例外にした場所と理由がコードに残ります。属性に指定するDeprecatedDeclarationのようなグループ名は、警告文の末尾に[#DeprecatedDeclaration]の形で表示されていました。

Xcode 27へ上げるときに確認する4つのこと

ここまでの結果から、Swift 6.4へ移るときの確認事項を4つにまとめます。

1. XCTest内の#expect失敗を直す

Xcode 27へ上げてテストを実行すると、XCTestのテストケースの中で失敗している#expectは、既定の設定のままでもテスト失敗になる(swift testとxcodebuild testの両方で確認)。CIが落ちたら、まずここを直す。

2. @Test内のXCTAssertの警告を洗い出す

tools-version 6.4未満のパッケージ、Xcode 27より前に作ったテストプラン、テストプランのない既存のプロジェクト(筆者の実測)では、既定で警告として出る。「Issue recorded」の警告が出た箇所を確認し、原因がコードのバグか期待値の誤りかを確かめて直す。

3. 直してからcompleteへ切り替える

修正とXCTAssertから#expectへの置き換えが済んだら、パッケージはtools-versionを6.4へ上げる。Xcodeのプロジェクトは、テストプランを使っていればその設定でcompleteを選ぶ。テストプランがなければ、xcodebuild testの環境変数TEST_RUNNER_SWIFT_TESTING_XCTEST_INTEROP_MODE=completeで指定できた。以後、同じ種類の失敗はテスト失敗として止まる。

4. 新機能は配布対象のOSで選ぶ

deferのawait、some Shape?、@diagnoseはiOS 17向けでもビルドできた。withTaskCancellationShieldはiOS 27以降なので、if #availableで分ける。

Swift 6.4にはこのほか、外部プログラムを実行するライブラリSubprocessの1.0や、Swift Package Managerの既定のビルド方式がSwift Buildになる変更も含まれています。全体の一覧はSwift.orgの発表記事で確認できます。