GeometryReader自体の高さを子ビューのフレームサイズと同じにする


記事タイトルをどうすべきか結構悩んだんですがw、要はやりたいのはこういうことです。

Rectangleの高さを、与えられたフレーム幅を基に動的に変化させたいとします。
ここではフレーム幅の半分のサイズの高さを指定することとします。

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")

            GeometryReader { geometry in
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: geometry.size.width, height: geometry.size.width / 2)
            }

            Text("Hello")
        }
    }
}


こんな画面になります。

f:id:bamboohero:20220123172122p:plain


Rectangleのフレームサイズはframe(width:height:)で指定した通りですが、GeometryReader自体は「Hello」から「Hello」までのフレームサイズを持っています。

GeometryReader自体のフレームサイズを指定していないので、親であるVStack内の利用可能な領域いっぱいまでフレームサイズが広がるようです。


次に、VStackをScrollView内に入れてみます。

struct ContentView: View {
    var body: some View {
        ScrollView(.vertical) {  // 追加
            VStack {
                Text("Hello")

                GeometryReader { geometry in
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: geometry.size.width, height: geometry.size.width / 2)
                }

                Text("Hello")
            }
        }
    }
}


すると、こんな画面になります。

f:id:bamboohero:20220123173147p:plain


ScrollViewの中に入ったので、GeometryReaderの高さが不定?になり、Rectangleが下の「Hello」の領域まではみ出てしまっています。

子ビューであるRectangleの高さと同じ高さになることを期待してましたが、そうではないようです。


どうすれば期待通りにできるでしょうか?

私は以下のようなやり方で解決しました。

  1. 子ビュー(Rectangle)の高さを取得する
  2. 子ビューの高さをGeometryReaderのframe(height:)に指定する

実装の全体はこんな感じです。

struct ContentView: View {
    @State private var rectangleHeight: CGFloat = .zero

    var body: some View {
        ScrollView(.vertical) {
            VStack {
                Text("Hello")

                GeometryReader { geometry in
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: geometry.size.width, height: geometry.size.width / 2)
                        .readHeight($rectangleHeight)
                }
                .frame(height: rectangleHeight)

                Text("Hello")
            }
        }
    }
}

extension View {
    func readHeight(_ height: Binding<CGFloat>) -> some View {
        background(GeometryReader { geometry -> Color in
            DispatchQueue.main.async {
                height.wrappedValue = geometry.size.height
            }
            return Color.clear
        })
    }
}


ビューの高さを取得することができるreadHeight(_:)というModifierを定義しています。

func readHeight(_ height: Binding<CGFloat>) -> some View

backgroundにGeometryReaderをあてることで、そのビューのフレームサイズを取得することができるという機能を利用しています。

この実装については以下のリンクが参考になると思うので見てみてください。

qiita.com

stackoverflow.com


readHeight(_:)で取得したRectangleの高さは@Stateプロパティに格納され、それをGeometryReaderの高さとして指定しています。

@State private var rectangleHeight: CGFloat = .zero

...

ScrollView(.vertical) {
    VStack {
        Text("Hello")

        GeometryReader { geometry in
            Rectangle()
                .fill(Color.blue)
                .frame(width: geometry.size.width, height: geometry.size.width / 2)
                .readHeight($rectangleHeight)  // ここでRectangleの高さを取得
        }
        .frame(height: rectangleHeight)  // Rectangleの高さを指定

        Text("Hello")
    }
}


こうすることで、GeometryReaderがRectangle分のフレームサイズを持つことになり、意図した画面にすることができます。

f:id:bamboohero:20220123175002p:plain