具体的なコンテンツ型に制約されたイニシャライザーを追加して、SwiftUI のジェネリックコンポーネントにデフォルトの外観を与えます。カスタマイズ用には別のビルダーイニシャライザーを用意します。このガイドでは、macOS ウィンドウで使える再利用可能なパネルを作成し、Swift 5.7 のデフォルト引数推論によってインターフェースを簡潔にできる場合について説明します。

コンポーネントがサポートする2つの呼び出し方を定義する

利用者の要件はシンプルです。タイトルだけを渡しても完成した見た目になり、別の画面からは独自のコンテンツを渡せるデスクトップパネルを作ります。どちらの場合も、タイトル、余白、枠線は一貫させます。

想定するインターフェースは、デフォルト表示用の DesktopPanel(title: "Workspace") と、カスタムコンテンツ用の DesktopPanel(title: "Workspace") { Text("Three sample items") } です。これらの呼び出しは、同じジェネリックコンポーネントの異なる特殊化を生成します。

Swift のジェネリックパラメーターは具体的な型を表します。Content: View のような制約は適合を要求し、Content == DefaultPanelContent のような同一型要件は特定の型を選びます。Swift 言語リファレンスには、これらの要件と制約を使ったイニシャライザーのオーバーロード解決の両方が説明されています。ジェネリックパラメーターと引数

これにより、コンポーネントの役割は明確に分かれます。body がパネル共通の外観を担い、イニシャライザーが内部に配置するコンテンツを決めます。デフォルトコンテンツには名前付きのビュー型を用意し、便利なイニシャライザーから安定した読みやすい特殊化を参照できるようにします。

macOS のサンプル全体を作成する

このガイドでは、macOS 11 以降をデプロイメントターゲットとし、Swift 5.7 以降のコンパイラーを使う macOS SwiftUI アプリターゲットを使用します。以下で使う App と WindowGroup の構成には、この macOS 最小バージョンが必要です。Apple の説明では、WindowGroup はウィンドウのビュー階層を格納するシーンコンテナです。WindowGroup

生成されたアプリのエントリーポイントを以下のソースファイル全体で置き換え、生成された ContentView.swift が別の ContentView を宣言している場合は削除します。ターゲット内の @main アプリ宣言は1つだけにしてください。以下に表示する値はすべて、合成したローカルのサンプルです。

import SwiftUI

struct DefaultPanelContent: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text("Sample workspace")
                .font(.headline)
            Text("Choose a layout to begin.")
                .foregroundColor(.secondary)
        }
    }
}

struct DesktopPanel<Content: View>: View {
    private let title: String
    private let content: Content

    init(
        title: String,
        @ViewBuilder content: () -> Content
    ) {
        self.title = title
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(title)
                .font(.title2)

            Divider()

            content
        }
        .padding(16)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.08))
        .cornerRadius(10)
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.gray, lineWidth: 1)
        )
    }
}

extension DesktopPanel where Content == DefaultPanelContent {
    init(title: String) {
        self.init(title: title) {
            DefaultPanelContent()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            DesktopPanel(title: "Default panel")

            DesktopPanel(title: "Custom panel") {
                HStack {
                    Text("Sample items")
                    Spacer()
                    Text("3")
                        .font(.headline)
                }

                Text("Layout: Compact")
                    .foregroundColor(.secondary)
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 300)
    }
}

@main
struct PanelExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

macOS ターゲットをビルドして実行します。想定される結果は、同じ外側のスタイルを持つ2つのパネルです。1つ目にはデフォルトのワークスペーステキストが、2つ目にはカウント行と、その後にレイアウトの説明が表示されます。この環境には Swift コンパイラーも Apple SDK もないため、サンプルを確認しましたが、ここではコンパイルできませんでした。

Apple のカスタムビューのガイドに沿った構成です。View に準拠する型を宣言し、その body を実装して、組み込みビューとカスタムビューを階層に組み立てます。また、モディファイアは変更後のビューを返すと説明されています。そのため、共通スタイルはパネルのコンテンツを囲むように適用します。カスタムビューを宣言する

制約付きイニシャライザーによってジェネリック型が決まる理由

カスタムイニシャライザーは、Content を返すクロージャーを受け取ります。content() を呼び出すと、パネルに格納する値が生成されます。この実装では初期化時にクロージャーを実行し、保持しないため、@escaping は必要ありません。

ビルダーを指定しない呼び出しでは、Content が DefaultPanelContent の場合に限り、extension が init(title:) を使用できるようにします。この要件によって、不足していた型情報が補われます。その実装はビルダーイニシャライザーに処理を委譲するため、プロパティの初期化を1か所にまとめられます。

生成されるデフォルト値の型は DesktopPanel<DefaultPanelContent> です。カスタムパネルのコンテンツ型はクロージャーから推論されます。呼び出し側で、複雑になることもあるこの型を記述する必要はありません。

この仕組みの利点として、デフォルトのインスタンスはすでに特殊化されています。どちらの値もパネルを表示するからといって、異なるコンテンツ型を持つパネルを同じ変数に代入することはできません。インターフェースで複数の表示方法が必要なら、周囲のビューにビルダーを使った分岐を用意してください。

ビルダー属性によって、サンプルの複数文からなるカスタムクロージャーが使えます。HStack とその後の Text を生成されるコンテンツに含められますが、コンポーネント自体の Content: View 要件に取って代わるものではありません。Apple のドキュメントでは、複数の式と条件付きコンテンツに対するビルダーの処理が説明されています。ViewBuilder

デフォルト値付きビルダーイニシャライザー1つでオーバーロードを置き換えられるのはどんな場合か

Swift 5.7 では SE-0347 が実装され、省略された引数の具体的なデフォルト式からジェネリック型を推論できるようになりました。このコンポーネントでは、より簡潔な代替方法になります。SE-0347:デフォルト式からの型推論

Swift 5.7 以降のコンパイラーを使う場合は、DesktopPanel 内のビルダーイニシャライザーを以下の断片で置き換え、制約付きの extension をすべて削除します。完全なサンプルにある import、保存プロパティ、DefaultPanelContent、body、アプリのコードはそのまま残してください。

// Replacement initializer inside DesktopPanel.
init(
    title: String,
    @ViewBuilder content: () -> Content = {
        DefaultPanelContent()
    }
) {
    self.title = title
    self.content = content()
}

クロージャーを省略すると、Content は DefaultPanelContent() から推論されます。クロージャーを指定した場合は、その引数から推論されます。これはデフォルト式からの推論であり、コンポーネント宣言におけるデフォルト型引数ではありません。

この提案では、推論できる条件が制限されています。対象のジェネリックパラメーターは引数リスト内の1か所だけに現れる必要があり、関連する同一型制約も規則を満たさなければなりません。ここでは Content はビルダーパラメーターにしか現れません。別のイニシャライザーパラメーターでも再利用すると、この方法が使えなくなることがあります。SE-0347 の詳細設計

デフォルトコンテンツの作成にイニシャライザーの入力値が必要なら、オーバーロードを選びます。たとえば、title から導出するデフォルトのサブタイトルは、init(title:) 内で作成してビルダーイニシャライザーに渡せます。デフォルト引数式は、イニシャライザー本体内で作成したクロージャーであるかのように、渡された title をキャプチャすることはできません。

コンポーネントの要件 適したインターフェース
固定のデフォルトコンテンツと Swift 5.7 以降のコンパイラー デフォルト値付きのビルダーパラメーターが簡潔です。
他のイニシャライザー入力から作成するデフォルトコンテンツ 制約付きイニシャライザーを使い、その内部で値を作成します。
名前付きのデフォルト表示が複数ある 明確なラベルと制約を持つイニシャライザーのオーバーロードを使います。
デフォルト特殊化の型を明示的に保存する DesktopPanel<DefaultPanelContent> を記述するか、型エイリアスを定義します。

このインターフェースを struct DesktopPanel<Content: View = DefaultPanelContent> として表そうとしないでください。現在の Swift のジェネリックパラメーターの文法には、そのようなデフォルト型構文はありません。ここで使える方法は、制約付きイニシャライザーとデフォルト式からの推論です。ジェネリックパラメーターと引数

ビルダーのバージョンをまたいでインターフェースを維持する

Apple の現行ドキュメントでは、Xcode 26 以前と Xcode 27 以降が区別されています。新しいツールチェーンでは、ViewBuilder は型に依存しないコンテンツを構築します。Apple は、この統一された動作を表す名前として、ViewBuilder の型エイリアスである ContentBuilder を導入しています。この違いはアプリのデプロイメントターゲットではなく、ビルドに使うツールチェーンに基づきます。ViewBuilder、ContentBuilder

このサンプルでは、以前のツールチェーンとの互換性のため @ViewBuilder を使います。Xcode 27 以降で新しい表記を使う場合は、そのパラメーター属性を @ContentBuilder に変更します。Content: View はそのままにしてください。パネルには、ビュー階層に配置できるコンテンツが引き続き必要です。

デフォルトの特殊化を、ネストしたスタックとタプルの型で記述するのは避けてください。Apple の移行ノートには、複数式ビルダーで TupleView が TupleContent に変更され、生成された構造を名前に含める制約が破損する可能性があることが説明されています。このサンプルでは、代わりに extension を DefaultPanelContent に制約し、内部レイアウトを body: some View の背後に置いています。TN3211:State と ContentBuilder に関する SwiftUI ソース互換性の問題の解決

この名前付きラッパーがあれば、通常の編集も簡単になります。デフォルトコンテンツに3行目を追加しても、便利なイニシャライザーに長い具体型を書き直す必要はありません。

レイアウトを変更する前に、要件を確認する

コンポーネントがコンパイルできない場合は、まずインターフェースのどの部分で型情報が不足しているかを特定します。余白を変えたり、すべてを別のスタックで囲んだりしても、不足しているジェネリック特殊化は解決しません。

  1. ビルダーを省略した呼び出しで Content を推論できない。 制約付きの init(title:) が存在すること、またはデフォルト値付きイニシャライザーが Swift 5.7 以降でコンパイルされていることを確認します。
  2. 具体的なデフォルト値を Content に代入できない。 代入が制約付き extension 内で行われていることを確認します。View に準拠しているだけでは、DefaultPanelContent があらゆる Content と置き換え可能になるわけではありません。
  3. 複数のカスタム式が拒否される。 クロージャーパラメーターにビルダー属性が付いていることを確認します。
  4. 明示した型がカスタムコンテンツを拒否する。 DesktopPanel<DefaultPanelContent> という注釈は、コンテンツ型を意図的に固定します。呼び出しから別の特殊化を推論する必要がある場合は、その注釈を削除します。
  5. ツールチェーンのアップグレード後、タプルベースの制約が機能しなくなった。 生成されたビルダー構造への依存を名前付きのデフォルトビューに置き換えるか、該当する制約について Apple が説明する移行手順に従います。

最後に、ビルダーを軽量に保ちます。この実装では、パネルを初期化するたびに実行されます。1回だけ実行される保証はありません。Apple は SwiftUI がビューを再初期化する場合があると説明し、イニシャライザーで大きな処理を避けるよう勧めています。カスタムビューを宣言する

明示的な出発点として、制約付きイニシャライザーを使います。デフォルトの特殊化が分かりやすくなり、カスタムコンテンツの型も簡単に推論できます。コンパイラーの要件と推論の制約が API に合う場合は、デフォルト値付きイニシャライザー1つに切り替えます。どちらの方法でも、呼び出し側は必要なコンテンツだけを渡し、macOS パネルが共通の外観を担えます。

出典