LoginSignup
4
4

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

Last updated at Posted at 2024-02-16

はじめに

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

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: を省略して呼び出すとビルドエラーが発生します。

ContentView.swift
import SwiftUI

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

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

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

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つ紹介します。

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

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

コメント を頂き、Swift 5.7からはこの方法が使えることに気づきました。

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

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

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つの問題が解消できました。

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")
            }
        }
    }
}

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

Appleの apple/sample-food-truck で使われている手法です。

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

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 をオプショナルのままにしている点が異なります。

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 でも問題ありません。

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

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

おわりに

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

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

参考リンク

4
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4