StateObjectの初期化にはご注意を
@StateObject
のものを直接初期化するのは要注意ということを最近体験したのでそのメモ。
詳細
現在 SVVS アーキテクチャをベースに開発をしているのですが、画面を push 遷移してそこからまた呼び出し元に pop で戻るというような挙動をやったときに、閉じた方の画面の ViewState
が破棄されないという問題に遭遇したのです。
破棄されないからからなのか、同じ画面を再びひらくと前回閉じるときに行った状態の変更を ViewState
が保持したままで利用されるため、求めていない結果になるという。
最初は NavigationLink
で開いた場合そういう挙動にでもなるのかな?とか思い onAppear()
で都度もろもろをリセットする処理でも入れようかともしたのですが、あまりに面倒くさいですし、なにより「再度開いた」のか「別の画面から戻ってきた」のかっていう切り分けの問題もあって、絶対そういう仕様ではないなと言う結論に。
原因
ググると結構簡単に原因らしきものに言及してくれているものに巡り会えました。(結構あるあるなのかな?)
少し具体的にかくと、、
SVVS なので View と ViewState が対になっていて、 View.init()
の中で ViewState の初期化を行っていました。
とはいえ、 ViewState は @StateObject
で保持しているので、単純に初期化するのではなく init(wrappedValue:)
を使用していました。
struct ContentView: View {
@StateObject private var state: ContentViewState
init() {
_state = .init(wrappedValue: ContentViewState())
}
}
どうやらこれが原因とのこと。
詳しいことはわかっていないのですが、 View の init 内で StateObject.init(wrapperdValue:)
を呼び出すとメモリ管理がおかしくなるみたいです。
回避策
根本的な原因がはっきり理解できていないので、対策というよりかは回避策になるのですが、 StateObject.init(wrapperdValue:)
を利用しないということが肝要そうです。
なので View の方では init()
を用意せずに、View 呼び出し側で直接 ViewState の初期化をしてあげる方向に落ち着きました。
こちらで確認したところ画面を閉じるとしっかりと deinit
がよばれるのを確認でき、再び画面を開いたときにちゃんと ViewState が作りおなされるのを確認できました。
ただしっかりと原因を理解しているわけではないので正しく実装したら回避できるかもしれないという思いもやはり拭いきれないとこではあります。
参考
- SwiftUI x Combineのメモリリークを防ぐ3つのTips - スタディサプリ Product Team Blog
ここが色々検証されていてめちゃくちゃ助かりました。 自分の場合、NavigationLink
も併用していたので結構まずい組み合わせだった(笑)