1.SwiftUI は Apple が新たにリリースした、未来志向・マルチプラットフォーム対応・宣言的プログラミングを実現するフレームワークです。
SwiftUI 最新バージョン 2.0 は iOS 14 が必要ですが、現在多くの環境では iOS 13 が使われているため、不完全な部分を SwiftUIX や各種ライブラリで補っており、バグも続出しています。
2.以下は私の @State @Published @ObservedObject に対する理解です。間違いがあればご指摘ください。
1. @State の説明
SwiftUI の View は構造体で実装されているため、プロパティを変更する構造体メソッドを作成する際には mutating キーワードを付ける必要があります。例:
mutating func doSomeWork()
しかし、Swift ではミュータブルな計算プロパティを作成できないため、mutating var body: some View と書くことはできません。
@State を使うと、構造体の制限を回避できます。通常、構造体のプロパティは変更できませんが、@State を付けることで SwiftUI がその値を別の場所に保存し、変更可能にします。
確かにこれはずるい感じがしますし、「なぜクラスを使わないのか?」と思うかもしれません。クラスなら自由に変更できます。しかし、信じてください。これには価値があります。SwiftUI は頻繁に構造体を破棄して再作成するため、構造体を小さくシンプルに保つことはパフォーマンス上重要です。
ヒント:SwiftUI でプログラムの状態を保存する方法はいくつかあり、すべて学ぶことになります。@State は単一のビュー内で使う単純なプロパティ専用に設計されています。そのため、Apple はこれらのプロパティに private アクセス制御を追加することを推奨しています。例:@State private var tapCount = 0。
2. @Published + @ObservedObject の説明
@Published は SwiftUI で最も有用なラッパーの一つで、自動的に監視されるオブジェクトプロパティを作成できます。SwiftUI はこのプロパティを自動的に監視し、変更があるとそのプロパティにバインドされた UI を自動的に更新します。
たとえば、私たちが定義するデータ構造の Model で、@Published は ObservableObject プロトコル内でのみ使用できます。 そして、そのオブジェクトを参照するには @ObservedObject を使います。@State を使ってもエラーにはなりませんが、更新はされません。
class BaseModel: ObservableObject{
@Published var name:String = ""
}
struct ContentView: View{
@ObservedObject var baseModel:BaseModel = BaseModel()
var body: some View{
Text("用户名\(baseModel.name)")
Button(action: {
baseModel.name = "Renew"
}, label: {
Text("更新视图")
})
}
}
3. 最も重要な部分(コードコメントが最も重要なので必ず最後まで読んでください)
上記の例では正常に表示・読み込みが行われますが、実際のプロジェクトでは無数のバグが発生します。なぜでしょうか?これらの 3 つの状態と View のバインディング関係を理解していないと、後々問題を残す可能性があります。
まずは一連の例を見てみましょう
//// MASK - 先定义两个Model 继承 ObservableObject
class WorkModel: ObservableObject {
@Published var name = "name"
@Published var count = 1
}
class UserModel: ObservableObject {
@Published var nickname = "nickname"
@Published var header = "http://www.baidu.com"
}
//// MASK - View显示层
struct ContentView: View {
@ObservedObject var workModel:WorkModel = WorkModel()
@ObservedObject var userModel:UserModel = UserModel()
var body: some View {
VStack{
Text("work.count \(workModel.count)")
Text("work.name \(workModel.name)")
Text("user.nickname \(userModel.nickname)")
Text("user.header \(userModel.header)")
Button(action: {
userModel.nickname = "Renew"
userModel.header = "http://..."
workModel.name = "work name"
workModel.count += 1
}, label: {
Text("更新数据")
})
}
}
}
上記のコードは、ボタンをクリックするとデータが更新されるはずです。では、ラッパークラスを使った場合はどうでしょうか?
class WrapperModel: ObservableObject{
@ObservedObject var workModel:WorkModel = WorkModel()
@ObservedObject var userModel:UserModel = UserModel()
}
struct ContentView: View {
@ObservedObject var wrapperModel:WrapperModel = WrapperModel()
var body: some View {
VStack{
Text("work.count \(wrapperModel.workModel.count)")
Text("work.name \(wrapperModel.workModel.name)")
Text("user.nickname \(wrapperModel.userModel.nickname)")
Text("work.header \(wrapperModel.userModel.header)")
Button(action: {
wrapperModel.userModel.nickname = "Renew"
wrapperModel.userModel.header = "http://..."
wrapperModel.workModel.name = "work name"
wrapperModel.workModel.count += 1
}, label: {
Text("更新数据")
})
}
}
}
この場合、ボタンをクリックしてもデータは更新されるでしょうか?答えは「No」です。なぜでしょうか?
SwiftUI のデータ更新はトリガーが前提です。
第一層にバインドされたオブジェクト wrapperModel のプロパティ(フィールド)が更新された場合にのみ、View 層がデータ更新を呼び出します。
しかし、第一層にバインドされたオブジェクトがさらに @ObservedObject や他の型のオブジェクトをバインドしている場合はどうでしょうか?
その場合、第一層のオブジェクトのプロパティ更新はトリガーされるのでしょうか?答えは「できません」。
didSet イベント内でキャプチャしようとしてもキャプチャできず、View は更新されません。他に解決策はあるでしょうか?
あります:
オブジェクト
wrapperModel.objectWillChange.send()メソッドを呼び出して View 層に「更新しました」と伝えます。 しかし、これで絶対でしょうか?いいえ、階層がさらに深い Model ではやはりバグが発生し、トリガーできません。
4. まとめと解決策
/// View と 状態のバインディング関係は、
/// 第一に ObservableObject を継承したクラスのプロパティ(フィールド)が更新されたときに View が更新されるという仕組みです。
/// そこで、ObservableObject に重要でないフィールドを追加し、更新を通知するメソッドを作成します。
class BaseobservableObject: ObservableObject {
///
/// 注意
/// 子クラス Model を受け取る際は @ObservedObject を使う必要があります。@Published は使えません。
/// なぜなら、SwiftUI の更新メカニズムは、現在のオブジェクトに @Published フィールドの更新があると View を更新するからです。
/// BaseModel 内で notifyUpdate を実装し、現在のオブジェクトの _lastUpdateTime フィールドを更新することで、自身の全フィールドを更新します。
@Published private var _lastUpdateTime: Date = Date()
///
/// 更新を通知
public func notifyUpdate() {
_lastUpdateTime = Date()
}
}
/// これで、ラッパークラス内のオブジェクトが更新されたときに、
/// ラッパークラスの notifyUpdate() メソッドを直接呼び出すことで、現在のオブジェクトのプロパティを更新し、View を更新できます。
/// 懸念点:notifyUpdate() を複数回呼ぶと View が 2 回更新されるのでは?
/// 答え:いいえ、1 つの関数スタック内で notifyUpdate() を複数回呼んでも、View は 1 回だけ更新されます。
/// 子クラスが BaseobservableObject を継承している場合、
/// そのオブジェクト内のプロパティに @ObservedObject や @Published を記述する必要はありません。
/// なぜなら、プロパティ更新後に notifyUpdate() を呼び出すことでオブジェクト全体が更新される効果があるため、省略できます。
5. その他の知識
/// MASK - 基本的な Model クラスを実装し、他の Model はそれを継承する
class BaseModel: ObservableObject {
@Published var isLoading = false
}
class SonModel: BaseModel {
@Published var name = "name"
@Published var count = 1
}
struct ContentView: View {
@ObservedObject var sonModel:SonModel = SonModel()
var body: some View {
VStack{
Text("name \(sonModel.name)")
Button(action: {
sonModel.name = "Renew"
}, label: {
Text("加载")
})
}
}
}
/// 問題:View 層で直接参照している場合、
/// 本来なら Text に "name Renew" と表示されるはずが、クリックしても反応しない。
/// 原因は?実は前述の問題と似ています。
/// SonModel は ObservableObject クラスを直接継承していません。
/// そのため、直接 ObservableObject(親オブジェクト)を継承したクラスのプロパティ(フィールド)が更新されなければ、View は更新されません。
/// 最も簡単な解決策は、直接 ObservableObject(親オブジェクト)内の任意のプロパティを更新することです。