見出し画像

ViewBuilderやViewModifierでSwiftUIのViewを分割する

SwiftUIは宣言的にUIを書けるが、気をつけないとbody内のViewが肥大化してしまう。そこでViewBuilderやViewModifierを使ってViewを分割してみたい。

ViewBuilder

複数画面で共通のナビゲーションバーやボタンを使う場合、毎回同じViewやmodifierを書くのは煩雑なので、共通のViewとして定義しておけば簡単に流用できる。そこでViewBuilderを使えば、HStackやVStackのようにsubviewsを構築するViewを定義できる。
ViewBuilderとは、クロージャーから複数のViewを構築するカスタムパラメータ属性で、複数のViewをTupleViewという型にまとめて返却する。

// ViewBuilderのメソッドの1つ
static func buildBlock<C0, C1>(
    _ c0: C0,
    _ c1: C1) -> TupleView<(C0, C1
)> where C0 : View, C1 : View

例えば、以下のようにインライン表示や常時表示、カラーテーマ設定でカスタマイズしたナビゲーションバーを複数の画面で設定したい場合、対象の全てのViewで再設定する必要があるが、ViewBuilderを使えば、共通化することができ、冗長な記述を避けられ可読性が上がり、設定漏れを防ぐこともできる。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("Hello!")
                .navigationBarTitleDisplayMode(.inline)
                .toolbarBackground(Color.pink, for: .navigationBar)
                .toolbarBackground(.visible, for: .navigationBar)
                .toolbarColorScheme(.dark, for: .navigationBar)
                .navigationTitle("Hello")
        }
    }
}
/// 共通のNavigationStack
struct CommonNavigationStack<Content: View>: View {
    let content: Content
    let toolBarColor = Color.pink
    
    // イニシャライザのパラメータに@ViewBuilderを付けることで、複数のViewから構築できるようになる
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        // 共通のNavigationStackに各種設定を適用
        NavigationStack {
            content
                .navigationBarTitleDisplayMode(.inline)
                .toolbarBackground(toolBarColor, for: .navigationBar)
                .toolbarBackground(.visible, for: .navigationBar)
                .toolbarColorScheme(.dark, for: .navigationBar)
        }
    }
}

struct FirstView: View {
    var body: some View {
        // 共通のNavigationStackの中にViewを記述すれば、上で定義した各種設定が反映される
        CommonNavigationStack {
            VStack {
                Text("First View")

                NavigationLink {
                    SecondView()
                } label: {
                    Text("Show Second")
                }
                .padding()
            }
            .navigationTitle("First")
        }
    }
}

struct SecondView: View {
    var body: some View {
        // ここでも共通のNavigationStackを使えば、簡単に共通のナビゲーションバーを適用できる
        CommonNavigationStack {
            Text("Second View")
                .navigationTitle("Second")
        }
    }
}

ViewModifier

先程のViewBuilderではViewを構築する部分を分割したが、同じような見た目の装飾をしたい場合に、modifierを複数箇所に記述してしまうのは冗長なので、カスタムViewModifierを定義して分割したい。
カスタムViewModifierを定義するには、ViewModifierプロトコルに準拠したstructを定義する。

// カスタムViewModifier
struct CustomModifier: ViewModifier {
    // ViewModifierは新しくViewを生成して返却する
    func body(content: Content) -> some View {
        // content: 元のView
        content.foregroundColor(Color.red)
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello!")
            // modifierメソッドに定義したカスタムViewModifierを渡す
            .modifier(CustomModifier())
    }
}

しかし、こちらの発表でもあるように、Modifierは新しいViewを返却するので、シンプルなModifierならViewModifierプロトコルに準拠せずにextensionで対応できる。

extension View {
    // Viewを返却するカスタムModifierメソッドを定義
    func customModifier() -> some View {
        foregroundColor(Color.red)
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello!")
            // extensionで定義したカスタムModifierを適用
            .customModifier()
    }
}

補足として、Modifierで@Stateなどで状態保持が必要な場合(状態に応じて表示を変更する場合)は、extensionでは対応できないのでカスタムViewModifierを利用する。ただし、公式ドキュメントでもあるように、カスタムViewModifierを利用する場合でも、extensionでラップするとよい。

struct CustomModifier: ViewModifier {
    func body(content: Content) -> some View {
        content.foregroundColor(Color.red)
    }
}

extension View {
    func customModifier() -> some View {
        modifier(CustomModifier())
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello!")
            .customModifier()
    }
}

参考


この記事が気に入ったらサポートをしてみませんか?