🔖

.onChangeの代わりにBinding型のsetで任意の処理を実行する

2025/01/11に公開

概要

  • 例えばSwiftUIのTextFieldで文字列を扱う際に、入力毎にアクションを挟みたいことがあります
  • これにはonChange(of:initial:_:)が使えますが、Viewの外側に.onChangeを書くため実装のコードが別れてしまい少し見にくくなってしまう問題があります
  • そこでBinding型を返すextensionを定義して簡潔に書いてみます
  • onChangeのほうがパフォーマンスは良いそうなので、実際に利用する際はInstrumentsでパフォーマンスを確認するとよさそうです

https://www.hackingwithswift.com/quick-start/swiftui/how-to-run-some-code-when-state-changes-using-onchange
That being said, please be sure to run your code through Instruments if you do this – using onChange() on a view is more performant than adding it to a binding.

実装

static関数を定義する方法

public extension Binding {
    static func bind<T>(_ value: @autoclosure @escaping () -> T,
                        with action: @escaping (T) -> Void) -> Binding<T> {
        Binding<T>(get: value, set: action)
    }
}
  • 用途としては下記のように、変更された値をStore側で管理したい場合に良さそうです

https://github.com/hydro1337x/swiftui-clean-mvvm-c/blob/main/Presentation/Sources/Presentation/Scenes/Auth/LoginView.swift#L21-L25

メソッドチェーンを使う方法

public extension Binding {
    @MainActor
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

利用例

  • それぞれ下記のコードで動作確認できます
struct ContentView: View {
    
    @State private var textA: String = ""
    @State private var textB: String = ""
    @State private var textC: String = ""
    
    var body: some View {
        Form {
            TextField("Text A", text: .bind(textA, with: { textA=$0 ; print($0) }))
            TextField("Text B", text: $textB.onChange({ print($0) }))
            TextField("Text C", text: $textC)
                .onChange(of: textC) { _, newValue in
                    print(newValue)
                }
        }
    }
}

Discussion