SwiftUI PreferenceKey サンプル実装
ポイント
-
DetermineHeightで各カードの高さを計測し、MaxHeightPreferenceKeyで最大値を集約 -
onPreferenceChangeで最大値が変わったら@Stateに反映し、全カードの高さを揃える -
.overlayで見た目に影響なく高さを計測
さらに解説やサンプルコードをご希望の場合はお知らせください。段階的に展開も可能です。
import SwiftUI
// 1. 最大高さを集約する PreferenceKey
struct MaxHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
// 2. 各カードの高さを PreferenceKey に伝えるための View
struct DetermineHeight: View {
var body: some View {
GeometryReader { proxy in
Color.clear
.anchorPreference(
key: MaxHeightPreferenceKey.self,
value: .bounds
) { anchor in
proxy[anchor].size.height
}
}
}
}
// 3. サンプルカード
struct SampleCard: View {
let text: String
let maxHeight: CGFloat
var body: some View {
Text(text)
.frame(maxWidth: .infinity)
.frame(height: maxHeight)
.background(Color.yellow.opacity(0.3))
.overlay(DetermineHeight())
.cornerRadius(8)
.padding(.horizontal)
}
}
// 4. 親ビューで高さを揃える
// onPreferenceChange で最大の高さが更新されたら全体の高さを更新
struct ContentView: View {
let items = [
"短いテキスト",
"とてもとても長いテキストサンプルです",
"中くらい"
]
// 状態管理プロパティ:@State を使うと更新時に ContentView の body が再評価される
// ※ 親Viewで状態(@State)を管理し、子Viewには値として渡すだけの場合は @Binding 不要
// 子Viewから親Viewの値を変更したい場合のみ、@Binding を使います
@State private var maxHeight: CGFloat = 0
var body: some View {
VStack(spacing: 16) {
ForEach(items, id: \.self) { item in
SampleCard(text: item, maxHeight: maxHeight)
}
}
.onPreferenceChange(MaxHeightPreferenceKey.self) { value in
maxHeight = value
}
.padding()
}
}
1. onPreferenceChange はいつ動作するか?
.onPreferenceChange(MaxHeightPreferenceKey.self) { value in
maxHeight = value
}
この部分は、MaxHeightPreferenceKey.Key の値(= 各カードの高さ)が変化したときに実行されます。
- 画面初回表示時(カードがレイアウトされたとき)
- データが変わってカードの高さが変化したとき
- 画面サイズや向きが変わってカードの高さが変化したとき
など、カードの高さに変化があったタイミングで自動的に実行されます。
この仕組みにより、すべてのカードの高さを揃えるために「最大の高さ」を動的に計算し、maximumCardHeight に反映しています。
2. PreferenceKey の reduce メソッドの役割
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
この reduce メソッドは、複数のビューから渡された値(この場合は高さなど)をひとつにまとめる役割を持ちます。
この実装では、value と nextValue() の 大きい方(最大値) を value に代入しています。
つまり、複数のカードの高さが報告されたとき、一番大きい高さを最終的な値として採用する、という意味です。
PreferenceKey の reduce メソッドの一般的な使用例
✅ 最大値を集める場合
struct MaxHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
✅ 合計値を集める場合
struct TotalWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
✅ 配列で全ての値を集める場合
struct ItemsPreferenceKey: PreferenceKey {
static var defaultValue: [String] = []
static func reduce(value: inout [String], nextValue: () -> [String]) {
value.append(contentsOf: nextValue())
}
}
3. GeometryReader と anchorPreference の使い方
GeometryReader { proxy in
Color.clear
.anchorPreference(
key: ItemsPreferenceKey.self,
value: .bounds
) { anchor in
[proxy[anchor].size.height.description]
}
}
このコードは、子ビューの高さ(size.height)を親ビューに伝えるための仕組みを実装しています。
-
GeometryReaderは、自身のサイズや位置情報を取得できるビューです。 -
Color.clearは透明なビューで、見た目には何も表示されません。 -
.anchorPreference(key:value:)は、ビューの特定の領域(ここでは.bounds)に関する情報をPreferenceKeyに渡すための修飾子です。 - クロージャ内で
proxy[anchor].size.heightを呼ぶことで、このビューの高さを取得し、PreferenceKeyに渡します。
これにより、親ビューで子ビューのサイズ情報を利用できるようになります。
5. .overlay(DetermineHeight()) の意味
.overlay(DetermineHeight())
これは、各カード(または各セル)の上に DetermineHeight ビューを重ねて表示しています。
-
DetermineHeight()は、自分自身の高さをPreferenceKey経由で親ビューに伝えるためのビューです。 -
.overlay()を使ってそれをカードに重ねることで、カード自体の高さを計測し、その情報を親ビューに渡すことができます。
この仕組みにより、親ビューで 「全カードの最大高さを揃える」 などのレイアウト調整が可能になります。