IT アプリ開発

【SwiftUI】GeometryReaderでViewのサイズを知ろう

2021年11月2日


GeometryReaderを使用すると、Viewで描画をする際のテクニックをより深く身につけることができます。
簡単ですがGeometryReaderがどういうものなのかを話していこうと思います。

 

GeometryReaderは特別なViewで、自身のサイズと座標空間を返す関数をクロージャーとして保持しています。そのクロージャーを通して、自身のViewのサイズや座標位置やRootViewのサイズや座標位置も取得することができます。
引用:https://blog.personal-factory.com/2019/12/08/how-to-know-coorginate-space-by-geometryreader/

GeometryReader自身もViewである

GeometryReaderは、Viewの大きさを教えてくれるわけですが、実は上でも軽く触れた通りGeometryReader自身もViewになります。
情報を教えてくれるわけで、表示されるものではないのでViewではないのでは?と思いますが、
Viewが書けるところにこのGeometryReaderのインスタンスを生成することによって、そのGeometryReaderが置かれた場所でのViewのサイズを把握することが可能になります。

 

実際に以下のGeometryReaderの定義コードを見てもプロトコルViewに準拠していることがわかります。

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct GeometryReader : View where Content : View {

    public var content: (GeometryProxy) -> Content

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

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    public typealias Body = Never
}

 

そしてGeometryReader{}自身のViewのサイズとあり、

GeometryReader(){ geometry in

}

のgeometryでGeometryReaderというViewのサイズを取得できる。

GeometryReaderはViewプロトコルに準拠しているので、Text()やButton()のように、
var body: some viewの中に書くことができる。
そしてGeometryReader()を記載した箇所のViewのサイズを取得できるのが、GeometryReaderの醍醐味です。

ということは、

GeometryReader(){ geometry in 
  Text(String("\(geometry.size)"))  
}.frame(100,200)

は出力すると、(100,200)になるかと思います。

よくいろんな記事で親Viewのサイズがわかるってあったので、GeometryReaderViewの親Viewの大きさが見れると
思い込んでいて、それが間違っていた。

 

GeometryReaderの書き方

ポイントはGeometryReaderはViewなので、Viewが書けるところに同じように記載できます。
GeometryReader()でインスタンスを生成すると、自動的にそのGeometryReader()宣言した箇所のViewの大きさが決まるので、
その大きさが以下ではgeometryという変数名で取得できるようになっています。

struct GeometryReaderTest:View{
    var body: some View{
        VStack(){
            Text("")
            GeometryReader(){ geometry in
                var _ = print(geometry)
            }
            Text("")
        }
    }
}

 

GeometryReaderで実際にViewのサイズを取得してみる

ここでは簡単な例で実際にGeometryReaderを使ってViewのサイズをとってみたいと思います。

 

【例1】View全体の場合

struct GeometryContentView: View {
    var body: some View {
        VStack(){
            GeometryReader() { geometry in
                Text(String("\(geometry.size)"))
            }.frame(width: 200, height: 300)
        }
        .frame(width: 400, height: 600)
    }
}

GeometryReaderに対してframeでサイズを(200,300)としてるので、当然ながら実行結果の表示は(200,300)になる。
GeometryReaderは親Viewの大きさを取得できるとあるけど、
GeometryReaderはViewなので、GeometryReaderを親として上記のようにText()が子Viewになるが、
Text()からみて親のViewの大きさが取れるということ。
つまりGeometryReaderから見れば自身のView、Textから見れば親のViewということになる。

GeometryReaderの親Viewとすると、VStackになるので(400,600)となりそうだけど違います。

 

【例2】同列で他にViewが存在する場合

struct GeometryContentView: View {
    var body: some View {
        VStack(spacing: 0){
            Color.yellow.frame(width: UIScreen.main.bounds.width, height: 200)
            GeometryReader() { geometry in
                Text(String("\(geometry.size)"))
            }.background(Color.green)
        }
    }
}

※ ビルドはiPhone 12 Pro Max
幅428、高さ926(UIScreenサイズ) = UIScreenはステータスバーとセーフエリアも含む(要は画面の大きさ)
上記実行するとTextの出力結果は(428, 645)で、ViewとViewの間で何pxか隙間がないようにspacingを0にしてます。

そしてセーフエリアと呼ばれる箇所についてはtopとbuttomがあるので、それぞれを取得する。以下のコードで取得でき、

struct GeometryContentView7: View {
    var body: some View {
        GeometryReader { geometry in
            HStack{
                Text(String("\(geometry.safeAreaInsets.top)"))
                Text(String("\(geometry.safeAreaInsets.bottom)"))
            }
        }
    }
}

この結果、topは47、bottomは34

セーフエリアはコンテンツが表示されない箇所なので、
UIScreenの高さ - ( セーフエリアのtopの高さ + セーフエリアのbottomの高さ )
が表示できる箇所で、926 - ( 47 + 34 ) = 845

そして今Color.yellowが高さ200なので、
GeometryReaderのViewサイズは
幅428、高さ645(845 - 200)となる

 

GeometryReaderは特殊なViewなので、
GeometryReaderが入っている箇所のViewの大きさを出していて、
例2のように他のViewのサイズが決まればGeometryReaderのViewサイズを取得できるが、
他のViewの大きさを子Viewでは取得できないので、Geometryを用いて取得できる

 

GeometryReaderを使って構成比グラフを作ってみる

swiftのchartsライブラリには、構成比グラフが作れません。
そのため自作が必要になるわけですが、ここで例としてGeometryReaderを使って作ってみます。
 

GeometryReaderはViewでもあるので、GeometryReaderに対して構成比グラフの幅や高さをframeで定義し、
そして、その中に性別ごとの割合を色分けして表示してみます。
構成比グラフを横並びに作りたいので、
GeometryReaderの中にHStackを入れて、四角形のRectangleで作ってみます。
 

以下プログラムの例です。
 

struct CompositionRatioView: View {
    
    public var male:Int = 300
    public var female:Int = 400
    public var other:Int = 100
    public var total:Int{
        return male+female+other
    }
    
    var body: some View {
        GeometryReader(){ geometry in
            HStack(spacing: 0){
                Rectangle()
                    .fill(Color.yellow)
                    .frame(width: geometry.size.width * CGFloat(male)/CGFloat(total), height: geometry.size.height)
                    .overlay(Text("男性"))
                    .border(Color.black, width: 1)
                Rectangle()
                    .fill(Color.green)
                    .frame(width: geometry.size.width * CGFloat(female)/CGFloat(total), height: geometry.size.height)
                    .overlay(Text("女性"))
                    .border(Color.black, width: 1)
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: geometry.size.width * CGFloat(other)/CGFloat(total), height: geometry.size.height)
                    .overlay(Text("その他"))
                    .border(Color.black, width: 1)
            }
        }
        .frame(width: UIScreen.main.bounds.width - 28, height: 50)
        HStack(){
            Text("男性:\(male) 構成比:\(male*100/total)%")
            Rectangle().fill(Color.yellow).frame(width: 15, height: 15)
        }
        HStack(){
            Text("女性:\(female) 構成比:\(female*100/total)%")
            Rectangle().fill(Color.green).frame(width: 15, height: 15)
        }
        HStack(){
            Text("その他:\(other) 構成比:\(other*100/total)%")
            Rectangle().fill(Color.blue).frame(width: 15, height: 15)
        }
    }
}

これを実行してみると、以下のようになります。


 

GeometryReaderから取得できる情報

GeometryReaderから取れる情報としては以下のような情報があります。

struct GeometryReaderTest:View{
    var body: some View{
        VStack(){
            GeometryReader(){geometry in
                Text("")
                // ===== ビルド iPhone12 Pro Max =====
                var _ = print(geometry)
                var _ = print(geometry.size.width)       // 428
                var _ = print(geometry.size.height)       // 845
                var _ = print(geometry.frame(in: .local).midX)        // 214
                var _ = print(geometry.frame(in: .local).midY)      // 422.5
                var _ = print(geometry.frame(in: .global).midX)        // 214
                var _ = print(geometry.frame(in: .global).midY)        // 469.5 (safeareaも含み?)
                var _ = print(geometry.frame(in: .global).origin.x)
                var _ = print(geometry.frame(in: .global).origin.y)
                var _ = print(geometry.frame(in: .global).width)
                var _ = print(geometry.frame(in: .global).height)
                var _ = print(geometry.safeAreaInsets)   // top:47, leading:0, bottom:34, trailing:0
            }
        }
    }
}
  • geometry:GeometryReaderから取得するオブジェクト
  • geometry.frame(in: .local):coordinateSpace(座標空間)がlocalという現在参照しているGeometryReaderViewを1つの基準の座標空間とした場合のオブジェクト
  • geometry.frame(in: .global):coordinateSpace(座標空間)がglobalという画面全体を1つの基準の座標空間とした場合のオブジェクト

※ Viewに.coordinateSpace(name: "scroll")のようにcoordinateSpaceモディファイアをつけて座標空間作成しつつその座標空間に名前をつけてgeometry.frame(in:scroll)のようにしてアクセスすることも可能。
上のサンプルコードではGeometryReaderから取得したオブジェクトをgeometryという変数名で扱うとして、

取得 説明
geometry GeometryReaderから取得するオブジェクト
geometry.size.width GeometryReaderView(空いてる空間)の横幅の大きさ
geometry.size.height GeometryReaderView(空いてる空間)の縦幅の大きさ
geometry.frame(in: .local).midX ローカル基準でのViewの真ん中の横幅
geometry.frame(in: .local).midY ローカル基準でのViewの真ん中の縦幅
geometry.frame(in: .global).midX 画面全体を基準とした場合の対象Viewの真ん中の横幅
geometry.frame(in: .global).midY 画面全体を基準とした場合の対象Viewの真ん中の縦幅
geometry.frame(in: .global).origin.x
geometry.frame(in: .global).origin.y
geometry.frame(in: .global).width 画面全体を基準とした場合の対象Viewの横幅
geometry.frame(in: .global).height 画面全体を基準とした場合の対象Viewの縦幅
geometry.safeAreaInsets SafeAreaの高さや幅の情報
(top, leading, bottom, trailing)

対象Viewの真ん中の位置取得できたりするので、
スクロールした際にその位置情報から他の物体に近づいたら反発させるようなアニメーションであったり、
ある位置に来たら拡大するなど、細かな挙動を作成できたりできます。

 

まとめ

GeometryReaderを使うことで、空いているViewスペースの大きさを取得できるので、それによってGeometryReaderView内のViewの配置を、ローカル起点で考えることができたり(Readerからはみ出ないようにViewの配置ができる)、Viewの真ん中の位置を取得できるので、さまざまなアニメーションに使用できたりします。

 

-IT, アプリ開発
-

© 2024 Yosshi Labo. Powered by AFFINGER5