【SwiftUI】 比率によるレスポンシブな配置を提案してみる

こんにちは uni928 です。
今回は SwiftUI で
レスポンシブな配置を行う方法について
自己流の方法を提案してみたいと思います。

全ての要素を
比率で配置していく形式になります。

サンプルコードを載せて
その解説を行う形式で
紹介したいと思います。

それではよろしくお願いいたします。

***

今回は次のような見た目の
配置を目指したいと思います。

文字とボタン 6 個だけの
簡単な画面になります。

![AutoLayoutQuestionScreenShot1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3752886/3b9c8bbf-e216-b4ab-814a-dfde7789b14c.png)

***

完成コード

```swift:完成コード
import SwiftUI

struct ContentView: View {
    
    var body: some View {
        GeometryReader { bodyView in
            getFirstView(bodyView.size.width, bodyView.size.height)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

/**
 * 画面全体 View
 *
 * 9:19.5 をなるべく大きく切り取って
 * そこに getMainContent を配置
 */
func getFirstView(_ width: CGFloat, _ height: CGFloat) -> some View {
    
    //端末が 9:19.5 以上に縦長の場合
    if 19.5 / 9.0 <= height / width
    {
        let myWidth = width
        let myHeight = width * 19.5 / 9.0
        
        return AnyView (
            VStack(spacing: 0){
                
                //上の余白
                VStack(spacing: 0){}
                    .frame(width: width, height: ((height - myHeight) / 2))
                
                //メインコンテンツ
                getMainContent(myWidth, myHeight)
                    .frame(width: myWidth, height: myHeight)
                
                //下の余白
                VStack(spacing: 0){}
                    .frame(width: width, height: ((height - myHeight) / 2))
            }
            .frame(width: width, height: height)
        )
    }
    else
    {
        let myWidth = height / 19.5 * 9.0
        let myHeight = height
        
        return AnyView (
            HStack(spacing: 0) {
                
                //左の余白
                HStack(spacing: 0){}
                    .frame(width: ((width - myWidth) / 2), height: height)
                
                //メインコンテンツ
                getMainContent(myWidth, myHeight)
                    .frame(width: myWidth, height: myHeight)
                
                //右の余白
                HStack(spacing: 0){}
                    .frame(width: ((width - myWidth) / 2), height: height)
            }
            .frame(width: width, height: height)
        )
    }
}

/**
 * 画面比率 9:19.5 のメインの配置
 */
func getMainContent(_ width: CGFloat, _ height: CGFloat) -> some View {
    return VStack(spacing: 0) {
        
        //上の空間
        HStack{}
            .frame(width: width, height: (height / 12 * 4.2))
        
        //ボタンのある横1列
        getHorizontalLine(width, height / 12 * 2, 0)
        
        //ボタンとボタンの間の余白
        HStack{}
            .frame(width: width, height: (height / 12 * 0.6))
        
        //ボタンのある横1列
        getHorizontalLine(width, height / 12 * 2, 1)
        
        //ボタンとボタンの間の余白
        HStack{}
            .frame(width: width, height: (height / 12 * 0.6))
        
        //ボタンのある横1列
        getHorizontalLine(width, height / 12 * 2, 2)
        
        //下の空間
        HStack{}
            .frame(width: width, height: (height / 12 * 0.6))
    }
        .frame(width: width, height: height)
}

/**
 * 横1列
 */
func getHorizontalLine(_ width: CGFloat, _ height: CGFloat, _ lineID: Int) -> some View{
    return HStack(spacing: 0){
        
        //左の余白
        ZStack{}.frame(width: (width / 12 * 1.3), height: height)
        
        //左のボタン
        getButtonView(width / 12 * 4, height, lineID * 2)
        
        //ボタンとボタンの間の余白
        ZStack{}.frame(width: (width / 12 * 1.4), height: height)
        
        //右のボタン
        getButtonView(width / 12 * 4, height, lineID * 2 + 1)
        
        //右の余白
        ZStack{}.frame(width: (width / 12 * 1.3), height: height)
    }
        .frame(width: width, height: height)
}

/**
 * ボタン部分のView
 */
func getButtonView (_ width: CGFloat, _ height: CGFloat, _ buttonID: Int) -> some View {
    return Button(action: {buttonAction(buttonID)}){
        Text("button")
            .foregroundColor(Color.white)
            .font(.system(size: width / 5))
            .fontWeight(.bold)
    }
        .frame(width: width, height: height).background(.blue)
}

/**
 * ボタンの処理
 */
func buttonAction(_ buttonID: Int) {
    switch(buttonID){
    case 0:
        print("button0 が押されました")
    case 1:
        print("button1 が押されました")
    case 2:
        print("button2 が押されました")
    case 3:
        print("button3 が押されました")
    case 4:
        print("button4 が押されました")
    case 5:
        print("button5 が押されました")
    default:
        print("予期せぬボタンが押されました")
    }
}
```

getFirstView メソッドの中で
9:19.5 のレイアウトを配置しています。

![AutoLayoutQuestionScreenShot2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3752886/2e255616-65ea-c38c-d6d5-f6ba89810154.png)


getMainContent メソッドの中で
次の図のようにレイアウトを配置しています

![AutoLayoutQuestionScreenShot3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3752886/7e754e60-fb05-6057-e24a-2bf0132182e3.png)


getHorizontalLine メソッドの中で
次の図のようにレイアウトを配置しています。

![AutoLayoutQuestionScreenShot4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3752886/9ecae079-a8df-06e2-8fcb-06dbcdb61a3c.png)

これらの配置を行えば
後はボタンとテキストを
レイアウト上に配置するだけです。

***

以上が私個人的な
SwiftUI のレスポンシブな配置方法になります。
恐らく独自の方法ではないでしょうか？

複雑な配置も
この方法なら意外と簡単にできます。
是非お試しください。

この記事が
皆さんの開発の役に立てれば幸いです。

閲覧ありがとうございました。