はじめに
例えば以下のように、任意でヘッダーを付けられるビューがあるとします。
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:
を省略して呼び出すとビルドエラーが発生します。
import SwiftUI
struct ContentView: View {
var body: some View {
FooView { // ❌: Generic parameter 'Header' could not be inferred
Text("Content")
}
}
}
Header
の型が確定していないためです。
おそらく関数ではオプショナルなクロージャに対して @ViewBuilder
を付けられないため、 header:
内のクロージャで条件によって異なるビューを返してもビルドエラーが発生します。
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を返すクロージャにする
コメント を頂き、Swift 5.7からはこの方法が使えることに気づきました。
そして本記事は、約2年前に私が投稿した記事と状況が異なるだけで本質的には同じでした。
EmptyView()
はレイアウトに影響を与えないため、 header
を非オプショナル型にして、デフォルト引数に EmptyView()
を返すクロージャを指定することでスマートに書けます。
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つの問題が解消できました。
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")
}
}
}
}
②headerを渡さないイニシャライザを追加する
Appleの apple/sample-food-truck で使われている手法です。
この方法を参考にすると以下のように書けます。
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
をオプショナルのままにしている点が異なります。
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
でも問題ありません。
// `AnyView` でも問題ない
- extension FooView where Header == EmptyView {
+ extension FooView where Header == AnyView {
しかし EmptyView
派が多いので、あえて型消去している AnyView
を指定する必要はないかもしれません。
おわりに
不要な場合に型パラメータを省略できるビューを実装できました。
私は「①>③>②」の順で好みですが、みなさんはどうでしょうか?
もっといい方法があればコメントなどで教えていただけると嬉しいです