Skip to main content
  1. Posts/

SwiftUIのViewModifierの使い方 - 共通する振る舞いをModifierに切り出す

SwiftUIのViewModifierはいまいちその使い道がわかりづらいですが、Viewに対して共通の振る舞いをつけたい時に便利だなと感じました。

例1: なんらかの共通処理を呼び出す #

例えば、アプリがフォアグラウンドになった時に、何か共通の処理をするとします。 これをする場合、最も簡単な方法は ScenePhase を使う方法です。 シンプルなアプリであれば以下の実装で十分かもしれませんが、Viewが大きくなってくると、これでは意図が読みづらくなることがあります。 なにより、ScenePhaseのプロパティ自体は、Viewの実装そのものとは、あまり関係がないかもしれません。

struct ContentView: View {
    @Environment(\.scenePhase) var phase
    var body: some View {
        VStack {
            Text("Hello world.")
        }
        .onChange(of: phase) { newValue in
            switch newValue {
            case .background, .inactive:
                break
            case .active:
                executeSomething()
            @unknown default:
                break
            }
        }
    }

    private func executeSomething() {}
}

ViewModifierに切り出す #

ViewModifierに切り出せば、意図が明確な実装になる可能性があります。 こうすることで、Viewのstructに記述される内容はViewのことだけになり、処理そのものの詳細は、Modifierの中に閉じ込めることができるようになります。

// なんらかの共通処理
struct ExecuteSomethingModifier: ViewModifier {
    @Environment(\.scenePhase) var phase

    func body(content: Content) -> some View {
        content
            .onChange(of: phase) { newValue in
                switch phase {
                case .background, .inactive:
                    break
                case .active:
                    executeSomething()
                @unknown default:
                    break
                }
            }
    }

    private func executeSomething() {
        // 処理の実装
    }
}

// ViewのExtensionにmodifierの呼び出しを実装する
extension View {
    func applyExecuteSomething() -> ModifiedContent<Self, ExecuteSomethingModifier> {
        modifier(ExecuteSomethingModifier())
    }
}

// 使う側は、Extensionに定義したメソッドを呼ぶだけ
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello world.")
        }
        .applyExecuteSomething()
    }
}

例2: 画面の呼び出しに使う #

例えば画面遷移やアラートの表示などは、実装が各画面に散らばってしまうことがあります。 共通する画面の呼び出しであれば、ViewModifierにまとめてしまうのも良いかもしれません。

以下の例は、アラートの表示をViewModifierにまとめた例です。

// アラートの表示処理をViewModifierにまとめた
struct LogoutAlertModifier: ViewModifier {
    @Binding var isPresented: Bool

    func body(content: Content) -> some View {
        content
            .alert("エラー", isPresented: $isPresented) {
                Button("OK", action: executeLogout)
            } message: {
                Text("ログアウトします")
            }
    }

    private func executeLogout() {}
}

// Viewのメソッドとして呼び出せるようにExtensionを用意
extension View {
    func forceLogoutAlert(
        isPresented: Binding<Bool>
    ) -> ModifiedContent<Self, LogoutAlertModifier> {
        modifier(
            LogoutAlertModifier(
                isPresented: isPresented
            )
        )
    }
}

// アラート表示を使用するViewは、Extensionで定義したメソッドを呼ぶだけ
struct ContentView: View {
    @State var showForceLogoutAlert = false

    var body: some View {
        VStack {
            Button("Push") {
                showForceLogoutAlert.toggle()
            }
        }
        .forceLogoutAlert(
            isPresented: $showAlert
        )
    }
}

この方法であれば、アラートの表示処理をViewModifierに閉じ込めることができ、またViewModifierの中に具体的な処理の呼び出しも閉じ込めることができます。 呼び出し側のViewの実装もスッキリさせることができるので、Viewの意図が読みやすくなります。

Notificationも使えばもっとスッキリすることがある #

さらに、NotificationCenterを使えば、Bindingも不要になり、Viewの実装をもっとスッキリさせることができます。

// Notification Nameの定義
extension Notification.Name {
    static var forceLogout = Notification.Name("forceLogout")
}

// アラートの表示処理をまとめたModifier
struct LogoutAlertModifier: ViewModifier {
    @State var isPresented: Bool = false    // Binding ではなく State になっている

    func body(content: Content) -> some View {
        content
            // .forceLogoutの通知をSubscribeする
            .onReceive(
                NotificationCenter.default.publisher(for: .forceLogout),
                perform: { _ in isPresented.toggle() })
            .alert("エラー", isPresented: $isPresented) {
                Button("OK", action: executeLogout)
            } message: {
                Text("ログアウトします")
            }
    }

    private func executeLogout() {}
}

// Viewのメソッドとして呼び出せるようにExtensionを用意
// 引数が不要になったので Computed Property になる
extension View {
    var forceLogoutAlert: ModifiedContent<Self, LogoutAlertModifier> {
        modifier(
            LogoutAlertModifier()
        )
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Button("Push") {
                // ここではボタンを押した時にNotificationをpostしているが
                // 実際にはビジネスロジック側で発生させるイメージ
                NotificationCenter.default.post(name: .forceLogout, object: nil)
            }
        }
        .forceLogoutAlert // ViewはforceLogoutAlertつけるだけ
    }
}

このように、View側はほとんど何もすることがなく、アラート表示に対応させることを補足的に書き加えておくだけで良くなりました。 この例ではアラートの表示処理でしたが、これを応用すれば、画面遷移などもまとめることができる期待があります。

Viewに関連する機能はまとめてViewModifierに切り出すとよさそう #

今回は、ViewModifierを使用して、共通する振る舞いを切り出すことをやってみました。 ViewModifierを使用することで、再利用性の高い方法で共通処理を実装することができそうです。 すべてのViewで参照しているServiceなどのインスタンスがあれば、EnvironmentObjectに出すことを考えがちです。 しかし、状況によっては、ViewModifierに切り出す方が都合が良いことがあるかもしれません。 Routerのような画面遷移を担うものとして実装することもできそうですし、Serviceとの接続、URLSessionへの接続などビジネスロジックとの接続にも使えそうです。