WWDC Quick Look 💓 By SwiftGGTeam
SwiftUI の新機能

SwiftUI の新機能

元の動画を見る

ハイライト

2027 年プラットフォームの SwiftUI には、直接ディスクアクセスとスナップショットベースの差分書き込みに対応した新しい Document API、並べ替え可能なコンテナ、Toolbar の表示優先度と自動最小化、任意のビューで使えるスワイプジェスチャ、そして @State のマクロ化と ContentBuilder によるコンパイル性能改善が追加されます。

主要内容

Liquid Glass とウインドウ状態への反応

2027 年版では、SwiftUI アプリはコードを変更しなくても Liquid Glass の更新された外観を自動的に受け取ります。(02:17) macOS 上のカスタム Liquid Glass 要素は interactive としてマークでき、マウスクリックへの反応がより滑らかになります。iPad アプリでは、ウインドウが非アクティブになるとアイコンと文字が自動的に暗くなり、ユーザーが現在のアクティブウインドウを見分けやすくなります。

開発者は appearsActive 環境値を使って、カスタムビューが非アクティブ時にどう表示されるかを細かく制御できます。

struct SidebarFooterView: View {
    @Environment(\.appearsActive) private var appearsActive

    var body: some View {
        MyAccountView()
            .opacity(appearsActive ? 1 : 0.5)
    }
}

ポイント:

  • appearsActive は 2027 年に追加された新しい環境値
  • ウインドウが非アクティブなら false、アクティブなら true を返す
  • カスタムのサイドバー、ツールバーなどに視覚的なフィードバックを与える用途に使える

iPad と Mac のメニューバーは、デフォルトでは重要な操作のアイコンだけを表示します。.labelStyle(.titleAndIcon) を使うと、特定のメニュー項目でアイコンと文字を同時に表示し、目立たせることができます。(03:34)

Toolbar の適応と表示制御

機能が増えるにつれ、Toolbar のボタンも増えていきます。iPhone や小さなウインドウでは、入りきらないボタンをシステムが自動的に隠します。2027 年版では、この問題に対応するための 3 つの新しい API が用意されます。

表示優先度: visibilityPriority(.high) を使うと、重要なボタンをスペース不足時にも優先して残せます。

StickerPageView()
    .toolbar {
        ToolbarItemGroup {
            UndoButton()
            RedoButton()
        }
        .visibilityPriority(.high)
        ToolbarOverflowMenu {
            ChoosePhotoButton()
            ExportAsImageButton()
            ClearAllStickersButton()
        }
        ToolbarItem(placement: .topBarPinnedTrailing) {
            ShareButton()
        }
    }

ポイント:

  • visibilityPriority(.high) は Undo/Redo を重要な操作としてマークし、優先的に表示する
  • ToolbarOverflowMenu は使用頻度の低いボタンを固定的にオーバーフローメニューへ入れる
  • .topBarPinnedTrailing は Share ボタンを常に右側に表示し、隠れないようにする

自動最小化: ユーザーが下にスクロールすると、ナビゲーションバーが自動的に折りたたまれ、コンテンツのための空間を広げます。(07:37)

ScrollView {
    StickerListView()
}
.toolbarMinimizeBehavior(.onScrollDown, for: .navigationBar)

新しい Document API

SwiftUI 既存の FileDocumentReferenceFileDocument プロトコルは、2027 年版で大きく拡張されました。新しい API は、文書作成コンテキスト、ディスク読み書きの性能最適化、文書 URL への直接アクセスを提供します。(08:12)

文書の作成元: アプリ起動時に、複数の文書作成方法を提示できます。

@main
struct Stickers: App {
    var body: some Scene {
        DocumentGroupLaunchScene("ステッカーページを作成") {
            NewDocumentButton("新規ステッカーページ", source: .blank)
            NewDocumentButton("写真からステッカーページ…", source: .photo)
        }

        DocumentGroup { document in
            StickerPageDocumentView(document)
        } { configuration, context in
            StickerPageDocument(configuration: configuration, context: context)
        }
    }
}

extension DocumentCreationSource {
    static let blank = Self(id: "blank")
    static let photo = Self(id: "photo")
}

ポイント:

  • DocumentGroupLaunchScene は起動画面に表示する新規文書ボタンを定義する
  • DocumentCreationSource は空白、写真から作成など、異なる作成元を宣言する
  • 作成クロージャは context 引数を通じて作成元の情報を受け取り、イニシャライザがそれに応じて動作を決める

書き込み最適化: 新しい WritableDocument プロトコルには、書き込み可能な形式リスト、スナップショットメソッド、Writer の 3 つが必要です。(11:25)

@Observable
final class StickerDocument {
    static let writableDocumentTypes: [UTType] = [.stickerDocument]

    @MainActor
    func snapshot(contentType: UTType) async throws -> sending PageSnapshot {
        makeSnapshot()
    }

    func writer(configuration: sending WriteConfiguration) -> sending Writer {
        Writer(contentType: configuration.contentType)
    }
}

struct Writer<Snapshot>: DocumentWriter {
    typealias Snapshot = PageSnapshot
    let contentType: UTType

    nonisolated func write(
        snapshot: sending PageSnapshot, to destination: URL,
        previous: sending PageSnapshot?, progress: consuming Subprogress
    ) async throws {
        // 現在の snapshot と previous snapshot を比較し、変更された部分だけを書き込む。
        // Subprogress を使って書き込みの進捗を報告する。
    }
}

ポイント:

  • snapshot() はある時点の文書全体の状態を返し、sending によってスレッド安全性を保証する
  • Writerwrite メソッドは nonisolated かつ非同期で、ディスク書き込みはバックグラウンドで実行される
  • snapshotprevious を比較することで差分書き込みを実現する
  • Subprogress は保存進捗をシステムに報告するために使う

複数形式への書き出し: 同じ Writer が複数の出力形式をサポートできます。(14:35)

@Observable
final class StickerDocument: WritableDocument {
    static let writableContentTypes: [UTType] = [.stickerDocument, .png]
}

// write メソッド内で contentType に応じて分岐する。
if contentType.conforms(to: .stickerDocument) {
    // カスタムパッケージ形式を書き込む。
} else if contentType.conforms(to: .png) {
    let context = CGContext(/* ... */)
    context.draw(/* ... */)
}

読み込みプロトコル: ReadableDocumentWritableDocument と対になるもので、DocumentReader と組み合わせてディスク読み込みを行います。(13:27)

並べ替え可能なコンテナ

2027 年版では Reorderable API が導入され、ListGrid など任意のコンテナでドラッグによる並べ替えをサポートします。(15:58)

List {
    ForEach(stickers) { sticker in
        StickerListItemView(sticker: sticker)
    }
    .reorderable()
}
.reorderContainer(for: Sticker.self) { difference in
    difference.apply(to: &stickers)
}

同じコードは、並べ替えロジックを変更せずにそのまま LazyVGrid にも使えます。

LazyVGrid {
    ForEach(stickers) { sticker in
        StickerListItemView(sticker: sticker)
    }
    .reorderable()
}
.reorderContainer(for: Sticker.self) { difference in
    difference.apply(to: &stickers)
}

ポイント:

  • .reorderable()ForEach を修飾し、ドラッグ操作を有効にする
  • .reorderContainer(for:) は外側のコンテナを修飾し、並べ替えジェスチャを調整する
  • difference.apply は swift-collections の OrderedDictionary を使って並び順の変更を反映する
  • 初めて watchOS もサポートされる

任意のビューで使えるスワイプジェスチャ

スワイプジェスチャは List だけのものではなくなり、任意のビューに適用できるようになりました。(18:15)

ScrollView {
    LazyVStack {
        ForEach(stickers) { sticker in
            StickerListItemView(sticker: sticker)
                .swipeActions {
                    DeleteButton(sticker: sticker)
                }
        }
    }
}
.swipeActionsContainer()

ポイント:

  • .swipeActions() は個別のリスト項目を修飾する
  • .swipeActionsContainer() は外側の ScrollView を修飾し、すべての子項目のスワイプジェスチャを調整する

Confirmation Dialog と Alert の item バインディング

confirmationDialogalert は、sheet と同じ item バインディングパターンをサポートするようになりました。(18:54)

struct StickerCanvasView: View {
    var stickers: [Sticker]
    @State private var stickerToDelete: Sticker?

    var body: some View {
        ZStack {
            ForEach(stickers) { sticker in
                PlacedStickerView(sticker: sticker)
                    .contextMenu {
                        Button("削除") { stickerToDelete = sticker }
                    }
            }
        }
        .confirmationDialog("削除しますか?", item: $stickerToDelete) { sticker in
            DeleteStickerButton(sticker)
        }
    }
}

ポイント:

  • item: $stickerToDelete はオプショナル値をバインドする
  • 値が nil でないときにダイアログが自動的に表示され、nil になると自動的に消える
  • alert も同じ item バインディングパターンをサポートする

詳細

AsyncImage のキャッシュ改善

以前の AsyncImage は、読み込んだ画像をメモリ内に保持しませんでした。上にスクロールしてから戻ると、画像は再度読み込まれていました。2027 年版では標準 HTTP キャッシュがデフォルトで有効になり、コード変更は不要です。(20:57)

より細かく制御したい場合は、カスタムの URLRequestURLSession を渡せます。

@Observable class StickerStore {
    static let imageSession: URLSession = {
        let config = URLSessionConfiguration.default
        config.urlCache = URLCache(
            memoryCapacity: 64 * 1024 * 1024,
            diskCapacity: 256 * 1024 * 1024)
        return URLSession(configuration: config)
    }()
}

ForEach(pets) { pet in
    AsyncImage(request: URLRequest(
        url: pet.imageURL,
        cachePolicy: .returnCacheDataElseLoad)
    )
}
.asyncImageURLSession(StickerStore.imageSession)

ポイント:

  • URLRequest ではキャッシュポリシー、リクエストヘッダーなどを設定できる
  • asyncImageURLSession 修飾子はグローバルな URLSession を設定し、長期的な構成に向いている
  • デフォルトキャッシュは追加コードなしで自動的に有効になる

@State のマクロ化と遅延初期化

2027 年版で、@State はプロパティラッパーからマクロに変わります。最大の変化は、@State が付いた Observable クラスが一度だけ初期化されることです。(23:08)

@Observable class StickerStore { }

struct StickerStoreView: View {
    @State private var store = StickerStore()

    var body: some View {
        // store は初回初期化時だけ作成されます。親ビューの更新では再作成されません。
    }
}

この挙動は iOS 17、macOS 14、および対応するバージョンまでバックデプロイされています。

移行時の注意: @State 変数にデフォルト値があり、さらに init 内で代入している場合、コンパイルエラーになります。(23:48)

// エラー: 変数 'self.title' が初期化前に使用されています
struct StickerPageView: View {
    @State private var page = StickerPage()
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title)
        self.title = title
    }
}

// 修正: デフォルト値を取り除く。
struct StickerPageView: View {
    @State private var page: StickerPage
    let title: String

    init(title: String) {
        self.page = StickerPage(title: title)
        self.title = title
    }
}

ContentBuilder によるコンパイル性能改善

深くネストした SwiftUI ビューは、コンパイラエラー「この式を妥当な時間内に型チェックできません」を引き起こしがちでした。(24:31)

原因は、SectionGroupForEach などのコンテナがそれぞれ複数のイニシャライザオーバーロードを持ち、コンパイラが型を決定するためにすべての組み合わせを試す必要があったことです。2027 年版では、これらのよく使う builder が ContentBuilder に統一され、型チェックの分岐の大半が解消されます。(26:07)

@ContentBuilder
func stickerLibraryView() -> some View {
    // ...
}

ポイント:

  • ContentBuilderViewBuilder の進化形で、複数の builder 型を統一する
  • コンパイル性能の改善はすべての最小デプロイメントターゲットに適用される
  • Xcode 27 でビルドすると有効になり、対象プラットフォームが 2027 年版でもそれ以前でも関係ない

重要な示唆

1. 新しい Document API で既存の文書アプリを再設計する

FileDocument ベースのアプリを保守しているなら、新しい WritableDocument/ReadableDocument プロトコルへ移行することで、差分書き込み、非同期保存進捗、複数形式への書き出しなどを得られます。入口は DocumentGroupDocumentGroupLaunchScene です。

2. iPad/Mac アプリに適応型 Toolbar を追加する

visibilityPriority(.high) で中核操作をマークし、ToolbarOverflowMenu に二次的なボタンを入れ、.topBarPinnedTrailing で共有ボタンを固定します。.toolbarMinimizeBehavior(.onScrollDown) と組み合わせれば、スクロール時にナビゲーションバーを自動的に折りたたみ、読みやすさを高められます。

3. カスタムのドラッグ並べ替えを Reorderable API に置き換える

以前 onMove や独自の DragGesture でリストの並べ替えを実装していたなら、.reorderable().reorderContainer() に置き換えられます。コードは短くなり、アニメーションとインタラクションは自動で得られ、ListGridLazyVGrid など複数のコンテナに対応します。

4. 任意のスクロールビューにスワイプジェスチャを追加する

スワイプジェスチャはもう List に限られません。LazyVStackScrollView 内の任意の子ビューに .swipeActions() を追加できます。外側のコンテナに .swipeActionsContainer() を付けるだけで、ジェスチャが調整されます。

5. @State のマクロ化によるコンパイルエラーを確認する

Xcode 27 にアップグレードしたら、プロジェクト内の @State 変数が init 内で重複代入されていないか確認します。修正方法は、プロパティ宣言側のデフォルト値を削除し、init 内だけで代入することです。

関連 Session

コメント

GitHub Issues · utterances