【SwiftUI】@StateはStored Propertyではない
タイトルをどうつけるべきか悩んだ結果、これになりました。
要は複数の@Stateつきのプロパティを更新したとき、思った挙動にならなかったので調べた記録です。
やりたかったこと
やりたかったことはシンプルで、SwiftUIベースのアプリで、シェアシートを出そうとしていました。
SwiftUIだとモーダル遷移がすべて .sheet()
を使うことになるので、
@Stateつきの状態変数を2つつくりました。
.sheet()
で使うBool値と、シェアシートで共有するデータです。
サンプルコードとしてはこんな感じ。
import SwiftUI
struct FailureView: View {
@State var isShowSheet = false
@State var shareData: String?
var body: some View {
Button("Share") {
shareData = "Show this message"
isShowSheet = true
}
.sheet(isPresented: $isShowSheet) {
Text("Passed data: " + (shareData ?? ""))
}
}
}
ただこのコード、shareData
を受け渡すことができません。
nilになります。
@StateはStored Propertyではない
なぜでしょうか。
それは端的に言えば、StateはStored Propertyではないからです。
僕の@Stateの理解は、いわばこんな感じでした。
var isShowSheet: Bool {
get {
// SwiftUIの管理するメモリ領域から値を返却
return _isShowSheet
}
set {
// SwiftUIの管理するメモリ領域への保存
_isShowSheet = newValue
// コンポーネントの更新
updateViewIfNeeded()
}
}
@StateはSwiftUIが管理しているメモリ領域にあるStored Propertyに紐づいていて、
更新するとコンポーネントの再作成が走るように条件づけられている、という理解でした。
そしてコンポーネントが再作成されても、通常の変数は初期化されるけれど、@State をつけていると保持されているので、
更新があれば更新された値を使うし、更新がなければ前回の値を使う……と思っていました。
これが誤解で、正しくは
- 更新であれば更新値
- 更新がなければ初期値
でした。
最初のサンプルコードの @State var shareData: String?
は初期値がnilなので、
isShowSheet
の更新タイミングではnilが返っていたと思われます。
もうちょっと詳しく動作を書く
以上の説明でピンと来なかった方向けに、何が起こっていたのかを詳しく書いてみようと思います。
- ユーザーがボタンを押す
-
shareData = "Show this message"
で状態変数が更新される - ただこのとき、
isShowSheet
はfalseなので、モーダルが出ることはない - なので画面再描画はされない
- (変化がないので、画面再描画がスキップされる……はず)
-
isShowSheet = true
で状態変数が更新される - モーダルが出る
- ただ
shareData
はnil
いかがでしょうか?
対策
オススメの対策は、Viewにフラグとデータを持たせずに、Controller要素(PresenterなりViewModelなり)に持たせることです。
あるいはフラグだけはViewに残して、シェア対象のデータは外に預かってもらってもいいかもしれません。
とにかく@Stateの複合条件は避けましょう。
ただViewで完結させる方法はなんかないかなと思って調べてみたら、できないことはありませんでした。
.sheet()
の isPresented:
ではなくて、.sheet()
の item:
を使うと、
Bool値ではなく、オブジェクトの更新そのものを画面再描画のトリガーにできます。
import SwiftUI
struct SuccessView: View {
@State var shareItem: ShareItem?
var body: some View {
Button("Change(Success)") {
shareItem = ShareItem(data: "Show this message")
}
.sheet(item: $shareItem) { item in
Text("Passed data: " + item.data)
}
}
}
struct ShareItem: Identifiable {
var id = UUID()
var data: String
}
ただ指定するitemはIdentifiableである必要があります。
どちらの対策がいいかはお好みで。
Gistにあげました
一応動くサンプルコードをGistにあげましたので、自分の環境で試したい方はご活用ください。
Discussion