初めに
SwiftUI で開発していると
@State@Binding@Published
のようなバインディング(アノテーション)を使用することは増えてくるでしょう。
その際に上手く動作しないことがあり、何度か調べたりすることがあったので、残しておきます。
実装
以下のパターンがあったので、順々に説明していきます。
- Case1: View と状態管理クラスを分離する場合
- Case2: 状態管理クラスが階層構造の場合
また、今回のまとめでは Swift Playgrounds でテストしたものを使用しています。
Case1: View と状態管理クラスを分離する場合
【前提】
状態管理を含む SwiftUI のコード例は、だいたい View 内に状態管理のプロパティを保持していることが多いです。
当たり前ですが、更新されたテキストが TextField に反映されていることがわかります。
ただ、実際にアプリを作る場合、状態管理の部分は View とは分離するでしょう。

分離先の StateObject クラスで @State がうまく動作しません。
【解決策】
@State はその画面で保持して使用しないと動作しません。
なので今回の場合は @State → @Published にすることで動作します。

無事に動作することが確認できます。
Code
final class StateObject: ObservableObject {
@Published var bindingText: String = ""
}
struct ContentView: View {
@ObservedObject var stateObject: StateObject
var body: some View {
TextField("text", text: $stateObject.bindingText)
.onAppear {
stateObject.bindingText = "update text"
}
}
}
Case2: 状態管理クラスが階層構造の場合
【前提】
状態を分離したコードで、さらにクラスなどを保持することはあるかと思います。
また、そのクラスから値をバインドすること(階層構造の場合)もあるでしょう。

階層構造先のクラスを ObservableObject に対応させ、@ObservedObject で保持していますが、うまく動作しません。
【解決策】
ObservableObject の objectWillChange を自前で更新することで解決します。
(詳しくは以下を読むと良いでしょう)
書き方は2パターンあるので、それぞれ載せておきます。
- パターン① ObservableObject を使用する
前提にあるコードと同じく、階層構造先の Binding クラスを ObservableObject に準拠させている場合です。
階層構造先の objectWillChange をハンドリングして、呼び出し元の objectWillChange にハンドリングすることで、反映することができます。
Code
final class Binding: ObservableObject {
@Published var bindingText: String = ""
}
final class StateObject: ObservableObject {
@ObservedObject var binding: Binding = .init()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = binding
.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}
struct ContentView: View {
@ObservedObject var stateObject: StateObject
var body: some View {
TextField("text", text: $stateObject.binding.bindingText)
.onAppear {
stateObject.binding.bindingText = "update text"
}
}
}
- パターン② @Published を使用する
@Published で値をバインドさせます。
階層構造先から $~~~ で値をハンドリングして、呼び出し元の objectWillChange にハンドリングすることで、反映することができます。
Code
final class Binding {
@Published var bindingText: String = ""
}
final class StateObject: ObservableObject {
@Published var binding: Binding = .init()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = binding
.$bindingText
.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}
struct ContentView: View {
@ObservedObject var stateObject: StateObject
var body: some View {
TextField("text", text: $stateObject.binding.bindingText)
.onAppear {
stateObject.binding.bindingText = "update text"
}
}
}
その他
開発の都度、つなぎ目のバインド(objectWillChange)を書くのは大変です。
こちらのブログにあるように、プロトコルを作り継承させ、隠蔽化する方法もあります。
以下、該当箇所を抜粋。
extension ViewModelObject where Binding.ObjectWillChangePublisher == ObservableObjectPublisher, Output.ObjectWillChangePublisher == ObservableObjectPublisher {
var objectWillChange: AnyPublisher<Void, Never> {
return Publishers.Merge(binding.objectWillChange, output.objectWillChange).eraseToAnyPublisher()
}
}
終わりに
階層構造の場合に上手く動作しなかったのは、最初は原因がわからずかなり手こずりました。
少なからずアーキテクチャーを導入すると、クラスを分離することになるので、今回のようなパターンは出てくるかと思います。
ご指摘などあればお願いいたしますmm


