読みやすいpaddingを付けるコツ(SwiftUI)

## はじめに

SwiftUIで読みやすい `padding` を付けるコツを紹介します。

## 環境

- OS：macOS Ventura 13.3.1
- Xcode：14.3 (14E222b)
- Swift：5.8

## リファクタリング前のコード

以下のコードを読みやすくなるようリファクタリングします。

```swift:FooView.swift
import SwiftUI

struct FooView: View {
    var body: some View {
        VStack(spacing: 0) {
            Image(systemName: "swift")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.red)
                .padding([.leading, .trailing], 24)

            Text("This is Swift. Swift is very beautiful programming language.")
                .padding(EdgeInsets(top: 8, leading: 24, bottom: 32, trailing: 24))

            Image(systemName: "gear")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.gray)
                .padding([.leading, .trailing], 24)

            Text("This is gear.")
                .padding(.top, 8)
                .padding([.leading, .trailing], 24)
        }
    }
}
```

## リファクタリング

### `.horizontal` や `.vertical` を使う

`.leading` と `.trailing` をまとめて `.horizontal` で表せます。
同様に `.top` と `.bottom` はまとめて `.vertical` と表せます。
記述が簡潔になるのでオススメです。

```diff_swift:FooView.swift
import SwiftUI

struct FooView: View {
    var body: some View {
        VStack(spacing: 0) {
            Image(systemName: "swift")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.red)
-               .padding([.leading, .trailing], 24)
+               .padding(.horizontal, 24)

            Text("This is Swift. Swift is very beautiful programming language.")
                .padding(EdgeInsets(top: 8, leading: 24, bottom: 32, trailing: 24))

            Image(systemName: "gear")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.gray)
-               .padding([.leading, .trailing], 24)
+               .padding(.horizontal, 24)

            Text("This is gear.")
                .padding(.top, 8)
-               .padding([.leading, .trailing], 24)
+               .padding(.horizontal, 24)
        }
    }
}
```

今回は使っていませんが、上下左右の `padding` は `.all` で表わせ、デフォルトは `.all` なので省略できます。
私は省略したほうが簡潔で読みやすいと思っています。

```swift
// 以下はすべて同義
// 下に行くほど読みやすいと思っている
.padding([.top, .leading, .bottom, .trailing], 24)
.padding(.all, 24)
.padding(24)
```

### `EdgeInset` を使わない

`EdgeInset` を使うことで、1行で上下左右の `padding` を設定できます。
しかし4方向すべてを指定するイニシャライザしか存在せず、 `.horizontal` のようにまとめたり、 一部分のみ省略したりできません。
好みもありますが、私は `EdgeInset` を使わず `.padding` を複数付けて表現します。

```diff_swift:FooView.swift
import SwiftUI

struct FooView: View {
    var body: some View {
        VStack(spacing: 0) {
            Image(systemName: "swift")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.red)
                .padding(.horizontal, 24)

            Text("This is Swift. Swift is very beautiful programming language.")
-               .padding(EdgeInsets(top: 8, leading: 24, bottom: 32, trailing: 24))
+               .padding(.top, 8)
+               .padding(.bottom, 32)
+               .padding(.horizontal, 24)

            Image(systemName: "gear")
                .resizable()
                .scaledToFit()
                .frame(width: 44, height: 44)
                .foregroundColor(.gray)
                .padding(.horizontal, 24)

            Text("This is gear.")
                .padding(.top, 8)
                .padding(.horizontal, 24)
        }
    }
}
```

### 要素同士の間隔には `spacing` を使う

[こちらの記事](https://qiita.com/uhooi/items/d1d2f0f4916e053f5746) で説明している通り、一般的に __`padding` は「親要素と子要素の間隔」を表します__ 。
SwiftUIで __「要素同士の間隔」は `spacing` で表す__ ので、そうなるように修正します。
`VStack` の `spacing` を `0` にして、 `padding` で調整するのは望ましくありません。

`VStack` や `HStack` はグルーピングの役割も果たします。
`Image` と `Text` 間の `spacing` は `8` 、 `swift` と `gear` のグループ間の `spacing` は `32` とわかるので、 `VStack(spacing:)` を使って表現します。

```diff_swift:FooView.swift
import SwiftUI

struct FooView: View {
    var body: some View {
-       VStack(spacing: 0) {
+       VStack(spacing: 32) {
+           VStack(spacing: 8) {
                Image(systemName: "swift")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.red)
                    .padding(.horizontal, 24)

                Text("This is Swift. Swift is very beautiful programming language.")
-               .padding(.top, 8)
-               .padding(.bottom, 32)
                .padding(.horizontal, 24)

+           VStack(spacing: 8) {
                Image(systemName: "gear")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.gray)
                    .padding(.horizontal, 24)

                Text("This is gear.")
-               .padding(.top, 8)
                .padding(.horizontal, 24)
        }
    }
}
```

グルーピングと `spacing` により、要素間の間隔が読みやすくなりました。

### できる限り親Viewに巻き上げる

`padding` はできる限り親Viewに巻き上げて付けたほうが読みやすいです。
今回は行っていませんが、 `Image` + `Text` を部品化した際に、内部に `padding` があると外から値が確認しづらくなるためです。
また `padding` を内部に付けると部品を使い回しにくくなります。

今回は水平方向の `padding` がすべて `24` なので、まとめて一番外側の `VStack` に付けます。

```diff_swift:FooView.swift
import SwiftUI

struct FooView: View {
    var body: some View {
        VStack(spacing: 32) {
            VStack(spacing: 8) {
                Image(systemName: "swift")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.red)
-                   .padding(.horizontal, 24)

                Text("This is Swift. Swift is very beautiful programming language.")
-                   .padding(.horizontal, 24)
            }

            VStack(spacing: 8) {
                Image(systemName: "gear")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.gray)
-                   .padding(.horizontal, 24)

                Text("This is gear.")
-                   .padding(.horizontal, 24)
            }
        }
+       .padding(.horizontal, 24)
    }
}
```

## リファクタリング後のコード

リファクタリング後のコードです。
リファクタリング前より `padding` と `spacing` の使い分けができており、読みやすいと思います。

```swift
import SwiftUI

struct FooView: View {
    var body: some View {
        VStack(spacing: 32) {
            VStack(spacing: 8) {
                Image(systemName: "swift")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.red)

                Text("This is Swift. Swift is very beautiful programming language.")
            }

            VStack(spacing: 8) {
                Image(systemName: "gear")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 44, height: 44)
                    .foregroundColor(.gray)

                Text("This is gear.")
            }
        }
        .padding(.horizontal, 24)
    }
}
```

最終的に `padding` の指定が1つのみとなり、本当に必要なのは「 `FooView` 全体に水平方向へ `24` の `padding` を設ける」とわかりました。

`FooView` を使い回す場合は内部から `.padding(.horizontal, 24)` を外し、呼び出し時に `padding` を付けます。

```swift
FooView()
    .padding(.horizontal, 24)
```

## おまけ: `padding` は `frame` に影響する

`padding` を付けるとその分 `frame` が変わります。

`border` を付けるとわかりやすいです。

|Before|After|
|:--:|:--:|
|![スクリーンショット 2023-04-30 23.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/562aba2e-838e-bbc9-5568-2b4405c9bf55.png)|![スクリーンショット 2023-04-30 23.38.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/73914460-9979-a7ba-d48e-a31bf76517f8.png)|

要素間の間隔は `frame` に影響させたくないことが多いので、その意味でも今回のリファクタリングは有用です。
また `frame` が異なるのでリファクタリングと呼べるか怪しいですが、見た目は変わっていないため便宜上リファクタリングと呼んでいます。

逆に言えば、 `frame` を変えたい場合は `padding` を使う必要があります。

## おわりに

読みやすい `padding` を付けるコツの紹介でした。
誤っている箇所やほかにコツがあれば、コメントなどで教えていただけると嬉しいです :relaxed:

## 参考リンク

- https://twitter.com/the_uhooi/status/1646728657392775169
- [EdgeInsets | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/edgeinsets)