ハイライト
今回の SwiftUI のアップデートは広範囲にわたりますが、もし一つだけ覚えておくとしたら、これです。過去 5 年間、リッチテキスト編集を必要とする SwiftUI アプリは
UITextViewをブリッジするしかなく、UIViewRepresentableのボイラープレートを大量に書く必要がありました。今やTextEditorはAttributedString、選択範囲の管理、検索ナビゲーションをネイティブにサポートし、このブリッジ層をついに撤去できるようになりました。
主要内容
太字、斜体、色付きハイライトをサポートするメモアプリを書くこと——これが過去の SwiftUI でどれほど苦痛だったか。答えはこうです。SwiftUI の TextEditor を諦めて UIViewRepresentable で UITextView をラップし、選択範囲の同期、キーボードツールバー、Undo スタックを自前で処理し、さらに NSAttributedString と SwiftUI の状態を結びつける必要がありました。一見シンプルな機能のために、200〜300 行のグルーコードを書くのが当たり前だったのです。WWDC25 の SwiftUI はこの道筋を一気にまっすぐにしました。TextEditor は AttributedString バインディングを直接受け取れるようになり、選択範囲の管理もファーストクラスへと格上げされました。
Liquid Glass は同じアップデートにおけるもう一つの主軸です。その影響範囲はビジュアルシステム全体に及び、単一 API の境界をはるかに超えています。システム標準の TabBar、Toolbar、Button は新しい SDK の下で自動的に新しい外観を獲得します。しかし、これまで手書きしていたガラス風ブラー、カスタム背景色、ZStack による重ね合わせは、新しいマテリアルと衝突する可能性があります。Apple はカスタム View もこのマテリアル言語に参加できるよう glassEffect() 修飾子を提供し、加えてツールバーボタンをグループ化する ToolbarSpacer を新設しました。さらに @Animatable マクロが Animatable プロトコルを自動合成し、visionOS では manipulable() によって任意の 3D View が手によるグラブとスケーリングを得られるようになります——iOS、iPadOS、macOS、visionOS 全プラットフォームを同期して進めるアップデートだといえます。
詳細
ツールバーのグルーピングと Liquid Glass ボタン。新登場の ToolbarSpacer は、ツールバー内のボタンを複数の独立したグループに分割するためのものです。Liquid Glass と組み合わせると、各グループが独立したカプセル状の外観になります。
import SwiftUI
struct TripDetailView: View {
var body: some View {
NavigationStack {
TripList()
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
UpButton()
DownButton()
}
ToolbarSpacer(.fixed, placement: .primaryAction)
ToolbarItem(placement: .primaryAction) {
SettingsButton()
}
}
}
}
}
ポイント:
ToolbarItemGroupは上下 2 つの矢印ボタンを 1 つのカプセルにまとめ、視覚的に同じグループとして扱われます。ToolbarSpacer(.fixed, placement: .primaryAction)はその後ろに固定間隔を挿入します。Liquid Glass はこれを基準に、後続のボタンを独立したカプセルとしてレンダリングします。- 結果として
SettingsButtonは前 2 つのボタンから切り離され、すべての操作が同じピル形状に詰め込まれるのを避けられます。
カスタム View にガラスマテリアルを適用する。システムコントロールは Liquid Glass に自動対応しますが、自前で描いたフローティングボタンや通知カードは明示的に宣言する必要があります。
import SwiftUI
struct ToTopButton: View {
var body: some View {
Button("To Top", systemImage: "chevron.up") {
scrollToTop()
}
.padding()
.glassEffect()
}
func scrollToTop() {
// Scroll to top of view
}
}
ポイント:
.glassEffect()は新しい View 修飾子で、対象の View を Liquid Glass マテリアルとしてレンダリングします。- これは、これまで
.background(.ultraThinMaterial)+.clipShape(Capsule())といった手書きの組み合わせで実現していた処理を置き換えるものです。 padding()を先に適用することで、ガラス領域がボタンの内側余白まで含むようになり、文字にぴったり貼りつくのを避けられます。
@Animatable マクロでボイラープレートを撲滅する。これまでカスタムの Shape をアニメーションさせるには、animatableData を手書きし、複数のプロパティを AnimatablePair のネストに詰め込む必要がありました。今や、マクロ 1 つで十分です。
import SwiftUI
@Animatable
struct LoadingArc: Shape {
var center: CGPoint
var radius: CGFloat
var startAngle: Angle
var endAngle: Angle
@AnimatableIgnored var drawPathClockwise: Bool
func path(in rect: CGRect) -> Path {
// Creates a `Path` arc using properties
return Path()
}
}
ポイント:
@AnimatableをShapeに付与すると、コンパイル時にanimatableDataが自動合成され、補間可能なすべてのプロパティが連結されます。@AnimatableIgnoredはアニメーションに参加しないプロパティをマークします——Boolのような離散値は補間できないため、必ず除外する必要があります。- 1 行のコード変更で、これまでの
AnimatablePair<AnimatablePair<…>, …>十数行のボイラープレートと等価になります。
visionOS: 3D View を掴めるようにし、テーブルの上に置けるようにする。manipulable() はジェスチャーによる View のグラブ・回転・スケーリングをユーザーに許可し、surfaceSnappingInfo は View が現実の平面にスナップしたかどうかを通知します。
import ARKit
import RealityKit
import SwiftUI
struct BackpackWaterBottle: View {
@Environment(\.surfaceSnappingInfo) var snappingInfo: SurfaceSnappingInfo
var body: some View {
VStackLayout().depthAlignment(.center) {
waterBottleView
.manipulable()
Pedestal()
.opacity(
snappingInfo.classification == .table ? 1.0 : 0.0)
}
}
var waterBottleView: some View {
Model3D(named: "waterBottle")
}
}
ポイント:
.manipulable()を 1 行付けるだけで、Model3DView にグラブ・回転・スケーリングといったシステムジェスチャーが付与されます。@Environment(\.surfaceSnappingInfo)で現在のスナップ情報を読み取ります——分類には.table、.wall、.floorなどがあります。- ユーザーが水筒をテーブルに置いたときにのみ、下部の
Pedestal(台座)が表示されます。空中に浮かんでいるときは隠すことで、視覚的に自然な振る舞いになります。
重要ポイント
-
やること: プロジェクト内のすべての
UIViewRepresentable<UITextView>リッチテキストブリッジコードを、ネイティブなTextEditor+AttributedStringに置き換える。- なぜやる価値があるか: 各ブリッジ層は平均で 200〜300 行のグルーコードを抱えており、選択範囲の同期・キーボードツールバー・Undo スタックという 3 つのバグの温床を含んでいます。ネイティブ API がその部分を SwiftUI 側に取り戻すことで、長期的なメンテナンスコストが大幅に下がります。
- どう始めるか: まずリポジトリ内のすべての
UIViewRepresentableサブクラスを洗い出し、太字・斜体・色など基本属性しか使っていない最小ケースをパイロットとして選び、徐々に複雑なケースへ移行していきます。
-
やること: ツールバーのカスタム背景を棚卸しし、
ToolbarSpacer+ Liquid Glass のデフォルト外観へ移行する。- なぜやる価値があるか: 視覚的な整合性のために設定していた
.toolbarBackgroundのカスタムカラーは、Liquid Glass の下では新マテリアルの屈折効果を壊してしまい、ビルド後に高確率で見栄えが悪くなります。 - どう始めるか: Xcode の全文検索で
toolbarBackground、ultraThinMaterialを洗い出し、カスタム指定を一つずつ取り除いてから、ToolbarSpacer(.fixed, ...)でボタンを再グルーピングし、ビジュアルリグレッションを行います。
- なぜやる価値があるか: 視覚的な整合性のために設定していた
-
やること: 手書きで
animatableDataを実装しているすべてのShapeを@Animatableマクロに置き換える。- なぜやる価値があるか: 手書きの
AnimatablePairネストはきわめて定型的なコードで、プロパティを 1 つ変えるだけで 3 か所を修正する必要があります。マクロはその源流からこうした低価値な労働を排除します。 - どう始めるか:
var animatableDataキーワードで候補を洗い出し、それぞれに@Animatableを付け、離散プロパティには@AnimatableIgnoredを補完し、UI テストを通してアニメーションカーブの一貫性を確認します。
- なぜやる価値があるか: 手書きの
-
やること: visionOS プロジェクトのすべての
Model3DView にmanipulable()を付けて評価する。- なぜやる価値があるか: 空間環境にいるユーザーの第一反応は手で物を掴むことです。これまでは
DragGesture+RotateGesture3Dの組み合わせを自前で書く必要がありましたが、これからは 1 行の修飾子で代替できます。 - どう始めるか: まずデモシーンで
.manipulable()を取り付け、続いてsurfaceSnappingInfoでスナップ状態を読み取り、スナップ後に台座や影などの補助ビジュアルを表示するかどうかを決めます。
- なぜやる価値があるか: 空間環境にいるユーザーの第一反応は手で物を掴むことです。これまでは
関連セッション
- Meet Liquid Glass — Liquid Glass デザイン言語の全体像。SwiftUI の実装を読む前に、まずこの 1 本に目を通しておきましょう。
- Build a SwiftUI app with the new design — 1 つの完結したアプリを通して、SwiftUI における Liquid Glass の実践フローを追体験できます。
- Cook up a rich text experience in SwiftUI with AttributedString —
TextEditor+AttributedStringによるリッチテキスト編集のコードレベルでの連携。 - Explore concurrency in SwiftUI — SwiftUI と Swift Concurrency のインタラクションモデル、今回のアップデート背後にある実行モデル。
- Optimize SwiftUI performance with Instruments — 新登場の SwiftUI Instrument。本セッションのアップデートと組み合わせてパフォーマンス検証に活用します。
コメント
GitHub Issues · utterances