LoginSignup
3
1

【SwiftUI】GeometryReader の基本と特徴的な挙動【入門編】

Posted at

はじめに

最近、仕事でも個人開発でも SwiftUI を触る機会が増えてきており、学習を進めています。

本記事では、GeometryReader について学習した内容と、
個人的に特徴的だと感じた挙動について、備忘録として記載しています。

間違っている点などあればご指摘いただけると幸いです🙇

GeometryReader とは?

まず、GeometryReader の基本について以下に列挙しています。

  1. GeometryReader は View の一種です
    • 他の View と同じように、View 階層内に宣言的に記述して使用します
  2. GeometryReaderGeometryProxy から「自身(== GeometryReader )のサイズ」や「自身の座標(x, y)」等の情報を取得できます
  3. GeometryReader は「GeometryProxy をもとに、1つ以上の View をレイアウトするための ContainerView である」と理解しています
    • 「どんなときに GeometryReader を使うのか?」といえば、「GeometryProxy から取得できる情報をもとにレイアウトを組みたい場合」だといえると思います

以下に具体的なコードを記載し、実際の動作を確認してみます。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {

            // 1. GeometryReader 自身も View のため、View 階層内に宣言的に記述可能です

            GeometryReader { proxy in
                // 2. ↑この「proxy」が「GeometryProxy」のインスタンスであり、
                //    ここから取得できる情報をもとに、例えば以下のようにレイアウトを組むことができます。
                //    また、以下は @ViewBuilder クロージャ内なので、次のように View を宣言的に列挙することができます

                Rectangle()
                    .fill(.red)
                    // 3. GeometryProxy から GeometryReader 自身のサイズを取得可能です。
                    //    ここでは Rectangle の高さを GeometryReader の半分の高さに指定しています
                    .frame(height: proxy.size.height / 2)

                Text("サンプル")
                    // 3. GeometryProxy から GeometryReader 自身の座標情報を取得可能です。
                    //    ここでは Text の中心位置が GeometryReader の中心位置になるように指定しています
                    .position(x: proxy.frame(in: .local).midX, y: proxy.frame(in: .local).midY)
            }

        }
    }
}

上記コードの実行結果:

なお、GeometryReader が複数の View を受け取れるのは、イニシャライザのクロージャが以下の通り @ViewBuilder として定義されているためです。

init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)

GeometryProxy から取得できる情報

GeometryProxy から取得できる情報はいくつかありますが、
ここでは個人的によく使いそうな2種類を記載しています。

  • GeometryReader 自身のサイズ
    • proxy.size: CGSize
  • GeometryReader 自身の座標位置
    • proxy.frame(in: .local): CGRect
      • ローカル座標
    • proxy.frame(in: .global): CGRect
      • グローバル座標

以下で具体的にみていきます。

GeometryReader 自身のサイズを取得する

ここでは GeometryProxy からサイズを取得するサンプルコードを載せています。

まず、以下では VStack 内に GeometryReader を2つ置き、GeometryProxy から取得したサイズを Text に表示しています。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            VStack {
                // GeometryReader 自身に .frame でサイズを指定していないため、親から提案されたサイズがそのまま自身のサイズとなる
                GeometryReader { proxy in
                    Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
                }
                .border(.red)
                
                GeometryReader { proxy in
                    Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
                }
                .border(.green)            
        }
    }
}

上記コードの実行結果:

また、GeometryReader.frame で直接サイズを指定している場合、
proxy.size には .frame で指定した値が入ってきます。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                // proxy.size には (300, 300) が入ってくる
                Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
            }
            // ここで GeometryReader のサイズを指定
            .frame(width: 300, height: 300)
            .border(.red)
        }
    }
}

上記コードの実行結果:

ただし、以下のように .padding().frame() というふうにして余白領域を追加している場合は少し挙動が変わります。
.padding で追加した余白領域を取り除いた実際の GeometryReader のサイズが GeometryProxy で取得できるようです。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                // 「余白領域 + GeometryReader のサイズ == (300, 300)」となる
                // -> 余白領域を除いた GeometryReader のサイズは (280, 280) なので、これが proxy.size から取得できる
                Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
            }
            .padding(10)
            .frame(width: 300, height: 300)
            .border(.red)
            
        }
    }
}

上記コードの実行結果:

GeometryReader 自身の座標位置を取得する

GeometryProxy から取得した座標位置を画面に表示するサンプルコードを載せています。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                // ローカル座標の位置を表示
                Text("local.x: \(proxy.frame(in: .local).origin.x.description), local.y: \(proxy.frame(in: .local).origin.y.description)")
                
                // グローバル座標の位置を表示
                Text("global.x: \(proxy.frame(in: .global).origin.x.description), global.y: \(proxy.frame(in: .global).origin.y.description)")
                    .padding(.top, 30)
            }
            .frame(width: 300, height: 300)
            .border(.red)
        }
    }
}

上記コードの実行結果:

ローカル座標の場合、常に位置は (0, 0) になると思われるので、
proxy.frame(in: .local) はあまり使うことがなさそうに思えます。

一方でグローバル座標の場合は、「RootView に対して GeometryReader がどこに位置されているか」を取得することができるので、色々と応用できそうな気がします。

GeometryReader の特徴的な挙動

最後に、今回 GeometryReader を触る中で、個人的に意識しておいた方がよさそうに感じた挙動を以下に記載します。

  1. GeometryReader 内の View は左上を起点にレイアウトされる
  2. 高さ未指定の GeometryReaderScrollView の中に入れると 10pt の高さしか与えられない
  3. GeometryReader のサイズは子 View のサイズにフィットするように調整されるわけではない
    • VStackHStack と同じ感覚で使うと失敗する

1. GeometryReader 内の View は左上を起点にレイアウトされる

VStackHStack では、子 View はデフォルトで中央(center)に配置されますが、
GeometryReader 内の View は左上 (topLeading) に配置されます。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                Rectangle()
                    .fill(.yellow)
                    .frame(height: 100)
                
                Text("sample")
                    .frame(height: 60)
            }
        }
    }
}

上記コードの実行結果:

VStackHStack と同じように center に配置されるようにするには、
子Viewの .position (中心座標) を GeometryReader の中心座標に移動させる必要があります。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                Rectangle()
                    .fill(.yellow)
                    .frame(height: 100)
                    .position(x: proxy.frame(in: .local).midX, y: proxy.frame(in: .local).midY)

                Text("sample")
                    .frame(height: 60)
                    .position(x: proxy.frame(in: .local).midX, y: proxy.frame(in: .local).midY)

            }
        }
    }
}

上記コードの実行結果:

2. 高さ未指定の GeometryReader を ScrollView の中に入れると、10pt の高さしか与えられない

※ 縦スクロールの前提で記載しています

SwiftUI の View の中には「与えられた領域いっぱいに広がる」という性質を持つものがいくつかあります。
Spacer, Rectangle など)

GeometryReader もこれらの View と同じ性質を持っています。

こうした性質を持つ View を高さ指定せずに ScrollView に入れると、10pt の高さしか与えられないようです。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ScrollView {
                GeometryReader { proxy in
                    Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
                }
                .border(.red)
            }
        }
    }
}

上記コードの実行結果:

ScrollView は理論的に無限大の高さを取れるのでこのような挙動になると思われます。
このため、ScrollView 内で GeometryReader を使う場合は .frame で明示的に高さを指定する必要がありそうです。

3. GeometryReader のサイズは子 View のサイズにフィットするように調整されるわけではない

以下のように、VStack であれば子 View の高さに応じて VStack 自身の高さも自動的に調整されます。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            VStack {
                Text("サンプル")
                    .frame(height: 60)
                    .background(.green)
            }
            .border(.red)
        }
    }
}

上記コードの実行結果:

しかし、GeometryReader の場合は、GeometryReader 自身のサイズは子 View のサイズに影響を受けないようです。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader { proxy in
                Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
                    .frame(height: 60)
                    .background(.green)
                    .position(x: proxy.frame(in: .local).midX, y: proxy.frame(in: .local).midY)
            }
            .border(.red)
        }
    }
}

上記コードの実行結果:

特に ScrollView に入れる際には注意が必要です。

struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ScrollView {
                GeometryReader { proxy in
                    Text("width: \(proxy.size.width.description), height: \(proxy.size.height.description)")
                        .background(.green)
                        .position(x: proxy.frame(in: .local).midX, y: proxy.frame(in: .local).midY)
                }
                .border(.red)
            }
        }
    }
}

上記コードの実行結果:

GeometryReader 内の子 View (Text) がはみ出て表示されており、
子 View のサイズと親 View のサイズが独立していることがわかります。

参考文献

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1