[SwiftUI]GeometryReaderを活用した、Global座標、Local座標の取得方法と.offsetモディファイヤの変化に伴う座標の変化

Xcode: 12.4, Swift: 5

SwiftUIでは、Viewを複数作成し、それらを組み合わせて表現したいViewを作成するかたちになりますが、狙った場所にViewを配置するためには、適切にViewの座標を把握することが重要になります。ここでは、Global座標、Local座標というところに着目して、その値の取得方法やViewの配置方法と、親子それぞれでオフセットを変更した場合に、どのように座標の値が変化するのかを簡単に整理したいと思います。

概要

親となる、黒色四角形の上に、ZStackを用いて、少し小さいサイズの黄色四角形を配置しながら、Global座標とLocal座標を確認していきます。

//  ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Rectangle()
                .frame(width: 300, height: 400, alignment: .center)
            ChildRectangle(str: "Child")
        }
    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        ContentView()
    }
}
//  ChildRectangle.swift
import SwiftUI

struct ChildRectangle: View {
    var str: String

    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color.yellow)
                .frame(width: 150, height: 180)
            Text(str)
                .font(.system(size: 20))
        }
    }
}

struct ChildRectangle_Previews: PreviewProvider {
    static var previews: some View {
        ChildRectangle(str: "Child")
    }
}
この状態から色々試していきます

子Viewである黄色四角形でGeometryReaderを用いてGlobal座標を表示する

それでは、子ViewであるChildRectangleでGeometryReaderを利用できるようにし、その座標を表示するようにしてみます。まずは、GeomertyReaderを利用可能にします。(特段GeometryReaderでの操作を行っていないのですが、黄色い四角形の配置が変化します。)

struct ChildRectangle: View {
    var str: String

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: 150, height: 180)
                Text(str)
                    .font(.system(size: 20))
            }
        }
    }
}

次に、Globalでの座標を表示してみます。GeomertyReaderを用いてGlobalの座標を取得するためには、GeometryProxyのインスタンスメソッドであるframeを利用します。以下のようにCoordinateSpaceの定数(今回は.global)を指定しアクセスすることで、CGRect型のアイテムを取得することができます。

geometry.frame(in: .global)

CGRect型には、高さや幅を保持するプロパティもありますが、今回は座標を取得したいため、以下の4つのプロパティを利用することにします。これら4つのプロパティが、この黄色い四角形の四隅のGlobal座標となります。

  • minX
  • maxX
  • minY
  • maxY

参考:
[Structure]GeomertyProxy
[Instance Method]frame(in:)

//  ChildRectangle.swift

struct ChildRectangle: View {
    var str: String

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: 150, height: 180)
                VStack {
                    Text(str)
                        .font(.system(size: 20))
                    Text("Global:\n X(\(Int(geometry.frame(in: .global).minX)),  \(Int(geometry.frame(in: .global).maxX)))\n Y(\(Int(geometry.frame(in: .global).minY)).  \(Int(geometry.frame(in: .global).maxY)))\n")
                }
            }
        }
    }
}
Global座標を表示した状態

黄色四角形の左上が(0, 20)の座標で、右下の座標が(375, 667)であることがわかります。

子Viewの位置を動かして、座標の変化を確認する。

次に、ContentView上で、.offsetモディファイヤを使って、子Viewである黄色四角形の配置を動かして見たいと思います。

//  ContentView.swift
struct ContentView: View {
    var body: some View {
        ZStack {
            Rectangle()
                .frame(width: 300, height: 400, alignment: .center)
            ChildRectangle(str: "Offset (100, 200)")
                .offset(x: 100, y: 200)
        }
    }
}
オフセットで指定した分だけずれて表示される

先程の位置(左上)から、.offsetモディファイヤに指定した値どおりに、右に100pt、下に200pt移動し、Globalの座標もそれぞれ100、200ずつ大きくなっています。

Local座標の表示

次に、Local座標を表示していきたいと思います。Local座標をGeometryReaderで取得するには、Global座標を取得したときのCoordinateSpaceの定数に.localを指定することで可能になります。

geometry.frame(in: .local)

Global同様に四隅の座標を取得します。

//  ChildRectangle.swift
struct ChildRectangle: View {
    var str: String

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: 150, height: 180)
                VStack {
                    Text(str)
                        .font(.system(size: 20))
                    Text("Global:\n X(\(Int(geometry.frame(in: .global).minX)),  \(Int(geometry.frame(in: .global).maxX)))\n Y(\(Int(geometry.frame(in: .global).minY)).  \(Int(geometry.frame(in: .global).maxY)))\n")
                    Text("Local:\n X(\(Int(geometry.frame(in: .local).minX)),  \(Int(geometry.frame(in: .local).maxX)))\n Y(\(Int(geometry.frame(in: .local).minY)).  \(Int(geometry.frame(in: .local).maxY)))")
                }
            }
        }
    }
}

このとき、Global座標とLocal座標の値が異なることがわかります。子ViewであるChildRectangleのPreviewを確認してみると、右の用になっています。子Viewの中では、一番左上に配置されていますが、GlobalではStatusBar(SafetyArea)である20pt分加算された状態で表現されています。Local座標に関しては、親View(ContentView)上でも子Viewで取得したものと同値が表示されています。

子Viewで配置を移動してみる

次に、子View内でこのZStackで積んだRectangleとTextを.offsetモディファイヤを使って移動してみます。

//  ChildRectangle.swift
struct ChildRectangle: View {
    var str: String

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: 150, height: 180)
                VStack {
                    Text(str)
                        .font(.system(size: 20))
                    Text("Global:\n X(\(Int(geometry.frame(in: .global).minX)),  \(Int(geometry.frame(in: .global).maxX)))\n Y(\(Int(geometry.frame(in: .global).minY)).  \(Int(geometry.frame(in: .global).maxY)))\n")
                    Text("Local:\n X(\(Int(geometry.frame(in: .local).minX)),  \(Int(geometry.frame(in: .local).maxX)))\n Y(\(Int(geometry.frame(in: .local).minY)).  \(Int(geometry.frame(in: .local).maxY)))")
                }
            }
            .offset(x: 50, y: 50)
        }
    }
}
ビルドした結果
ChildRectangleでのPreview

ContentViewとしては、子Viewで設定したオフセットの分だけずれて黄色四角形が表示されますが、GeomertyReaderで取得したGlobal座標、Local座標共に、子Viewでオフセットを設定する前と同じ値が表示されています。ChildRectangleのPreview画面で確認できますが、オフセットされたオブジェクトも表示こそずれて表示されていますが、オブジェクト自体の位置は青い枠で示された場所にいることが示されています。

まとめ

GeomertyReaderを使って、Global座標、Local座標を取得し、親子それぞれのViewでオフセットによるViewの位置の変化が座標にどう反映されるのかをみてみました。今回は.offsetモディファイヤを使って検証してみましたが、このあたりまだまだ理解も足りず、奥も深そうなので、また別の機会に追加で検証してみようと思います。