SwiftUIでViewを引数として渡せるViewを作る(@ViewBuilder)

f:id:bamboohero:20210525030857p:plain 以前の記事でナビゲーションバーの色を変更可能にしたNavigationViewの実装方法を紹介しました。

bamboo-hero.com


上記記事では、カスタムModifierを実装し、以下のようにModifierを指定することでナビゲーションバーの色を変更できるようにしました。

NavigationView {
    Text("Hello")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarColor(.blue)  // ナビゲーションバーの色を青にする
}


しかし、ナビゲーションバーの色はブランドカラーとしてアプリ全体で統一することが多く、上記のままだとナビゲーションバーを使う画面それぞれで同じModifierを指定する必要があります。

そこで、共通で使用できるNavigationViewを作成し、それをアプリ全体で使い回すようにします。



共通で使用できるNavigationViewを実装する

ナビゲーションバーの色やタイトルの表示モードなどの設定を含んだ独自のNavigationViewを定義して、以下のように使うことができると良さそうです。

CommonNavigationView {
    Text("Hello")  // ナビゲーションバーの設定をするModifierが必要ない
}


このCommonNavigationViewは、以下のように実装します。

struct CommonNavigationView<Content: View>: View {
    let content: Content

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

    var body: some View {
        NavigationView {
            content
                .navigationBarTitleDisplayMode(.inline)
                .navigationBarColor(.black)
        }
    }
}


@ViewBuilderという見慣れないアトリビュートが出てきました。次項で@ViewBuilderについて説明します。


@ViewBuilderとは

@ViewBuilderはSwiftUIライブラリが提供するResult Builderです。

Result Builderはざっくりいうとクロージャに渡された複数の値を一つにまとめる機能を持っていて(ざっくりすぎ?)、例えばSwiftUIだと以下のようにVStackのクロージャに複数のビューを渡すことで一つのビューを構築しますが、これは@ViewBuilderが複数のビューをTupleViewという一つの型にまとめてくれているからなんですね。

VStack {
    Text("Hello")
    Text("World")
}


ViewBuilderは以下のメソッドを定義しています。上記の例だと戻り値の型がTupleView<(Text, Text)>になるということですね。

static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View


CommonNavigationViewの実装に戻ると、イニシャライザで@ViewBuilderを適用したクロージャ引数を定義しているので、以下のような実装が可能になるんですね。

CommonNavigationView {
    Text("Hello")
    Text("World")
}

参考

programmingwithswift.com