[SwiftUI] iOS 16までのObservableObject, @ObservedObject, @StateObject。iOS 17からの@Observable
こんにちは。きんくまです。
今回はモデルの違いについてです。
iOS 16までのObservableObject, @ObservedObject, @StateObject
つくったもの
ソースコード
class Counter: ObservableObject {
@Published var count: Int = 0
@Published var message: String = ""
@Published var isOptionOn: Bool = true
}
struct CounterView: View {
@ObservedObject var counter = Counter()
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct CounterView2: View {
@StateObject private var counter = Counter()
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct ContentView: View {
@State private var isOnInContentView: Bool = true
var body: some View {
VStack {
Toggle(isOn: $isOnInContentView) {
Text("Content View Toggle")
}
Text("ObservedObject")
.padding(.top, 30)
CounterView()
.padding(10)
.border(Color.blue)
Text("StateObject")
.padding(.top, 30)
CounterView2()
.padding(10)
.border(Color.red)
}
.frame(width: 300)
}
}
説明
構造は以下です。
- ContentView
|- CounterView -> @ObservedObjectで設定
|- CounterView2 -> @StateObjectで設定
プログラムの書き方として、ObservableObjectで作ったモデルをView側で使うときは、上記のように@ObservedObjectや@StateObjectでセットすれば良いです。
それでここから本題で、動画をみていただくとわかりますが、挙動は以下です。
- ContentViewのToggleを変更
- ContentViewの再描画が走る
- CounterView(@ObservedObjectで設定)は、中身がリセットされるが、CounterView2(@StateObjectで設定)は中身がリセットされない
@ObservedObjectで設定したCounterViewが毎回リセットされるのは困ります。
なので、以下のように変更します。
CounterViewの中でCounter()を初期化するのではなく、親であるContentViewで初期化したものをプロパティとしてCounterViewに渡す
変更後のコード
class Counter: ObservableObject {
@Published var count: Int = 0
@Published var message: String = ""
@Published var isOptionOn: Bool = true
}
struct CounterView: View {
//@ObservedObject var counter = Counter()
// ここを変更
@ObservedObject var counter: Counter
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct CounterView2: View {
@StateObject private var counter = Counter()
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct ContentView: View {
@State private var isOnInContentView: Bool = true
@State private var counter = Counter() // ここを追加
var body: some View {
VStack {
Toggle(isOn: $isOnInContentView) {
Text("Content View Toggle")
}
Text("ObservedObject")
.padding(.top, 30)
CounterView(counter: counter) // ここを変更
.padding(10)
.border(Color.blue)
Text("StateObject")
.padding(.top, 30)
CounterView2()
.padding(10)
.border(Color.red)
}
.frame(width: 300)
}
}
変更後の動画
これで親のViewの再描画があっても子供のViewにまで影響が出ませんでした
まとめ
- ObservableObjectで作ったモデルは @ObservedObject, @StateObject などでViewにセットする
- @ObservedObjectは親からプロパティでもらう
- @StateObjectは自分自身で初期化して持つ
iOS 17からの@Observable
つづいてiOS 17からの@Observableです。
つくったもの
挙動はiOS 16の修正版と同じです
ソースコード
@Observable class Counter {
var count: Int = 0
var message: String = ""
var isOptionOn: Bool = true
}
struct CounterView: View {
// @ObservedObjectをつけない
var counter: Counter
var body: some View {
// $counterを使いたいので@Bindableで宣言
// そうでなければこの記述は不要
@Bindable var counter = counter
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct CounterView2: View {
// @StateObjectじゃない
@State private var counter = Counter()
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
Toggle(isOn: $counter.isOptionOn) {
Text("Option")
}
.padding(.bottom, 15)
Text("message: \(counter.message)")
Button {
counter.message = "Hello"
} label: {
Text("set message")
}
}
}
}
struct ContentView: View {
@State private var isOnInContentView: Bool = true
@State private var counter = Counter()
var body: some View {
VStack {
Toggle(isOn: $isOnInContentView) {
Text("Content View Toggle")
}
Text("ObservedObject")
.padding(.top, 30)
CounterView(counter: counter)
.padding(10)
.border(Color.blue)
Text("StateObject")
.padding(.top, 30)
CounterView2()
.padding(10)
.border(Color.red)
}
.frame(width: 300)
}
}
説明
少しずつ書き方が違っています。
(旧)iOS 16までのモデル
class Counter: ObservableObject {
@Published var count: Int = 0
@Published var message: String = ""
@Published var isOptionOn: Bool = true
}
(新)iOS 17からのObservationを使ったモデルの書き方
- class定義の前に@Observableがつく
- プロパティに@Publishedをつけない
@Observable class Counter {
var count: Int = 0
var message: String = ""
var isOptionOn: Bool = true
}
(旧)iOS 16までの@ObservedObject
struct CounterView: View {
@ObservedObject var counter: Counter
//省略
(新)iOS 17からのObservationを使った書き方
struct CounterView: View {
// @ObservedObjectをつけない
var counter: Counter
(旧)iOS 16までの@StateObject
struct CounterView2: View {
@StateObject private var counter = Counter()
//省略
iOS 17からのObservationを使った書き方
struct CounterView2: View {
// @StateObjectじゃない
@State private var counter = Counter()
違いはApple公式がすごくわかりやすいです。
Migrating from the Observable Object protocol to the Observable macro
あと、ハマリポイントとしては、CounterViewでStepperやToggleにプロパティを渡したいので以下のように@Bindableで宣言しなおすところでした。
struct CounterView: View {
var counter: Counter
var body: some View {
// $counterを使いたいので@Bindableで宣言
// そうでなければこの記述は不要
@Bindable var counter = counter
VStack(alignment: .leading, spacing: 10) {
Stepper("count \(counter.count)", value: $counter.count)
という感じです。ではでは。