オプショナルな引数の型パラメータを省略する方法(SwiftUI)

## はじめに

例えば以下のように、任意でヘッダーを付けられるビューがあるとします。

```swift:FooView.swift
import SwiftUI

// FIXME: 修正する必要がある
struct FooView<Content: View, Header: View>: View {
    @ViewBuilder private let content: () -> Content
    @ViewBuilder private let header: (() -> Header)?

    var body: some View {
        VStack {
            header?()
            content()
        }
    }

    init(
        @ViewBuilder content: @escaping () -> Content,
        header: (() -> Header)? = nil
    ) {
        self.content = content
        self.header = header
    }
}
```

試してみるとわかりますが、上記のコードで `header:` を省略して呼び出すとビルドエラーが発生します。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        FooView { // ❌: Generic parameter 'Header' could not be inferred
            Text("Content")
 }
    }
}
```

`Header` の型が確定していないためです。

おそらく関数ではオプショナルなクロージャに対して `@ViewBuilder` を付けられないため、 `header:` 内のクロージャで条件によって異なるビューを返してもビルドエラーが発生します。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        FooView { // ❌: Type '()' cannot conform to 'View'
            Text("Content")
        } header: {
            if true {
                Text("Header")
            } else {
                Button("False") {}
            }
        }
    }
}
```

本記事ではこれら2つの解決法を紹介します。

## 環境

- OS: macOS Sonoma 14.2.1 (23C71)
- Swift: 5.9.2

## 解決法

解決法を3つ紹介します。

どの解決法も「 `Header` を `EmptyView` に準拠させる」点は同じです。

### ①headerのデフォルト引数をEmptyViewを返すクロージャにする

[コメント](https://qiita.com/uhooi/items/bab6549451bcf9c75ac5#comment-c74a745aa8b3d601f985) を頂き、Swift 5.7からはこの方法が使えることに気づきました。

そして本記事は、約2年前に私が投稿した記事と状況が異なるだけで本質的には同じでした。

https://qiita.com/uhooi/items/336935761a631f5ea9be

`EmptyView()` はレイアウトに影響を与えないため、 `header` を非オプショナル型にして、デフォルト引数に `EmptyView()` を返すクロージャを指定することでスマートに書けます。

https://qiita.com/uhooi/items/bed9cf340c7dec81b2da

```diff_swift:FooView.swift
import SwiftUI

- // FIXME: 修正する必要がある
struct FooView<Content: View, Header: View>: View {
    @ViewBuilder private let content: () -> Content
-     @ViewBuilder private let header: (() -> Header)?
+     @ViewBuilder private let header: () -> Header

    var body: some View {
        VStack {
-             header?()
+             header()
            content()
        }
    }

    init(
        @ViewBuilder content: @escaping () -> Content,
-         header: (() -> Header)? = nil
+         @ViewBuilder header: @escaping () -> Header = { EmptyVIew() }
    ) {
        self.content = content
        self.header = header
 }
}
```

これで上記2つの問題が解消できました。

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            FooView {
                if true {
                    Text("Content")
                } else {
                    Button("False") {}
                }
            } header: {
                if true {
                    Text("Header")
                } else {
                    Button("False") {}
                }
            }

            Divider()

            FooView {
                Text("Content")
            }
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138245/c39e8af4-5e64-2461-43b0-657d2cdc5cf7.png" width=50%>

### ②headerを渡さないイニシャライザを追加する

Appleの [apple/sample-food-truck](https://github.com/apple/sample-food-truck) で使われている手法です。

https://github.com/apple/sample-food-truck/blob/3954a769e99f3cc53297d94f2b960ceb2665b3d6/FoodTruckKit/Sources/Donut/DonutBoxView.swift

この方法を参考にすると以下のように書けます。

```diff_swift:FooView.swift
import SwiftUI

struct FooView<Content: View, Header: View>: View {
    @ViewBuilder private let content: () -> Content
    @ViewBuilder private let header: () -> Header

    var body: some View {
        VStack {
            header()
            content()
        }
    }

    init(
        @ViewBuilder content: @escaping () -> Content,
-         @ViewBuilder header: @escaping () -> Header = { EmptyVIew() }
+         @ViewBuilder header: @escaping () -> Header
    ) {
        self.content = content
        self.header = header
    }
}
+ 
+ extension FooView where Header == EmptyView {
+    init(@ViewBuilder content: @escaping () -> Content) {
+         self.init(content: content) {
+             EmptyView()
+         }
+     }
+ }
```

やっていることは①と同様です。
しかしイニシャライザを複数用意する必要があり、冗長だと思います。

### ③headerをオプショナルのままにしてイニシャライザを追加する

②とほぼ同様ですが、 `header` をオプショナルのままにしている点が異なります。

```diff_swift:FooView.swift
import SwiftUI

struct FooView<Content: View, Header: View>: View {
    @ViewBuilder private let content: () -> Content
-     @ViewBuilder private let header: () -> Header
+     @ViewBuilder private let header: (() -> Header)?

    var body: some View {
        VStack {
-             header()
+             header?()
            content()
        }
    }

    init(
        @ViewBuilder content: @escaping () -> Content,
        @ViewBuilder header: @escaping () -> Header
    ) {
        self.content = content
        self.header = header
 }
}

extension FooView where Header == EmptyView {
    init(@ViewBuilder content: @escaping () -> Content) {
-         self.init(content: content) {
-             EmptyView()
-         }
+         self.content = content
+         self.header = nil
    }
}
```

「ヘッダーが任意」ということがわかりやすいので、個人的には②より好みです。

またこの方法だと `Header` の型は制約（ `<Header: View>` ）に準拠していれば何でもいいはずです。
`AnyView` でも問題ありません。

```diff_swift:ContentView.swift
// `AnyView` でも問題ない
- extension FooView where Header == EmptyView {
+ extension FooView where Header == AnyView {
```

しかし `EmptyView` 派が多いので、あえて型消去している `AnyView` を指定する必要はないかもしれません。

https://x.com/treastrain/status/1758394352479744099

## おわりに

不要な場合に型パラメータを省略できるビューを実装できました。
私は「①>③>②」の順で好みですが、みなさんはどうでしょうか？

もっといい方法があればコメントなどで教えていただけると嬉しいです :relaxed:

## 参考リンク

- [Can I assign a default type to generic type T in Swift? - Stack Overflow](https://stackoverflow.com/questions/42015081/can-i-assign-a-default-type-to-generic-type-t-in-swift) 
- https://x.com/zunda_pixel/status/1758380071059140626