LabelStyleの自作方法(SwiftUI)

## はじめに

Appleのサンプルコードを参考に、LabelStyleを自作する方法を紹介します。

https://github.com/apple/sample-food-truck/blob/820dd9fd2165debc6fd6d29f18169ebb45787dde/App/Truck/Cards/CardNavigationHeaderLabelStyle.swift

## 環境

- OS：macOS Monterey 12.4
- Xcode：13.4.1 (13F100)
- Swift：5.6.1

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

以下のコードをリファクタリングしながら、LabelStyleの実装を紹介します。

```swift:MonsterCellView.swift
HStack(spacing: 32) {
    Image(iconName) // var iconName: String
        .resizable()
        .scaledToFit()
        .frame(width: 68, height: 68)
    Text(name) // var name: String
        .font(.title)
        .frame(maxWidth: .infinity, alignment: .leading)
}
.padding(16)
```

### LabelStyleを作成する

まずLabelStyleを実装します。

`LabelStyle` に準拠した構造体を作成し、 `makeBody(configuration:)` メソッドで既存コードの `HStack` をほぼそのまま返せばOKです。

変更点は以下です。

- `Image(iconName)` を `configuration.icon` 、 `Text(name)` を `configuration.title` へ置き換える
- `Image` に付いている `.resizable()` を外す
  - 理由はわからないが、付けるとビルドエラーになる

```diff_swift:MonsterCellLabelStyle.swift
+ import SwiftUI
+ 
+ struct MonsterCellLabelStyle: LabelStyle {
+     func makeBody(configuration: Configuration) -> some View {
+         HStack(spacing: 32) {
+             configuration.icon
+                 .scaledToFit()
+                 .frame(width: 68, height: 68)
+             configuration.title
+                 .font(.title)
+                 .frame(maxWidth: .infinity, alignment: .leading)
+         }
+     }
+ }
```

### LabelStyleのプレビューを作成する（任意）

必要に応じてLabelStyleのプレビューを作成します。

```diff_swift:MonsterCellLabelStyle.swift
import SwiftUI

struct MonsterCellLabelStyle: LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 32) {
            configuration.icon
                .scaledToFit()
                .frame(width: 68, height: 68)
            configuration.title
                .font(.title)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}
+ 
+ struct MonsterCellLabelStyle_Previews: PreviewProvider {
+     static var previews: some View {
+         VStack {
+             Label {
+                 Text("uhooi")
+             } icon: {
+                 Image("uhooi")
+                     .resizable()
+             }
+             Label {
+                 Text("とてつもなく長い名前のモンスター")
+             } icon: {
+                 Image("uhooi")
+                     .resizable()
+             }
+         }
+         .labelStyle(MonsterCellLabelStyle())
+     }
+ }
```

`Label` の最初のクロージャーに `Text` 、 次のクロージャーに `Image` を渡します。
`Image` には先ほど外した `.resizable()` を付けます。

`.labelStyle(MonsterCellLabelStyle())` でLabelStyleを適用します。

### 既存のViewをLabelに置き換える

LabelStyleを作成したら、既存のViewを `Label` に置き換えます。

```diff_swift:MonsterCellView.swift
- HStack(spacing: 32) {
-     Image(iconName)
-         .resizable()
-         .scaledToFit()
-         .frame(width: 68, height: 68)
-     Text(name)
-         .font(.title)
-         .frame(maxWidth: .infinity, alignment: .leading)
+ Label {
+     Text(name)
+ } icon: {
+     Image(iconName)
+         .resizable()
}
+ .labelStyle(MonsterCellLabelStyle())
.padding(16)
```

これでLabelStyleの実装は完成です。

### LabelStyleの指定をスマートにする（任意）

Appleのサンプルコードに面白いエクステンションがあったので、LabelStyleの指定をスマートにしたい場合に実装します。

```diff_swift:MonsterCellLabelStyle.swift
import SwiftUI

struct MonsterCellLabelStyle: LabelStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 32) {
            configuration.icon
                .scaledToFit()
                .frame(width: 68, height: 68)
            configuration.title
                .font(.title)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

+ extension LabelStyle where Self == MonsterCellLabelStyle {
+     static var monsterCell: Self { .init() }
+ }
+ 
struct MonsterCellLabelStyle_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            Label {
                Text("uhooi")
            } icon: {
                Image("uhooi")
                    .resizable()
            }
            Label {
                Text("とてつもなく長い名前のモンスター")
            } icon: {
                Image("uhooi")
                    .resizable()
            }
        }
        .labelStyle(MonsterCellLabelStyle())
    }
}
```

このエクステンションにより、ドット始まりでLabelStyleを指定できるようになります。

```diff_swift:MonsterCellView.swift
Label {
    Text(name)
} icon: {
    Image(iconName)
        .resizable()
}
- .labelStyle(MonsterCellLabelStyle())
+ .labelStyle(.monsterCell)
.padding(16)
```

Swiftっぽくなっていい感じです。

## おわりに

これでLabelStyleを自作できるようになりました。
Labelを使うことで画像と文字列に関連があることを示せ、LabelStyleを使うことで構造と装飾を分離できます。

LabelとLabelStyleを使い、Viewの可読性を高めていきましょう :relaxed: 

## 参考リンク

- [Refactor MonsterCellView · uhooi/UhooiPicBook-SwiftUI-Playground@35f7015](https://github.com/uhooi/UhooiPicBook-SwiftUI-Playground/commit/35f7015ed9e4f766200fc499036e723a33779381)
- [Refactor MonsterCellLabelStyle · uhooi/UhooiPicBook-SwiftUI-Playground@f57e83d](https://github.com/uhooi/UhooiPicBook-SwiftUI-Playground/commit/f57e83d879390e2c08088480096ae1a165bd6e97)
- [Refactor ButtonStyle by uhooi · Pull Request #139 · uhooi/Loki](https://github.com/uhooi/Loki/pull/139)
- https://twitter.com/_rockname/status/1624931190947061760
- [How to make a custom button style supports leading dot syntax in SwiftUI | Sarunw](https://sarunw.com/posts/how-to-make-custom-button-style-supports-leading-dot-syntax-in-swiftui/)