【SwiftUI】Viewの引数にViewを使用する

SwiftUI

SwiftUIでViewを書いていると、繰り返し使っている部分は再利用できる形で分割していきます。
その時に、Buttonのラベルの様にViewを引数とするViewを作りたい事もあると思います。
今回はそういった場合のViewの書き方を紹介します。

完成形

順を追って紹介していきますが、説明を読むのが面倒な人向けに完成形を先に見せます。

struct ViewParamView<Content:View,Label:View>: View {
    
    let content:Content
    let label:Label
    
    init(@ViewBuilder content: () -> Content ,@ViewBuilder label: () -> Label){
        self.content = content()
        self.label = label()
    }
    
    var body: some View {
        VStack{
            label
                .padding()
                .border(.black)
            content
        }
    }
}

struct ViewParamView_Previews: PreviewProvider {
    static var previews: some View {
        ViewParamView{
            Text("Content")
            Button("TestButton") {                
            }
        } label: {
            Text("Label")
        }
    }
}

この様なコードを使うと複数のViewを引数として使用する事ができます。

Viewを引数にする(特定View)

まずは特定のViewを引数にする方法からです。

これは単純に変数を持たせれば良いだけです。
今回はTextを使いました。

struct SingleViewParamView: View {
    
    let text:Text
    
    var body: some View {
        text
    }
}

struct SingleViewParamView_Previews: PreviewProvider {
    static var previews: some View {
        SingleViewParamView(text: Text("Param"))
    }
}

AnyViewを引数にする

SwiftUIにはAnyViewというものがあります。
AnyViewは何らかのViewを一つ持たせる事ができます。
つまり、とりあえずAnyViewでラップしてあげると引数としてView好きなViewを使えます。

struct AnyViewParamView: View {
    
    let anyView:AnyView
    
    var body: some View {
        anyView
    }
}

struct AnyViewParamView_Previews: PreviewProvider {
    static var previews: some View {
        AnyViewParamView(anyView: AnyView( Button("Test"){} ) )
    }
}

実の所、コードの可読性を無視すればこれでも結構好き放題できます。
AnyViewに持たせるViewは一つだけですが、それをGroupやVStackなどにすれば実質中身は複数Viewです。

import SwiftUI

struct AnyViewParamView: View {
    
    let anyView:AnyView
    
    var body: some View {
        VStack {
            anyView
        }
    }
}

struct AnyViewParamView_Previews: PreviewProvider {
    static var previews: some View {
        AnyViewParamView(anyView: AnyView( Group {
            Button("Test"){}
            Text("Test")
        } ) )
    }
}

この様に可能ではありますが、AnyView( Group { ~ })は読みにくいので極力やめましょう。

@ViewBuilderを使う

最初に紹介したパターンです。

@ViewBuilder属性はメソッドやクロージャで複数のViewを返すことを可能とします。
これを利用してViewを引数として使用します。

引数の型

まずは引数に使用する型についてです。

引数に使用するViewは使用時に自由に決めたいです。
特定のViewではありませんがViewプロトコルを実装した何かです。

こういった使うときに型が変わる場合にはジェネリクスを使います。
今回はViewが実装されているのが前提なのでその指定もしてあげましょう。

struct ViewParamView<Content:View>: View

クロージャを引数にする

@ViewBuilder属性はメソッドやクロージャで使えます。
その為、引数もクロージャにする必要があります。

イニシャライザを用意します。
引数のラベルに@ViewBuilder属性をつけ、returnは先程のジェネリクスの型を指定します。

struct ViewParamView<Content:View>: View {

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

Viewを保持する

引数で渡したViewはイニシャライザではなくbodyで使うので変数で保持する必要があります。
この時、特に問題がないのであればクロージャを実行して中身のViewのみを保持します。

struct ViewParamView<Content:View>: View {
    
    let content:Content
    
    init(@ViewBuilder content: () -> Content){
        self.content = content()
    }
}

クロージャのまま保持することも出来ますが、こちらの場合は@escaping属性が必要になります。
Viewのみ保持する場合はイニシャライザの実行時にクロージャが実行されますが、
クロージャのまま保持するとViewが表示される時に実行される為、いつになるか分かりません。
その為@escaping属性が必要になります。

struct ViewParamView<Content:View>: View {
    
    let content:() -> Content
    
    init(@ViewBuilder content: @escaping () -> Content){
        self.content = content
    }
}

Viewを変数で維持し、表示時にクロージャを実行することで結果が変わる場合などは後者の@escaping属性を使う事があるかもしれません。

しかし、おおよその場合はViewのbodyにそのまま書くと思うので前者のイニシャライザで実行するパターンで問題ないと思います。
実行時に中身が変わる様な場合も、@State属性や@Published属性を使用した変数でViewの表示を分岐させる事で済む場合が多いからです。

bodyで使用する

bodyで使用する場合はそのまま変数を書くだけです。
Viewなので特別何かが必要になることはありません。

struct ViewParamView<Content:View>: View {
    
    let content:Content
    
    init(@ViewBuilder content: () -> Content){
        self.content = content()
    }
    
    var body: some View {
        VStack{
            content
        }
    }
    
}

クロージャで保持した場合は実行しなければいけない為カッコがつきます。

Viewを使う

あとはVStackなどの既存のViewの様に使用できます。
クロージャなのでtrailing closureでラベル無しで使う事もできます。

struct ViewParamView_Previews: PreviewProvider {
    static var previews: some View {

        ViewParamView{
            Text("Content")
            Button("TestButton") {
            }
        } 

    }
}

完成形を確認する

最後に完成形をもう一度見てみましょう。

完成形は引数を2つにしています。同じ様にして引数の数を増やす事ができます。
Sectionなどの既存のViewでもよく見る形ですね。

当然ですが、Buttonの様にView以外の引数と組み合わせる事もできます。

struct ViewParamView<Content:View,Label:View>: View {
    
    let content:Content
    let label:Label
    
    init(@ViewBuilder content: () -> Content ,@ViewBuilder label: () -> Label){
        self.content = content()
        self.label = label()
    }
    
    var body: some View {
        VStack{
            label
                .padding()
                .border(.black)
            content
        }
    }
    
}

struct ViewParamView_Previews: PreviewProvider {
    static var previews: some View {
        ViewParamView{
            Text("Content")
            Button("TestButton") {
                
            }
        } label: {
            Text("Label")
        }
    }
}

最後に

Viewに引数にViewを使用する方法でした。
普段使っているViewで使用されている手法ですが、しっかり理解しようとすると、
ジェネリクス、@ViewBuilder、@escaping、trailing closureなど、
様々な要素を使用して構成されています。

初心者であれば理解するのが大変でコピペで済ませてしまうかもしれませんが、
より詳しくなる事が出来る良いきっかけだと思うので、
時間のある時にでもそれぞれについて良く知っておくと良いと思います。

コメント

タイトルとURLをコピーしました