ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIコンテナの解説
SwiftUIのコンテナビューの機能について説明し、サブビューがコンテナによってどのように管理されるか理解するための枠組みを提供します。新しいAPIを利用してカスタムコンテナをビルドした後、コンテナのコンテンツをカスタマイズする修飾子を作成し、コンテナをさらにブラッシュアップしてアプリの魅力を高める方法をご紹介します。
関連する章
- 0:00 - Introduction
- 3:17 - Composition
- 10:42 - Sections
- 13:18 - Customization
- 16:52 - Next steps
リソース
関連ビデオ
WWDC23
WWDC21
-
ダウンロード
こんにちは SwiftUIを担当しているMattです SwiftUIのカスタムコンテナビューの 構築について説明します SwiftUIのAPIは豊富な機能を備えた 様々なコンテナを提供します 例えば Listコンテナビューがあります
コンテナビューはトレイリングビュー ビルダークロージャを使って コンテンツをラップします
ビュービルダーでは ハードコーディングした このTextビューのリストのように コンテンツを静的に定義できます コンテンツを動的に定義することもできます 例えば ForEachビューを使って データからTextビューを生成できます
ビュービルダーは 同じコンテナ内で あらゆる種類のコンテンツを 作成することをサポートしています 一部のコンテナは より高度な機能もサポートしています 例えば 設定可能なヘッダとフッタを 使って コンテンツを 個別のセクションにグループ化できます コンテンツをカスタマイズする コンテナ固有のモディファイアもあります この例では 行間に表示される 区切り線を非表示にしています このビデオでは 新しいAPIを使って このような機能をサポートする カスタムコンテナビューを 作成する方法を 説明します
最初に 任意のコンテンツの コンポジションをサポートする カスタムコンテナを作成して その柔軟性を 最大限に引き出す方法を説明します
また セクションのサポートを 追加する方法を説明します
さらに カスタムモディファイアを定義して コンテナ内のコンテンツを 装飾する方法を説明します
それぞれのトピックについて SwiftUIとそのAPI設計の基盤となる 重要な概念も説明します それでは早速始めます・・・
これは何でしょう?
「What's New in SwiftUI」を担当する SommerとSamが WWDCを祝ってカラオケパーティーを 開くそうです
出欠の返事に 私が歌う 曲を書く必要がありますが
どんな曲を選べばいいのかわかりません このようにピンチになったら 私がいつも試すことがあります SwiftUIの宣言型APIの パワーと柔軟性を使って 問題を解決することです このビデオでは カラオケに ぴったりの曲を選ぶ中で 役に立つと思われる作業を行っていきます これは私が頼りにしている ディスプレイボードです
カラオケの曲を いくつか選んでおきました
イニシャライザを使って 曲のアイデアを Textビューにマップします このビューはボードにピンで固定した カードに表示されます
DisplayBoardの実装では カスタムレイアウトを使用し ボード上でカードをランダムに配置します カード自体を構築するために 入力データについて反復処理する ForEachビューを使っています 各データ要素から コンテンツビューを生成し 作成したカスタムCardViewで そのコンテンツビューをラップします
ここまでは順調ですが DisplayBoardコンテナによって 私の創造性が制限されています 単一のデータコレクションからしか カードを作成できません コンテナがもっと柔軟になるように サポートを追加して コンテンツ内のコンポジションの 種類を増やすことができます
ただし最初に コンポジションの 意味を理解することが重要です
このようなSwiftUI Listを考えてみます Samがすすめる曲のリストが表示されます
このListは DisplayBoardと同様に データのコレクションで 初期化されます ただし SwiftUIでは 他の方法でもリストを作成できます
例えば 一連のビューを手作業で書き出して Listを作ることができます 私の曲のアイデアリストでは このようになります
SwiftUIは 様々な種類のコンテンツを 一緒に作成するAPIを 提供することで この2つの手法のギャップを埋めます
例えば ForEachビューで データ駆動型 リストを書き直すことができます これは以前と同じ機能を サポートしていますが ForEachビューは ビュービルダー内でネストできます
このことが重要なのは ビューだけを使って 両方のListのコンテンツを 定義しているからです
つまり それらのビューを 1つの統合されたListにまとめて これまでに集めた曲のアイデアを すべて表示できます この統合されたListが コンポジションの例です
最初の3行はハードコーディングした Textビューを使って 静的に定義し 残りの行はデータを使って 動的に生成しています すべて同じList内で実現しています
DisplayBoardコンテナでも 柔軟な コンポジションをサポートしたいのですが そのためには 実装を変更する必要があります
最初のステップでは コンテナをリファクタリングします それにより 1つのビュービルダーのみを 使って初期化されます
まず 既存のデータ駆動型プロパティを 単一の汎用のビュープロパティに 置き換えます
ViewBuilder属性を追加すると デフォルトのイニシャライザはトレイリング ビュービルダークロージャを使って 自動的にコンテンツを構築します
次に 新しいコンテンツビューを 使用するようにビュー本体を更新します それにはForEach(subviewOf:)と呼ばれる 新しいAPIを使います
この新しいForEachイニシャライザは 単一のビュー値を入力として受け入れます そして それぞれのサブビューを トレイリングビュービルダーに渡し それらのサブビューを カードビューなど別の種類のビューに 変換できます
この新しい実装により 先ほどの曲のアイデアリストを使って
同じコンテンツをDisplayBoardでラップし 各Textビューを ボード上のカードに変換できます
これは大きな改善ですが 仕組みを理解することが大切です
実装に戻って 新しいAPIを詳しく見てみます
ForEach(subviewOf:の
サブビューとは何でしょうか
サブビューは単に 別のビューに含まれるビューのことです コンテンツにサブビューが いくつあるか調べてみましょう 実は あらゆる素晴らしい問いの答えと同様に 「場合によります」
コードの最上位レベルのビューについて 考えると 4つあります 3つのTextビューと 1つのForEachビューです
しかし ForEachは 単なる1つのビューではありません データから生成されるビューの コレクションを表します
この場合 9つのサブビューに解決されます それぞれがSamのお気に入りの曲です
つまり このDisplayBoardのコンテンツは 合計12個の独立した サブビューに解決されます それは ボードに表示された 12枚のカードからも明らかです
また 12の行が表示されている リストの内容とも 一致します
これらの2種類のサブビュー間の違いを 理解することが重要です
DisplayBoardのコードにある オレンジ色の枠で囲んだ 4つのサブビューは 宣言されたサブビューと呼ばれます
一方 青色の枠で囲んだ画面上のビューは 解決されたサブビューと呼ばれます これには 手動で定義した 3つのTextビューと ForEachによって生成された 9つのTextビューが含まれます
SwiftUIの宣言型システムでは SwiftUIアプリの実行中に 解決されたサブビューを生成するレシピを 宣言されたサブビューで定義します
例えば ForEachビューは 単独では特定の外観や 動作を持たない 宣言されたサブビューです むしろ ForEachビューの目的は 解決されたサブビューの コレクションを生成することです
Groupビューは組み込みコンテナの もう1つの例で 解決されたサブビューの コレクションを表します 例えば 3つのTextビューのGroupは 対応する3つのサブビューに解決されます
EmptyViewのように 宣言されたサブビューの一部では サブビューに解決されない場合があります
また if文の分岐などのように条件付きで 異なる数のサブビューに 解決される場合があります
新しいForEach(subviewOf:)APIは コンテンツの解決されたサブビューのみを 反復処理で生成します
これにより より少ないコードを使って コンテナでコンテンツの 任意のコンポジションをサポートできます これは それらのサブビューがコード内で どのように宣言されているかに関係なく SwiftUIがサブビューの解決を 処理してくれるからです
柔軟なコンポジションをサポートすると 新しい曲をボードにとても簡単に 追加できます
Samの曲に加え Sommerもお気に入りの曲を すすめてくれました 別のForEachビューを使って それらの曲を追加できます これは コンテナの実装に 変更を加えることなく実現できます 新しいアイデアの追加は とても簡単になりましたが すべてのカードを表示するのは困難です このことを解決するために ボードが一杯になってきたら カードのサイズを縮小しましょう
ボードに追加したカードが 15枚を超えたら カードのサイズを縮小します カードの枚数を数えるために 別の新しいAPIを使うことができます Group(subviewsOf:)と呼ばれ これで実装のForEachをラップします
ForEach(subviewOf:)APIと同様に このビューはビューを入力として受け取り サブビューを解決します
ただし 1つずつ反復するのではなく Group(subviewsOf:)APIは すべての解決されたサブビューの コレクションを返します
このコレクションに対して countプロパティを使って カードの総数を確認します 15枚以上ある場合は 小さいサイズを使うよう CardViewを設定します
アプリに戻ると サイズが小さいため カードがあまり重なりません これでカードが読みやすくなりましたが ボードはまだ少し雑然としています
そこで次はセクションのサポートを 追加して整理します
Listは セクションをサポートする 組み込みコンテナの一例で SwiftUIのSectionビューを使います Sectionビューの動作は Groupビューとよく似ていますが オプションのヘッダやフッタなど セクション固有のメタデータがあります
私のディスプレイボードの目標は 各自のお気に入りの曲について 個別のセクションを作ることです
ただし カスタムコンテナはデフォルトで セクションをサポートしていないため 追加の作業を行って セクションを有効にします
これは私が考えている デザインのスケッチです ボードをセクションごとに縦の列に分割し 一番上にヘッダを表示します
私の実装では まず 既存のカードレイアウトのコードを 独自のビューに分離します
個別のセクション内でカードを配置する際に このビューを再利用します
次に セクションコンテンツを 水平スタックでラップし ボードを列に分割します 列を作るには ディスプレイボードの コンテンツ内にある Sectionビューの情報に アクセスする必要があります そのために ForEachの もう1つの新しいAPIを使用できます
これはForEach(sectionOf:)と呼ばれ ForEach(subviewOf:)と 同じように機能し ビューインスタンスを入力として 受け取ります
ただし このバージョンでは ビュー内で 検出した各セクションについて反復を行い セクション設定を ビュービルダーに提供します
各セクションには そのコンテンツビューのプロパティがあり カードを配置するために 先ほど作成したヘルパービューに それを渡すことができます
さらに 各セクションにカスタムの背景を追加し セクションを見分けやすくします
アプリをもう一度実行すると カードが前よりも整理され 各セクションが 別の列に配置されています 次に セクションヘッダを表示するための サポートを追加します
まず 各セクションをVStackでラップし ヘッダとコンテンツの両方を含めます
次にif文を使ってセクションに ヘッダがあるかどうかを確認します ここでisEmptyプロパティを使います これは 解決されたサブビューが ヘッダに含まれているかどうかを返します
ヘッダが存在する場合 先ほど作成したカスタムヘッダカードに それを表示します
ボードを見ると 各セクションの上に 該当するヘッダカードが表示されています
ただし 曲選びを進めるために ボツの曲に線を引く必要があります そのために コンテナのコンテンツを カスタマイズするサポートを追加します
ビデオの冒頭で .listRowSeparator()モディファイアを 使った例を示しました このモディファイアは List内のビューに適用されますが 行間に区切り線を描画するかどうかを 決定する動作は List自体に実装します
私のディスプレイボードで 特定の曲をボツにする場合に カードに線を引く動作を サポートしましょう
新しいAPIを使って このような種類の コンテナ固有のモディファイアを構築できます これらのモディファイアは コンテナ値と呼ばれます コンテナ値は新しい種類の キー付きストレージで EnvironmentやPreferenceと 概念が似ています
環境値は ビュー階層全体で上から下に渡されます
Preferenceでは 含まれている すべてのビューについてビュー階層全体で 下から上へ値を渡します
解決されたサブビューのコンテナ値は その上位のコンテナからしか アクセスできないため コンテナ固有の カスタマイズオプションを 実装するのに最適なツールです
私のディスプレイボードでは コンテナ値を使って カードに線を引くカスタムの ビューモディファイアを作成します 新しい種類のコンテナ値の定義に 必要なコードはわずか数行です
まず ContainerValues型の Extensionを作成します これはSwiftUIの新しい型です
Extension内で 新しいEntryマクロを使って プロパティを宣言します このマクロには カードが却下されたか どうかを追跡するブール値が格納されます
Entryマクロは新しいAPIであり SwiftUIのキー付きストレージ型に 環境値やフォーカス値などの 新しい値を追加するのに 便利な構文を提供します
次に プロパティの設定を容易にするために カスタムビューモディファイアを宣言します これは新しいcontainerValue() API モディファイアを呼び出し プロパティのキーパスと 新しく設定する値を渡します
次に コンテナ内に 新しいコンテナ値のサポートを 追加します セクションの実装で カードのコンテンツが 却下されたかどうかに応じて 各カードビューを カスタマイズする必要があります そのために 新しいcontainerValues プロパティを使用します コンテナ値は解決されたサブビューと セクションの両方から読み取りできます
カスタム値を CardViewのisRejectedパラメータに 渡します これにより カードが却下された場合 カスタムの装飾が表示されます
新しいモディファイアを使うことで いよいよ いくつかの曲をボツにできます
まず「Scrolling in the Deep」は 大好きな曲ですが カラオケでその音域の声を出せるか どうかわかりません
ボード上でそのカードに指で線を引くと 大きな赤い斜線が表示されます
Samは いくつかの曲を 自分で歌うそうなので それらも却下します
Sommerが何を歌うつもりか わかりません 念のため 彼女のセクションは すべて却下しましょう
セクション全体にモディファイアを適用すると その全サブビューが同じ値に設定されます
つまり 右側のSommerのすべての曲に 線が引かれます
カラオケにぴったりの曲を見つける作業が かなり前進しましたが 最終決定を行う必要があります 私の曲選びはまだ続きますが これらの新しいAPIを試してみてください
ForEachとGroupで 新しいイニシャライザを使うと 解決されたサブビューや ビューのセクションについて反復と 変換を行うことができます カスタムコンテナの設計上 可能な場合は セクションのサポートを追加しましょう コンテナでセクションを使う 意味がない場合も問題ありません セクションサポートの追加は オプションです
最後に コンテナ値を使って 個々のコンテンツの カスタマイズや装飾を行いましょう
これらの新しいAPIのおかげで 残りの数曲に 絞り込むことができました
今 気づきました まだ検討していない曲が1曲あります それが探していた曲かも知れません
ホイットニー・Viewストンの 「I Will Always Subview」です
ここで私がやるべきことは SommerとSamに出欠の返事を返して このセッションを終わらせることです なぜなら本当に曲の練習を始めなければ ならないからです! またお会いしましょう
-
-
0:20 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
0:36 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(otherSongs) { song in Text(song.title) } }
-
0:54 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
1:00 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
2:35 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
2:47 - DisplayBoard implementation
// Insert code snvar data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
3:08 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
3:30 - List composition
List(songsFromSam) { song in Text(song.title) }
-
3:46 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
3:56 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List(songsFromSam) { song in Text(song.title) }
-
4:05 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List { ForEach(songsFromSam) { song in Text(song.title) } }
-
4:24 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
4:59 - DisplayBoard implementation
var data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
5:15 - DisplayBoard implementation
// DisplayBoard implementation @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } DisplayBoard { ForEach(songsFromSam) { song in Text(song.title) } }
-
5:27 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
5:52 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
5:57 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:12 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
6:23 - DisplayBoard subviews
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:36 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
7:11 - List subviews
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
7:19 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:00 - Resolved ForEach
// 1 declared view ForEach(songsFromSam) { song in Text(song.title) } // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:16 - Resolved Group
// 1 declared view Group { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View")
-
8:32 - Resolved EmptyView
// 1 declared view EmptyView() // Zero resolved subviews
-
8:39 - Resolved if expression
// Insert code snippet.
-
8:48 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:11 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
9:17 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } ForEach(songsFromSommer) { song in Text(song.title) } }
-
9:44 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:55 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView { subview } } } } .background { BoardBackgroundView() } }
-
10:19 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
10:47 - List sections
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
11:03 - DisplayBoard sections
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { song in Text(song.title) } } }
-
11:26 - Implementing DisplayBoard sections
DisplayBoard sections @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
11:35 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { DisplayBoardSectionContent { content } .background { BoardBackgroundView() } } struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content ... }
-
11:42 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in DisplayBoardSectionContent { section.content } } } .background { BoardBackgroundView() } }
-
12:48 - Implementing DisplayBoard section headers
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in VStack(spacing: 20) { if !section.header.isEmpty { DisplayBoardSectionHeaderCard { section.header } } DisplayBoardSectionContent { section.content } .background { BoardSectionBackgroundView() } } } } .background { BoardBackgroundView() } }
-
13:30 - List customization
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
14:46 - Custom container values
extension ContainerValues { @Entry var isDisplayBoardCardRejected: Bool = false } extension View { func displayBoardCardRejected(_ isRejected: Bool) -> some View { containerValue(\.isDisplayBoardCardRejected, isRejected) } }
-
15:42 - Implementing DisplayBoard customization
struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in let values = subview.containerValues CardView( scale: (subviews.count > 15) ? .small : .normal, isRejected: values.isDisplayBoardCardRejected ) { subview } } } } } }
-
16:15 - DisplayBoard customization
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") .displayBoardCardRejected(true) Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) .displayBoardCardRejected(song.samHasDibs) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { Text($0.title) }}} } .displayBoardCardRejected(true) }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。