SwiftUI .modifier() と View extension
カスタムモディファイアを作る方法には、.modifier() を使う方法と View の extension を使う方法がある。それぞれの特性を理解して、適切に使い分けよう。
.modifier() の基本
ViewModifier プロトコルを実装した型を、.modifier() で適用する。
struct BoldTitleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.title)
.fontWeight(.bold)
}
}
// 使用
Text("タイトル")
.modifier(BoldTitleModifier())View extension の基本
View の extension としてメソッドを定義し、内部でモディファイアを適用する。
extension View {
func boldTitle() -> some View {
self
.font(.title)
.fontWeight(.bold)
}
}
// 使用
Text("タイトル")
.boldTitle()こちらの方が呼び出しがシンプルで、標準のモディファイアと同じ見た目になる。
どちらを使うべきか
内部で状態(@State)や環境変数(@Environment)を使いたい。複雑なロジックを含む。他のモディファイアと concat で連結したい。
シンプルなモディファイアのチェーン。標準モディファイアと同じ API を提供したい。@ViewBuilder を使った条件分岐が必要。
ViewModifier で状態を持つ
ViewModifier 内では @State や @Environment を使える。
struct HoverEffectModifier: ViewModifier {
@State private var isHovered = false
func body(content: Content) -> some View {
content
.scaleEffect(isHovered ? 1.05 : 1.0)
.animation(.easeInOut(duration: 0.2), value: isHovered)
.onHover { hovering in
isHovered = hovering
}
}
}View extension では @State を直接使えないため、このようなケースでは ViewModifier が必要になる。
@ViewBuilder を使った extension
条件分岐を含む場合は @ViewBuilder を使う。
extension View {
@ViewBuilder
func visible(_ isVisible: Bool) -> some View {
if isVisible {
self
} else {
self.hidden()
}
}
}
Text("表示/非表示")
.visible(shouldShow)@ViewBuilder により、異なる型を返す分岐が可能になる。
ジェネリックを使った柔軟な extension
extension View {
func overlay<V: View>(
alignment: Alignment = .center,
@ViewBuilder content: () -> V
) -> some View {
self.overlay(content(), alignment: alignment)
}
}ジェネリクスと @ViewBuilder を組み合わせることで、柔軟な API を設計できる。
実践的なパターン:ラッパーとしての ViewModifier
ViewModifier を使って、ビューを別のビューでラップするパターン。
struct CardWrapperModifier: ViewModifier {
var padding: CGFloat = 16
func body(content: Content) -> some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding(padding)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}
extension View {
func cardWrapper(padding: CGFloat = 16) -> some View {
modifier(CardWrapperModifier(padding: padding))
}
}これにより、任意のコンテンツをカード風にラップできる。
VStack {
Text("タイトル").font(.headline)
Text("本文がここに入ります")
}
.cardWrapper()組み合わせのベストプラクティス
// ViewModifier で実装
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
content
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(isEnabled ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
}
}
// View extension で公開
extension View {
func primaryButtonStyle() -> some View {
modifier(PrimaryButtonModifier())
}
}ViewModifier で実装し、View extension で呼び出しやすい API を提供するという組み合わせが実用的だ。内部実装は ViewModifier でカプセル化しつつ、使う側は .primaryButtonStyle() のようにシンプルに呼び出せる。
どちらのアプローチも有効であり、要件に応じて使い分けることが重要である。状態や環境変数が必要なら ViewModifier、シンプルな変換なら extension と覚えておくとよい。











