この記事で説明しているケースは、macOS Big Sur 11.3 Beta 2 で修正されました。iOS14.5RC でも修正されました。
Sponsor Link
環境&対象
- macOS Big Sur 11.2.2
- Xcode 12.4
- iOS 14.4
ObservableObject と @Published
ObservableObject に準拠したクラスのプロパティに @Published と付与します。
ビューの側では、@ObservedObject や @StateObject で扱うと、変更が入った時に、ビューが更新されます。
例えば、以下のコードでは、ボタンを押すと、画面が更新され、変更された値が表示されます。
class DirectConformClass:ObservableObject {
@Published var directProperty:Int = 0
}
struct ContentView: View {
@StateObject var object:DirectConformClass = DirectConformClass()
var body: some View {
VStack {
Spacer()
Text("DirectProperty \(object.directProperty)")
Button(action: {
object.directProperty += 1
}, label: {
Text("DirectProperty +1")
})
Spacer()
}
.padding()
}
}

ObservableObject, @Published, @StateObject の組み合わせで、値が変化した時に画面更新が行われます。
継承したクラスで プロパティに @Published を付与した時
モデルは、クラスなので、継承するときもあるかもしれません。しかし、継承したクラスで @Published を使用すると問題が発生します。
継承したクラスのプロパティに @Published を付与しても、値更新時に 画面更新がなされません。
class DirectConformClass:ObservableObject {
@Published var directProperty:Int = 0
}
class InheritedConformClass:DirectConformClass {
@Published var indirectProperty:Int = 0
}
struct ContentView: View {
@StateObject var object:InheritedConformClass = InheritedConformClass()
var body: some View {
VStack {
Spacer()
Text("DirectProperty \(object.directProperty)")
Button(action: {
object.directProperty += 1
}, label: {
Text("DirectProperty +1")
})
Spacer()
Text("IndirectProperty \(object.indirectProperty)")
Button(action: {
object.indirectProperty += 1
}, label: {
Text("IndirectProperty +1")
})
Spacer()
}
.padding()
}
}

InheritedConformClass も ObservableObject に 親クラスの DirectConformClass 経由で準拠しているはずですが、@Published を付与したプロパティの変更に対して画面更新がなされません。
ObservableObject は、子孫クラスに対して有効でない!?
設計意図は分かりませんが、ObservableObject に準拠したクラスを親クラスに持っていても、@Published は、有効とならないようです。
回避策
子クラスに改めて、ObservableObject 準拠を指定すると、親クラスですでに準拠しているというエラーとなります。
一番分かりやすい回避策は、該当プロパティを親クラスへ移動してしまうことです。
ただ、設計意図もあり、子クラスに持たせていると思いますので、移動が難しいことも多いはずです。
そのような時の回避策が以下です。
objectWillChange で個別に通知
親クラスは、ObservableObject に準拠しているため、子クラスからも objectWillChange を使うことができます。
ですので、子クラス側で以下のように定義することで、変更時の画面更新が行われます。
class InheritedConformClass:DirectConformClass {
@Published var indirectProperty:Int = 0 {
willSet {
objectWillChange.send()
}
}
}

@Published は意味ないです
コードを見てわかる通り、indirectProperty の値変更検知は、willSet で行われているので @Published を付与している意味は無いです。
以下のことを想定して、付与しておくと良い気がします。
- 将来的に、子クラスの @Published も 変更通知対象としてくれた時に向けた備忘録
- 意図として、変更監視対象であることの明示
まとめ:親クラスが準拠している ObservableObject は、子クラスの @Published を更新通知対象としない
- 親クラスが ObservableObject に準拠していても、子クラスでの @Published 付きのプロパティは、変更監視対象とならない
- 回避策1:該当プロパティを 親クラスへ移動する
- 回避策2:該当プロパティの willSet で objectWillChange.send() を使い、自ら変更通知する
子クラスの @Published が無視される理由をご存知でしたら、教えてください _o_ → Bug でした。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link