Swift 6.3 の新機能まとめ - weak let と Module Selectors

はじめに

2026 年 3 月 24 日に Swift 6.3 がリリースされました。Xcode 26.4 に同梱されています。

個人的に気になった機能をまとめてみました。

weak let (SE-0481)

Swift を書いていて「弱参照は var でしか宣言できない」というのが地味に不便だと感じたことはないでしょうか。Swift 6.3 では weak let が使えるようになり、不変の弱参照を宣言できます。

Swift 6 の strict concurrency で Sendable 準拠が求められる場面が増えましたが、弱参照を持つクラスは var プロパティがあるために Sendable に準拠できないという問題がありました。

// Before Swift 6.3: エラーになる
final class ViewModel: Sendable {
    weak var delegate: SomeDelegate?  // エラー: Sendable 準拠型に var は使えない
}

弱参照は参照先が解放されると自動的に nil になりますが、これは「ストレージが変わった」のではなく「参照先がなくなった」だけ、という考え方です。この解釈に基づいて weak let が導入されました。

// Swift 6.3: weak let が使える!
final class Node: Sendable {
    let value: String
    weak let parent: Node?

    init(value: String, parent: Node? = nil) {
        self.value = value
        self.parent = parent
    }
}

クロージャの [weak self] キャプチャもデフォルトで不変 (let) になります。

final class Service: Sendable {
    let name = "MyService"

    func createTask() -> @Sendable () -> Void {
        return { [weak self] in
            guard let self else { return }
            print("Service: \(self.name)")
            // self = nil  // エラー: 不変キャプチャなので再代入不可
        }
    }
}

Upcoming Feature Flag ImmutableWeakCaptures で利用できます。なお、この機能は SE-0481 として実装されていますが、公式リリースブログでは触れられていません。

@IBOutlet では使えない

ちなみに @IBOutlet weak let を試してみたところ、コンパイルエラーになりました。

class TestViewController: UIViewController {
    @IBOutlet weak let label: UILabel?
    // エラー: '@IBOutlet' requires property to be mutable
}

@IBOutlet は Interface Builder が実行時にプロパティへ値を設定する仕組みなので、var であることが必須です。Storyboard や XIB を使っている場合は従来通り @IBOutlet weak var を使いましょう。

Module Selectors (SE-0491)

新しい :: 構文で、モジュール名を明示して名前の衝突を解消できるようになりました。SwiftUI のエントリポイントを書くときに、自分の型名と SwiftUI 側の App プロトコルが同じ名前になるケースが分かりやすい例です。

import SwiftUI

@main
struct App: SwiftUI::App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

これまでは struct App: App と書くと自分自身に準拠しているように解釈されてエラーになるため、型名を MyApp などに変える必要がありました。SwiftUI::App で「SwiftUI モジュールの App プロトコル」と明示できるので、自分の型を素直に App と名付けられます。

@_specialize 属性 (SE-0460)

ジェネリック関数の特定の型に対して特殊化(specialization)を明示する @_specialize も Swift 6.3 で使えます。Swift 6.3 時点ではアンダースコア付きのままです。

@_specialize(exported: true, where T == Int)
@_specialize(exported: true, where T == String)
public func process<T: Comparable>(_ items: [T]) -> T? {
    return items.max()
}

パフォーマンスが重要な場面で、ジェネリクスのオーバーヘッドを減らしたいときに使えます。

@inline(always) の安定化 (SE-0496)

これまで非公式だった @inline(always) が正式な属性として提供されるようになりました。

@inline(always)
func fastSquare(_ x: Int) -> Int {
    return x * x
}

@inline(always) はインライン化を強制する属性ですが、公式ドキュメントによると Debug ビルドでは効果がありません。Release ビルド(-O-Osize)で機能します。@inline(never) は逆にインライン化を禁止します。指定なしの場合は最適化設定に依存し、Debug(-Onone)ではほぼインライン化されず、Release(-O)ではコンパイラの判断に委ねられます。

@export(implementation) (SE-0497)

@inlinable を付けると、関数の実装が利用元のモジュールでインライン化されます。これにより高速化できますが、ライブラリを更新しても再ビルドしていないアプリには古い実装が残り続けるという問題があります。つまり @inlinable は「この実装をずっと保証します」という約束になってしまいます。

@export(implementation) はこの問題を解消する新しい属性です。関数の実装を最適化のために公開しつつ、ライブラリ更新時にはアプリ側も新しい実装を使います。

@export(implementation)
public func computeHash(_ data: Data) -> UInt64 {
    var hasher = Hasher()
    hasher.combine(data)
    return UInt64(hasher.finalize())
}

違いを整理すると次のようになります。

  • 指定なし - 関数の中身はモジュール外から見えない。実装は自由に変更できる
  • @inlinable - 中身が見えて最適化できる。ただし実装を変えると再ビルドしていないアプリが古い動作のままになる
  • @export(implementation) - 中身が見えて最適化できる。実装を変えてもアプリ側に反映される

ただし、Apple の標準ライブラリのような ABI 安定フレームワーク向けの機能なので、普段のアプリ開発で使うことはあまりなさそうです。

Swift Testing の強化 (SWT-0013, SWT-0014, SWT-0016)

テストフレームワークにもいくつか便利な機能が追加されました。

テストを失敗させずに警告だけ記録できる severity: .warning(SWT-0013)が入りました。たとえば非推奨 API を使っているテストで「テスト自体は通るけど、そろそろ移行したい」という状況を警告として残しておけます。

@Test func testDeprecatedAPI() {
    let apiVersion = 2
    if apiVersion < 3 {
        Issue.record(
            "API v\(apiVersion) は非推奨です。v3 への移行を推奨します。",
            severity: .warning
        )
    }
    #expect(apiVersion > 0)  // テスト自体は成功する
}

try Test.cancel("reason")(SWT-0016)で条件に応じてテストを動的にスキップできるようになりました。CI 環境でのみ実行したいテストや、特定の OS バージョンが必要なテストで使えます。

@Test func testCIOnly() throws {
    guard ProcessInfo.processInfo.environment["CI"] != nil else {
        try Test.cancel("CI 環境ではないためスキップします")
    }
    // CI 環境でのみ実行される処理
}

Attachment.record(value, named: "file.txt")(SWT-0014)でテスト結果にデータを添付する機能も追加されています。テストが失敗したときの調査用にログやレスポンスデータを残しておくと便利です。

@Test func testWithAttachment() throws {
    let jsonData = try JSONEncoder().encode(result)
    Attachment.record(jsonData, named: "api_response.json")
    #expect(result.status == .ok)
}

Swift Build の SPM 統合(プレビュー)

Xcode のビルドシステムは既に Swift Build を使っています。Swift 6.3 で公開されたのは、SPM(swift build コマンド)側でも Swift Build を使えるようにするプレビューです。

なぜ統一が必要なのか

Xcode も SPM も、最終的には llbuild というビルド実行エンジンを使っています。ただし llbuild が受け取る「マニフェスト」(ビルドに必要なタスク一覧とその依存関係を記述したファイル)を生成するツールが異なります。Xcode は .xcodeproj からマニフェストを生成し、SPM は Package.swift から生成します。

このマニフェスト生成ツールの違いが、実際の開発で地味に困るポイントにつながっています。

  • swift build では通るのに Xcode ではエラーになる(またはその逆)
  • ビルド設定の解釈が微妙に違う
  • キャッシュの扱いが違う

Swift Build は、Xcode と SPM の両方でこのマニフェスト生成を統一するツールです。将来的に「Xcode でビルドした結果」と「swift build した結果」が同じになることを目指しているようです。

# Swift Build を使用したビルド(プレビュー)
swift build --build-system swift-build

まだプレビュー段階ですが、ビルド周りの改善もあります。マクロの実装コードを共有ライブラリに切り出す際に swift-syntax のプリビルドバイナリを利用できるようになりました。マクロ専用のライブラリであれば、swift-syntax をソースからコンパイルする必要がなくなります。

パッケージトレイト(SE-0450)はライブラリのオプション機能を ON/OFF できる仕組みです。Swift 6.1 で導入された機能ですが、swift package show-traits コマンドが追加されたのでおさらいしておきます。ライブラリ本体は常に使えますが、トレイトで指定した追加機能や追加依存を利用者側で選択できます。

実際に試してみました。まずライブラリ側で Logging トレイトを定義し、トレイト有効時のみ LOGGING が define されるようにします。

// ライブラリ側の Package.swift (swift-tools-version: 6.1)
let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    traits: [
        .default(enabledTraits: []),
        .trait(name: "Logging", description: "Enable logging support"),
    ],
    targets: [
        .target(
            name: "MyLibrary",
            swiftSettings: [
                .define("LOGGING", .when(traits: ["Logging"])),
            ]
        ),
    ]
)

ライブラリのコードでは #if LOGGING で条件分岐できます。

public struct MyLibrary {
    public static func hello() -> String {
        #if LOGGING
        return "Hello from MyLibrary (Logging enabled)"
        #else
        return "Hello from MyLibrary"
        #endif
    }
}

利用者側は依存宣言の traits: パラメータでトレイトを有効にします。

// 利用者側の Package.swift
dependencies: [
    // Logging トレイトを有効にする
    .package(url: "https://github.com/example/MyLibrary.git",
             from: "1.0.0", traits: ["Logging"]),
]

swift package show-traits でライブラリが提供するトレイトを確認できます。

$ swift package show-traits
Logging - Enable logging support
Metrics - Enable metrics collection

swift build --traits でトレイトを有効にしてビルドできます。トレイトなしでビルドすると LOGGING は define されず、#if LOGGING の分岐に入りません。

# トレイトなしでビルド(LOGGING は無効)
$ swift build
Build complete!

# Logging トレイトを有効にしてビルド(LOGGING が define される)
$ swift build --traits Logging
Build complete!

DocC の実験的機能

Xcode 26.4 で DocC に 3 つの実験的フラグが追加されました。実際に試してみたので、フラグの有無で何が変わるのか見ていきましょう。

Markdown 出力

--enable-experimental-markdown-output を付けると、通常の HTML に加えて Markdown ファイルが生成されます。

docc convert \
  --additional-symbol-graph-dir ./symbols \
  --fallback-display-name MyFramework \
  --fallback-bundle-identifier com.example.myframework \
  --enable-experimental-markdown-output \
  --output-dir ./docs

実際に試したところ、フラグなしでは 87 ファイルだった出力が 106 ファイルに増えました。増えた 19 ファイルはすべて data/documentation/ 以下に生成された .md ファイルです。Jekyll や Hugo などの静的サイトジェネレーターに組み込みやすくなります。

Static HTML コンテンツ

--experimental-transform-for-static-hosting-with-content を付けると、HTML ファイルの中身がガラッと変わります。

フラグなしの場合、<noscript> タグには「This page requires JavaScript.」というメッセージしか入っていません。

<!-- フラグなし -->
<noscript>
  <h1>This page requires JavaScript.</h1>
  <p>Please turn on JavaScript in your browser...</p>
</noscript>

フラグありにすると、ドキュメントの実際の内容がセマンティック HTML として埋め込まれます。

<!-- フラグあり -->
<noscript>
  <article>
    <h1>HelperKit</h1>
    <h2>Topics</h2>
    <h3>Structures</h3>
    <ul>
      <li><a href="config/index.html">
        <code>struct Config</code>
        <p>HelperKit モジュールの Config 型</p>
      </a></li>
      <!-- ... -->
    </ul>
  </article>
</noscript>

<title> も汎用の「Documentation」からページ固有の「HelperKit」に変わります。検索エンジンのクローラーや JavaScript を無効にした環境でもコンテンツが読めるようになるので、SEO の改善が期待できます。

コードブロックアノテーション

--enable-experimental-code-block-annotations を有効にすると、DocC の記事内でコードブロックに表示制御を追加できます。構文はカンマ区切りです。

```swift, nocopy, showLineNumbers, wrap
func example() {
    let a = 1
    print(a)
}
```

実際に試したところ、以下の動作を確認できました。

アノテーション検証結果
nocopyJSON 出力で copyToClipboard: false に変わる
showLineNumbersshowLineNumbers: true に変わる
wrap引数なしで受け付けられる
highlight引数なしのみ受け付けられる

highlight(2-3) のように行番号を指定する構文は現時点ではエラーになりました。実験的機能のため、行指定のハイライトは今後のバージョンで対応されるかもしれません。

まとめ

そろそろ Xcode 26.4 を本格的に使い始めるタイミングですが、まず weak let は積極的に使っていきたいと思っています。Sendable 準拠で weak var が使えず困っていた場面がスッキリ解決しますし、@IBOutlet 以外の弱参照は基本的に weak let に置き換えられそうです。個人的には swift-format に「再代入していない weak varweak let に変換する」ルールが追加されてほしいなと思っています。よければ皆さんも試してみてください。

参考リンク