どうも、モバイルエンジニアのEtsuwoです。
SwiftUIを使ってる中で、親Viewから子Viewや孫Viewのframeへアクセスしたい場合がありました。
その際PreferenceKeyという仕組みを使用すると非常に便利だったので記事にします。
1. PreferenceKeyとは
PreferenceKeyは下位のViewからより上位の階層のViewへと値を渡す仕組みです。
これを使うことにより、下位Viewで起こったframeの変更などを上位Viewで取得できるようになります。
二階層以上離れていても取得することができるので、Viewを細かく作りたい場合などに便利です。
2. 実装してみる
今回は親、子、孫の3つのViewを作り親Viewで孫ViewのFrameを取得する場合を実装してみます。
以下のように中央の黒い長方形のFrameを表示できたら完成です。
PreferencyKeyを使用するには以下の3つを行う必要があります。
- プロトコル
PreferenceKey
を継承するstructの作成 - 下位Viewで
.preference()
などを使用して上位Viewに渡したい値を入れる - 上位Viewで
.onPreferenceChange()
を使用して下位Viewから渡された値を受け取る
それでは順番に見ていきましょう。
プロトコルPreferenceKey
を継承するstructの作成
まずPreferenceKey
を継承するstructを作成し、値を受け渡しするKeyを作成すると共に、どんな値を受け渡しするのか決定します。
PreferenceKey
では初期値が入るdefaultValue
と更新時に呼ばれるreduce()
が実装必須となり、これらを実装する際に受け渡しをする値の型が決まります。
今回は、孫ViewのFrameを取得したいので、CGRect型を渡してあげましょう。
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = CGRect() // 初期値が入る
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { // valueが更新される時に呼ばれる
value = nextValue()
}
}
下位Viewで.preference()
などを使用して上位Viewに渡したい値を入れる
作成したGrandChildViewで.preference()
を使用してFrameを渡してあげます。
これにより、GrandChildViewより上位のViewでGrandChildViewのFrameを取得できるようになります。
struct GrandChildView: View {
var body: some View {
GeometryReader { geometry in
Rectangle() // できる限り広がろうとする四角形
.frame(maxWidth: .infinity, maxHeight: .infinity)
.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
}
}
}
なお、本記事では通常のpreferenceを使用するためにGeometryReaderを使用してFrameの取得を行なっていますが、.anchorPreference()
を使用するとGeometryReaderを使用する事なしにFrameを取得することができます。
上位Viewで.onPreferenceChange()
を使用して下位Viewから渡された値を受け取る
最後に親Viewと中間の子Viewを作ります。
子Viewでは、Spacerと先ほど作成したGrandChildViewを使ってレイアウトを組んでいます。
struct ChildView: View {
var body: some View {
VStack {
Spacer()
.frame(height: 100) // テキトーな余白
HStack {
Spacer()
.frame(width: 50)
GrandChildView()
Spacer()
.frame(width: 50)
}
Spacer()
.frame(height: 100)
}
}
}
最後に親Viewでは、孫Viewから渡されてきた値を受け取って処理をします。
.onPreferenceChange()
でGrandChildViewのframeを受け取りTextに入れています。
struct ParentView: View {
@State private var grandChildViewFrame: CGRect = CGRect()
var body: some View {
ZStack(alignment: .center) {
ChildView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onPreferenceChange(FramePreferenceKey.self, perform: { frame in
grandChildViewFrame = frame
})
Text(grandChildViewFrame.debugDescription)
.foregroundColor(.white)
}
}
}
以上です。