[WWDC24] SwiftUIの新機能のまとめ
2024年6月25日
所 友太 / @tokorom
WWDC24の What’s new in SwiftUI のまとめです。
今回、このセッションで紹介される項目の数が例年以上に多すぎてびっくりでした。 セッションでは短い間隔でポンポンとたくさんの機能が流れるように紹介されていきます。
このまとめでは、セッションでは軽く触れられた程度の内容も、APIリファレンスへのリンクをつけるなどしてもう少しだけ補足します。
このセッションを視聴する/この記事を参照する目的は、WWDC24で発表されたSwiftUIの新機能をさらっと把握し頭の中にインデックスを貼ることだと思います。
サイドバー/タブバー
- サイドバー/タブバーがより柔軟に
- フローティングタブバーをサポート
- 項目の並び替えや使用頻度の低いオプションの非表示など、ユーザーが自分好みにカスタマイズすることもできる
- TabViewに内包する要素も新しいタイプセーフな書き方に
struct KaraokeTabView: View {
@State var customization = TabViewCustomization()
var body: some View {
TabView {
Tab("Parties", image: "party.popper") {
PartiesView(parties: Party.all)
}
.customizationID("karaoke.tab.parties")
Tab("Planning", image: "pencil.and.list.clipboard") {
PlanningView()
}
.customizationID("karaoke.tab.planning")
Tab("Attendance", image: "person.3") {
AttendanceView()
}
.customizationID("karaoke.tab.attendance")
Tab("Song List", image: "music.note.list") {
SongListView()
}
.customizationID("karaoke.tab.songlist")
}
.tabViewStyle(.sidebarAdaptable)
.tabViewCustomization($customization)
}
}
tabViewStyleに sidebarAdaptable を指定することで、プラットフォームごとに柔軟にサイドバー OR タブバーが適用される
- iOSでは常にボトムタブバー
- iPadOSではフローティングタブバーとサイドバーが自動で切り替わる
- macOS/tvOSでは常にサイドバー
- visionOSではオーナメント/TabSectionが使われている場合はサイドバーも
tabViewCustomization により、ユーザーがカスタマイズできる要素を調整できる(並び替え、非表示など)
tvOSのサイドバーはフローティング表示に
- macOSではセグメンティッドコントロールスタイルに切り替えることもできる
sheet
- UIKitでいうpresentViewController
- 従来はsheetが半モーダル表示という位置付けだったが、iOS18からはsheetで表示するViewに対してどのような表示にするかを指定できるようになる
- presentationSizing
automatic
: プラットフォームごとに適切な表示にfitted
: コンテンツにフィットするサイズにform
:page
よりも一回り小さいpage
: おそらく従来のsheetのデフォルトサイズ
- また、presentationSizingは追加で
fitted
、sticky
、proposedSize
を追記できるfitted
: コンテンツにフィットさせるsticky
: コンテンツに合わせて大きくはなるが小さくはなならないproposedSize
: 詳細不明
.presentationSizing(
.page
.fitted(horizontal: false, vertical: true)
.sticky(horizontal: false, vertical: true)
)
Zoom Navigation Transition
- 写真アプリにある写真セルを選択すると拡大しながらその写真の画面に遷移するトランジション
- navigationTransition が新設され、そこに
.zoom
を指定することで実現できる - あわせて matchedTransitionSource で設定をする必要がある
- 逆にいうとこれでトランジションをカスタマイズできる
struct PartyView: View {
var party: Party
@Namespace() var namespace
var body: some View {
NavigationLink {
PartyDetailView(party: party)
.navigationTransition(.zoom(
sourceID: party.id, in: namespace))
} label: {
Text("Party!")
}
.matchedTransitionSource(id: party.id, in: namespace)
}
}
struct PartyDetailView: View {
var party: Party
var body: some View {
Text("PartyDetailView")
}
}
struct Party: Identifiable {
var id = UUID()
static var all: [Party] = []
}
Controls
- コントロールセンターに表示するボタンやスイッチをアプリが作成できるように
- ロック画面のカメラボタンやライトボタンも置き換えられる
- ControlはWidgetの一種でApp Intentsを使って作成できる
import WidgetKit
import SwiftUI
struct StartPartyControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.karaoke_start_party"
) {
ControlWidgetButton(action: StartPartyIntent()) {
Label("Start the Party!", systemImage: "music.mic")
Text(PartyManager.shared.nextParty.name)
}
}
}
}
// Model code
class PartyManager {
static let shared = PartyManager()
var nextParty: Party = Party(name: "WWDC Karaoke")
}
struct Party {
var name: String
}
// AppIntent
import AppIntents
struct StartPartyIntent: AppIntent {
static let title: LocalizedStringResource = "Start the Party"
func perform() async throws -> some IntentResult {
return .result()
}
}
Swift Charts
- Vectorized plotsという新しいチャートが加わった
import SwiftUI
import Charts
struct AttendanceView: View {
var body: some View {
Chart {
LinePlot(x: "Parties", y: "Guests") { x in
pow(x, 2)
}
.foregroundStyle(.purple)
}
.chartXScale(domain: 1...10)
.chartYScale(domain: 1...100)
}
}
TableColumnForEach
- TableColumnForEach を使うことで、動的な数のカラムを持ったテーブルを作ることができる
Table(guestData) {
// A static column for the name
TableColumn("Name", value: \.name)
TableColumnForEach(partyData) { party in
TableColumn(party.name) { guest in
Text(guest.songsSung[party.id] ?? 0, format: .number)
}
}
}
MeshGradient
- MeshGradient で、複雑なメッシュグラデーションを表現できるように
MeshGradient(
width: 3,
height: 3,
points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
],
colors: [
.red, .purple, .indigo,
.orange, .cyan, .blue,
.yellow, .green, .mint
]
)
Document Launch Scene
- DocumentGroupLaunchScene を使い、ドキュメントベースのアプリのリッチな起動画面を作りやすく
- 大きな文字のタイトル
- 操作可能なボタン類の追加
- 背景のカスタマイズ
- 装飾をタイトルのViewの前面と背面に追加
DocumentGroupLaunchScene("Your Lyrics") {
NewDocumentButton()
Button("New Parody from Existing Song") {
// Do something!
}
} background: {
PinkPurpleGradient()
} backgroundAccessoryView: { geometry in
MusicNotesAccessoryView(geometry: geometry)
.symbolEffect(.wiggle(.rotational.continuous()))
} overlayAccessoryView: { geometry in
MicrophoneAccessoryView(geometry: geometry)
}
SF Symbols
- SymbolEffect に3つのアニメーションが追加された
breathe
: シンボルを滑らかに上下にスケールrotate
: 回転エフェクト、指定したアンカーポイントを中心にシンボルの一部を回転させるwiggle
: くねくねエフェクト、シンボルを好きな方向や角度に揺らす
replace
など既存のアニメーションにも新しい機能が追加された- MagicReplace により、バッジやスラッシュをスムーズにアニメーションできるように
macOS
Window
- windowStyle に
plain
を追加- デフォルトのウィンドウ背景やバーのないスタイル
- windowLevel を追加
- ‘normal’, ‘desktop’, ‘floating’ の3階層のどの層で表示するかを指定
Window("Lyric Preview", id: "lyricPreview") { ... }
.windowStyle(.plain)
.windowLevel(.floating)
- defaultWindowPlacement でWindowのデフォルト位置を任意に調整することもできる
.defaultWindowPlacement { content, context in
let displayBounds = context.defaultDisplay.visibleRect
let contentSize = content.sizeThatFits(.unspecified)
return topPreviewPlacement(size: contentSize, bounds: displayBounds)
}
WindowDragGesture でコンテンツ部分をドラッグしてウィンドウを動かせるようにできる
UtilityWindow のような新しいタイプのWindowもある
- アプリのメインコンテンツに関連するコントロール、設定、情報を表示するWindow
modifierKeyAlternate
- modifierKeyAlternate でショートカットキーに対する修飾キーを付与できるように
- その修飾キーを押している間、修飾後のショートカットメニューがUIに自動的に明示される
Button("Preview Lyrics in Window") {
// show preview in window
}
.modifierKeyAlternate(.option) {
Button("Preview Lyrics in Full Screen") {
// show preview in full screen
}
}
.keyboardShortcut("p", modifiers: [.shift, .command])
onModifierKeysChanged
- また onModifierKeysChanged により修飾キーを押している間にViewを変化させるのを実装しやすくなった
LyricLine()
.overlay(alignment: .top) {
if showBouncingBallAlignment {
// Show bouncing ball alignment guide
}
}
.onModifierKeysChanged(mask: .option) {
showBouncingBallAlignment = !$1.isEmpty
}
pointerStyle
- pointerStyle でマウスポインタの外観や可視性をカスタマイズできるように
ForEach(resizeAnchors) { anchor in
ResizeHandle(anchor: anchor)
.pointerStyle(.frameResize(position: anchor.position))
}
visionOS
pushWindow
- 新しい PushWindowAction により現在のWindowをバックグラウンドにして新しいWindowで覆うことができる
- 覆ったWindowは既存の
DismissWindowAction
で閉じる
- 覆ったWindowは既存の
struct EditorView: View {
@Environment(\.pushWindow) private var pushWindow
var body: some View {
Button("Play", systemImage: "play.fill") {
pushWindow(id: "lyric-preview")
}
}
}
hoverEffect
- これまでvisionOSのホバーエフェクト(視線を合わせた時の挙動)はカスタマイズしづらかったが、 hoverEffect でカスタムできるようになった
- ただしプライバシーを守るためユーザーが視線を合わせたタイミングをトラッキングできないのはこれまでどおり
.hoverEffect { effect, isActive, _ in
effect.scaleEffect(isActive ? 1.05 : 1.0)
}
iPadOS
スクイーズ
- onPencilSqueeze でApple Pencilのスクイーズをハンドリングできる
- このときユーザーがシステムに設定した期待される動作を preferredPencilSqueezeAction で参照することができる
@Environment(\.preferredPencilSqueezeAction) var preferredAction
var body: some View {
LyricsEditorView()
.onPencilSqueeze { phase in
if preferredAction == .showContextualPalette, case let .ended(value) = phase {
if let anchorPoint = value.hoverPose?.anchor {
lyricDoodlePaletteAnchor = .point(anchorPoint)
}
lyricDoodlePalettePresented = true
}
}
}
watchOS
Live Activity
- iOS用のLive Activityは何もしなくてもwatchOSでそのまま動作するように
- supplementalActivityFamilies でどのサイズのLive Activityをサポートするかを指定できる
ダブルタップジェスチャー
- handGestureShortcut を使うことで、watchOSのダブルタップジェスチャーに対応することができるように
- 具体的には
.handGestureShortcut(.primaryAction)
を付与する
- 具体的には
時間や日付の表示形式
- Textの新しいイニシアライザ により、時間や日付の表示がしやすく
- 具体的には
Text(.currentDate, format: .reference(to: xxx))
などで「あと何分」といった表示がしやすいように - その他「-0:58」といった表記や、「00:06.78」のようなタイマー的な表記も指定できる
- なお、これはLive Activityと相性が良いと紹介されたが、watchOSだけでなくiOS/visionOSなど全てのOSで利用できる機能
- 具体的には
ウィジェットの表示示唆
- WidgetRelevances により、システムがスマートスタックなどにウィジェットを表示することを判断する材料を与えやすくなった
SwiftUI Framework foundations
カスタムコンテナ
- たとえばコルクボードにメモをランダムに並べるようなカスタムなコンテナを定義できるように
- 組み込みのListのようなコンテナと同様にForEachで複数の要素を渡す
- Sectionにも対応
- カスタムなmodifierも付与できる
Entryマクロ
- Entryマクロにより、EnvironmentValues、FocusValues、Transaction、ContainerValuesを拡張しやすく
extension EnvironmentValues {
@Entry var karaokePartyColor: Color = .purple
}
extension FocusValues {
@Entry var lyricNote: String? = nil
}
extension Transaction {
@Entry var animatePartyIcons: Bool = false
}
extension ContainerValues {
@Entry var displayBoardCardStyle: DisplayBoardCardStyle = .bordered
}
アクセシビリティ
- SwiftUIに組み込まれたアクセシビリティラベルに追加情報を付与できるように
Xcode Preview
- リビルドせずにPreviewt実行を切り替えられるように
- また
@Previewable
マクロでPreview用のStateを簡単に追加できるようになった
#Preview {
@Previewable @State var showAllSongs = true
Toggle("Show All songs", isOn: $showAllSongs)
}
テキストの選択範囲
- TextFieldのイニシアライザに
selection
が加わり、テキストの選択範囲を取得できるように
struct LyricView: View {
@State private var selection: TextSelection?
var body: some View {
TextField("Line \(line.number)", text: $line.text, selection: $selection)
// ...
}
}
searchFocused
- searchFocused により、検索フィールドへのフォーカス移動ができるように
textInputSuggestions
- macOS専用だが、 textInputSuggestions でテキストフィールドにテキスト補完の候補を出せるように
色の混合
- Colorに mix が新設され、色の混合がしやすく
カスタムシェーダー
- シェーダーをプリコンパイルできるように
ContentView()
.task {
let slimShader = ShaderLibrary.slim()
try! await slimShader.compile(as: .layerEffect)
}
ScrollView
- onScrollGeometryChange でスクロール位置による任意のアクションを実装できるように
- 例えばここまでスクロールしたときだけこのViewが表示されるなど
ScrollView {
// ...
}
.onScrollGeometryChange(for: Bool.self) { geometry in
geometry.contentOffset.y < geometry.contentInsets.top
} action: { wasScrolledToTop, isScrolledToTop in
withAnimation {
showBackButton = !isScrolledToTop
}
}
- onScrollVisibilityChange で特定のViewが画面にスクロールイン・スクロールアウトした時のアクションを実装できるように
- 例えば動画プレイヤーがスクロールインしたら再生を開始し、スクロールアウトしたら停止するなど
VideoPlayer(player: player)
.onScrollVisibilityChange(threshold: 0.2) { visible in
if visible {
player.play()
} else {
player.pause()
}
}
- ScrollPosition が新しくなり、従来通りの特定のIDを持つViewの指定だけでなく、具体的なオフセットや端なども指定できるように
struct ContentView: View {
@State private var position: ScrollPosition =
.init(idType: Int.self)
var body: some View {
ScrollView {
// ...
}
.scrollPosition($position)
.overlay {
FloatingButton("Back to Invitation") {
position.scrollTo(edge: .top)
}
}
}
}
Swift 6
- Swift 6サポートのためのAPI改善
- Viewはデフォルトで
@MainActor
に
- Viewはデフォルトで
UIKit/AppKit連携
- UIGestureRecognizerをSwiftUIで直接利用できるように
- UIKit/AppKitのアニメーションをSwiftUIで直接利用できるように
visionOS 2
Volumeベースプレート
- visionOS 2ではVolumeの境界を明示するベースプレートを表示できる
- ベースプレートは
volumeBaseplateVisibility(.hidden)
で隠すことができる
WindowGroup {
ContentView()
}
.windowStyle(.volumetric)
.defaultWorldScaling(.trueScale)
.volumeBaseplateVisibility(.hidden)
onVolumeViewpointChange
- onVolumeViewpointChange でユーザーがどの方向から見ているかを検知して、常にユーザーの方向に向けるなどのアクションをすることができる
Model3D(named: "microphone")
.onVolumeViewpointChange { _, new in
micRotation = rotateToFace(new)
}
.rotation3DEffect(micRotation)
.animation(.easeInOut, value: micRotation)
没入度の範囲指定
- ImmersiveStyleに progressive が加わり、没入度の範囲指定ができるようになりました
新しいSurroundingsEffect
- preferredSurroundingsEffect に semiDakrやcolorMultiplyやdim など新しい効果を指定できるようになりました
- これによりパススルーを好みの暗さにしたりムーディーな雰囲気にできるようになりました
- これまでは
systemDark
しか指定できませんでした
struct LoungeView: View {
var body: some View {
StageView()
.preferredSurroundingsEffect(.colorMultiply(.purple))
}
}
オーナメント
- その他オーナメントにもアップデートがあるよう
テキストViewの拡張
- TextRenderer でテキストにブラーや色合いの変更など様々なエフェクトがかけられるように
struct KaraokeRenderer: TextRenderer {
func draw(
layout: Text.Layout,
in context: inout GraphicsContext
) {
for line in layout {
for run in line {
var glow = context
glow.addFilter(.blur(radius: 8))
glow.addFilter(purpleColorFilter)
glow.draw(run)
context.draw(run)
}
}
}
}
struct LyricsView: View {
var body: some View {
Text("A Whole View World")
.textRenderer(KaraokeRenderer())
}
}
まとめは以上です。 盛りだくさんすぎてびっくりでした。