SwiftUIの @State, @Binding, @Published, $, _ などのなんとなく使ってる文法をちゃんと理解する
@Published
@PublishedはObservableObjectプロトコルに準拠したクラス内のプロパティを監視し、変化があった際にViewに対して通知を行う
@State に近い
class SignUpViewModel: ObservableObject {
@Published var firstName: String = ""
@Published var lastName: String = ""
}
struct LoginView: View {
@StateObject var viewModel = SignUpVeiwModel()
var body: some View {
VStack {
Text("フルネーム:\(viewModel.lastName) \(viewModel.firstName)") // 表示は $ 不要
TextField("姓", text: $viewModel.lastName) // ここで変更も行うので先頭に $
TextField("名", text: $viewModel.firstName) // ここで変更も行うので先頭に $
}
}
}
@StateObject と @ObservedObject の違い
上記で @ObservedObject var viewModel
と書くこともできるが、@ObservedObject
だとライフサイクルとして、例えば親ビューの何かの値の更新による再描画で、自身も再描画された際に、@ObservedObject
で定義した viewModel
も破棄されてしまうらしい(つまり上記の例では firstName
, lastName
が親ビューの何かの値の更新の度に空文字に戻ってしまうらしい。
@ObservedObject
というのは viewModel のようなものに対してでなく、データオブジェクト(データの入れ物の箱としてのオブジェクト?)に対して使うそうだが、いまいちユースケースがピンとこない。
@State と @Binding
データバインディング、すなわち異なるViewやViewModelで同じデータを扱う(バインドする)ための仕組みか。
struct ParentView: View {
@State var isCheck: Bool = false
var body: some View {
Image(systemName: isCheck ? "bell.fill" : "bell.slash")
ChildView(isCheck: $isCheck) // この時に $を付けて参照渡し
}
}
struct ChildView: View {
@Binding var isCheck: Bool // 親Viewのプロパティ参照を受け取る
var body: some View {
Button("画像切替") {
isCheck.toggle() // 参照共有しているプロパティを変更する
}
}
}
ただこの場合は親ビュー→子ビューという関係だけど、View と ViewModel という関係で @Binding
を使っていいのか疑問。
@State vs ただの var
@State
を付ける付けないでどうかわるのか、var
は変数の変更を受け付けるんだから @State
付けなくても動きそうだけど.. →動かないというかコンパイルエラーになる。
ポイント
- View というのは struct である
- struct では
var
で宣言してても変数を変更できない(なんでや..) -
@State
を付けると状態変数になるので、変更できるし、変更があると View が再描画される
まぁまさに React で言う useState()
だけど、ViewModel (Class) で @State
で宣言した変数はどういった挙動をするんだろう
そもそもの @~
のような修飾子?のことを PropertyWrappers というらしい。
名前のとおりプロパティをラップして wrappedValue
と projectedValue
という2つの新たなプロパティへのアクセスを用意し、それらの getter, setter でごにょごにょしたい時に使えるらしい。
wrappedValue
は(名前の通り?)通常は定義した値そのものを返すだけだが、何らかの処理を挟んで返すことも可能っぽい。
@State isPlaying: Bool
var body: some View {
Text(isPlaying ? "再生中" : "停止中")
PlayButtion(isPlaying: $isPlaying) // $isPlaying == isPlaying.projectedValue
}
じゃあこの書き方は何なのか?
struct EditProfileView: View {
@StateObject var viewModel: EditProfileViewModel
init(user: User) {
self._viewModel = StateObject(wrappedValue: EditProfileViewModel(user: user))
}
これは _viewModel
が、viewModel
宣言時に内部で暗黙的に宣言された StateObject<EditprofileViewModel>
型の値であるから、それに直接代入している、ということ。ここで _viewModel.wrappedValue
== viewModel
か?
下記を参考にすると少し理解が深まる気がする
次の1行の宣言文は
@State var like = true
Swiftコンパイラ内部で次のような宣言に変換されてコンパイルされます。
private var _like: State<Bool> = State(wrappedValue: true)
var like: Bool {
get {
return _like.wrappedValue
}
set {
_like.wrappedValue = newValue
}
}
var $like: Binding<Bool> {
get {
return _like.projectedValue
}
}
@State vs @StateObject
基本的に @State
は Bool, String, Int などのシンプルな変数の状態管理に使うが、 @StateObject
は ObservableObject の変数宣言時に使う。
じゃあ ObservableObject って何かって言うと、全てのプロパティが State のようなオブジェクト?、、、なのか、、?
ただ ObservableObject のプロパティは基本 @Published
で宣言するっぽく、これらのプロパティに変更があった時に View は再描画を行う。
じゃあ無理やりこうかくとどうなるのか?
struct EditProfileView: View {
@Environment(\.dismiss) var dismiss
@State var viewModel: EditProfileViewModel
init(user: User) {
self.viewModel = EditProfileViewModel(user: user)
}
@StateObject
で宣言すると get-only property やでと怒られるが、@State
で宣言すれば文法エラーにはならない。このとき user
に変更があったときどうなるのか?
- 親から渡されてきた user に変更があったとき
- viewModel 内で
@Publish
されているuser
を変更したとき
ViewModel やそれが保持するプロパティをどうやって App -> ParentView -> ChildView へと渡していくべきか?