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