はじめに
最近、仕事でも個人開発でも SwiftUI を触る機会が増えてきており、学習を進めています。
本記事では、GeometryReader
について学習した内容と、
個人的に特徴的だと感じた挙動について、備忘録として記載しています。
間違っている点などあればご指摘いただけると幸いです🙇
GeometryReader とは?
まず、GeometryReader
の基本について以下に列挙しています。
-
GeometryReader
は View の一種です- 他の View と同じように、View 階層内に宣言的に記述して使用します
-
GeometryReader
はGeometryProxy
から「自身(==GeometryReader
)のサイズ」や「自身の座標(x, y)」等の情報を取得できます -
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
を触る中で、個人的に意識しておいた方がよさそうに感じた挙動を以下に記載します。
-
GeometryReader
内の View は左上を起点にレイアウトされる - 高さ未指定の
GeometryReader
をScrollView
の中に入れると 10pt の高さしか与えられない -
GeometryReader
のサイズは子 View のサイズにフィットするように調整されるわけではない-
VStack
やHStack
と同じ感覚で使うと失敗する
-
1. GeometryReader 内の View は左上を起点にレイアウトされる
VStack
や HStack
では、子 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)
}
}
}
}
VStack
や HStack
と同じように 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 のサイズが独立していることがわかります。
参考文献