NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    【SwiftUI】GeometryRederでViewのサイズや座標(位置)を取得する

    概要

    GeometryReaderはSwiftUIで使用できるContainer Viewで、Viewのサイズや座標を取得することができます。
    Viewのサイズや座標を取得できるようになると以下のような動的にサイズを変更したりすることが容易にできるようになります。

    今回はそんなGeometryReaderの使用方法と事例を紹介したいと思います。

    サイズの取得方法

    スクリーンサイズの取得はUIScreen.main.bounds.widthなどでもできますが、Viewのサイズを取得するのはGeometryReaderを使用します。
    概要にあるようにContainer Viewでクロージャーの引数はGeometryProxy型となります。
    GeometryProxyはGeometryReaderのサイズと座標空間にアクセスするためのプロキシですので、この引数を使用することでサイズを取得する事ができます。
    以下幅と高さを取得するサンプルコードです。

    struct HogeView: View {
        var body: some View {
            GeometryReader { geometry in
                VStack {
                    Text("x:\(geometry.size.width)")
                    Text("y:\(geometry.size.height)")
                }
            }
        }
    }
    

    プレビューはこんな感じです。

    Viewのサイズを定義していないのでSafeAreaを除いた画面サイズを返しています。
    SafeAreaを含めると以下のようになります。

    struct HogeView: View {
        var body: some View {
            GeometryReader { geometry in
                VStack {
                    Text("x:\(geometry.size.width)")
                    Text("y:\(geometry.size.height)")
                }
                .frame(width: geometry.size.width, height: geometry.size.height)
            }
            .edgesIgnoringSafeArea(.all)
        }
    }
    

    プレビューはこんな感じです。

    適当な四角のViewを作って親Viewで使用&サイズを定義してもちゃんとサイズを表示してくれる様子です。

    上記をよりSwiftUIらしく書き換えたサンプルコードです。

    import SwiftUI
    
    struct HogeTopView: View {
        var body: some View {
            VStack {
                HogeRectangle(widthSize: 150, heightSize: 100)
                HogeRectangle(widthSize: 200, heightSize: 150)
                HogeRectangle(widthSize: 300, heightSize: 200)
            }
        }
    }
    
    struct HogeTopView_Previews: PreviewProvider {
        static var previews: some View {
            HogeTopView()
        }
    }
    
    struct HogeRectangle: View {
        @State var widthSize: CGFloat
        @State var heightSize: CGFloat
        var body: some View {
            GeometryReader { geometry in
                ZStack {
                    Rectangle()
                        .stroke(lineWidth: 1)
                    VStack {
                        Text("幅:\(Int(geometry.size.width))")
                        Text("高さ:\(Int(geometry.size.height))")
                    }
                }
            }
            .frame(width: widthSize, height: heightSize)
        }
    }
    

    以上がGeometryReaderを使用してViewのサイズを取得する方法です。

    座標の取得方法

    続いて座標の取得です。
    座標の取得に関してはglobalとlocalがあり、それぞれ特徴があります。

    global

    まず簡易的なサンプルコードとプレビューです。

    struct HogeView: View {
        var body: some View {
            GeometryReader { geometry in
                VStack {
                    Text("X: \(Int(geometry.frame(in: .global).origin.x))")
                    Text("Y: \(Int(geometry.frame(in: .global).origin.y))")
                }
            }
        }
    }
    

    サイズ、座標の両取得に言える事ですが、GeometryReaderでは座標は左上からX軸は右へ、Y軸は下への距離で表示しています。

    そのため、今回X=0、Y=47と表示された理由はVstackの座標は左から0、上から47(赤枠のSafeArea分)の位置にあることを表しています。

    local

    続いてlocalを使用してみます。

    struct HogeView: View {
        var body: some View {
            GeometryReader { geometry in
                VStack(alignment: .center) {
                    Text("X: \(Int(geometry.frame(in: .local).origin.x))")
                    Text("Y: \(Int(geometry.frame(in: .local).origin.y))")
                }
            }
        }
    }
    

    プレビューの様子を見ていただくとわかるように、X、Y共に0となっています。
    localの場合はGeometryReaderで生成されたViewがどこにあるかではなく、View内での座標を取得するため、originで座標を取得すると共に0になります。
    四角に囲いをつけると分かりやすいですが、画面でなく、この囲いの左上が常に基準となるので、どこに位置しようが上記の手法で取得すると0になります。

    min、mid、max

    ではどのような場面でlocalが使えるのかと言うとmin、mid、maxで座標を取得した時に効果を発揮します。
    まずはサンプルコードとプレビューです。

    import SwiftUI
    
    struct HogeView2: View {
        var body: some View {
            VStack(alignment: .leading) {
                Text("X軸のGlobal座標")
                GlobalRectangle()
                    .frame(maxWidth: .infinity, maxHeight: 150)
                    .background(
                        Rectangle()
                            .foregroundColor(.purple)
                    )
                Text("X軸のLocal座標")
                LocalRectangle()
                    .frame(maxWidth: .infinity, maxHeight: 150)
                    .background(
                        Rectangle()
                            .foregroundColor(.green)
                    )
                Spacer()
            }
            .padding(.horizontal)
        }
    }
    
    struct HogeView2_Previews: PreviewProvider {
        static var previews: some View {
            HogeView2()
        }
    }
    
    struct GlobalRectangle: View {
        var body: some View {
            GeometryReader { geometry in
                VStack(alignment: .leading, spacing: 10) {
                    HStack {
                        Text("minX: \(Int(geometry.frame(in: .global).minX))")
                        Spacer()
                        Text("midX: \(Int(geometry.frame(in: .global).midX))")
                        Spacer()
                        Text("maxX: \(Int(geometry.frame(in: .global).maxX))")
                    }
                    VStack(alignment: .leading) {
                        Text("minY: \(Int(geometry.frame(in: .global).minY))")
                        Spacer()
                        Text("midY: \(Int(geometry.frame(in: .global).midY))")
                        Spacer()
                        Text("maxY: \(Int(geometry.frame(in: .global).maxY))")
                    }
                }
            }
        }
    }
    
    struct LocalRectangle: View {
        var body: some View {
            GeometryReader { geometry in
                VStack(alignment: .leading, spacing: 10) {
                    HStack {
                        Text("minX: \(Int(geometry.frame(in: .local).minX))")
                        Spacer()
                        Text("midX: \(Int(geometry.frame(in: .local).midX))")
                        Spacer()
                        Text("maxX: \(Int(geometry.frame(in: .local).maxX))")
                    }
                    VStack(alignment: .leading) {
                        Text("minY: \(Int(geometry.frame(in: .local).minY))")
                        Spacer()
                        Text("midY: \(Int(geometry.frame(in: .local).midY))")
                        Spacer()
                        Text("maxY: \(Int(geometry.frame(in: .local).maxY))")
                    }
                }
            }
        }
    }
    

    min、mid、maxに対するXやY軸座標がどの位置を取得しているかは以下の通りです。

    min mid max
    X 最左 真中 最右
    Y 最上 真中 最下

    globalは画面から座標を取得している為、paddingでできたスペースが16であり、そこがGlobalRectangleの最左に当たる座標となることがわかります。
    localはmaxXやmaxYでLocalRectangleのサイズがわかるようになります。

    以上がGeometryReaderを使用して座標を取得する方法です。

    使用事例

    以上のGeometryReaderでViewのサイズや座標を取得する方法の紹介でしたが、これができるとどんな事ができるのか、少し実践的なサンプルを用意しました。

    Photo ListにImageを仮想としたViewを配置して、とある位置から位置までに到達した時に拡大、縮小をするようにしています。すると上記のようなViewを容易に作る事ができます。

    最後に

    SwiftUIではなかなか自由にUIを作る事ができないという話を時々聞きますが、実はGeometryReaderを使用すると解決できることが結構あると思います。
    しかしGeometryReaderは少し複雑な点やViewのレイアウト処理がガラっと変わったりするのでサンプルコードのようにGeometryReaderを使用したいViewに対してコンポーネント単位で切り分けて使用するところから始めてみることをおすすめします。
    わかりにくい部分、もっとこんな方法があるよと言う方は是非ご質問、ご指摘ください。

    執筆者岡優志

    iOSエンジニア
    iOSを専門とし、モバイルアプリの開発を行なっています。

    Twitter