8
Help us understand the problem. What are the problem?

posted at

updated at

[SwiftUI] PreferenceKeyを使用して下位Viewから上位Viewへ値を伝える

どうも、モバイルエンジニアのEtsuwoです。

SwiftUIを使ってる中で、親Viewから子Viewや孫Viewのframeへアクセスしたい場合がありました。
その際PreferenceKeyという仕組みを使用すると非常に便利だったので記事にします。

1. PreferenceKeyとは

PreferenceKeyは下位のViewからより上位の階層のViewへと値を渡す仕組みです。
これを使うことにより、下位Viewで起こったframeの変更などを上位Viewで取得できるようになります。
二階層以上離れていても取得することができるので、Viewを細かく作りたい場合などに便利です。

2. 実装してみる

今回は親、子、孫の3つのViewを作り親Viewで孫ViewのFrameを取得する場合を実装してみます。
以下のように中央の黒い長方形のFrameを表示できたら完成です。

スクリーンショット 2022-04-11 23.43.51.png

PreferencyKeyを使用するには以下の3つを行う必要があります。

  1. プロトコルPreferenceKeyを継承するstructの作成
  2. 下位Viewで.preference()などを使用して上位Viewに渡したい値を入れる
  3. 上位Viewで.onPreferenceChange()を使用して下位Viewから渡された値を受け取る

それでは順番に見ていきましょう。

プロトコルPreferenceKeyを継承するstructの作成

まずPreferenceKeyを継承するstructを作成し、値を受け渡しするKeyを作成すると共に、どんな値を受け渡しするのか決定します。
PreferenceKeyでは初期値が入るdefaultValueと更新時に呼ばれるreduce()が実装必須となり、これらを実装する際に受け渡しをする値の型が決まります。
今回は、孫ViewのFrameを取得したいので、CGRect型を渡してあげましょう。

GrandChildView.swift
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を取得できるようになります。

GrandChildView.swift
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を使ってレイアウトを組んでいます。

ChildView.swift
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に入れています。

ParentView.swift
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)
        }
    }
}

以上です。

参考文献

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
8
Help us understand the problem. What are the problem?