SwiftUIの .sheet
内で @State
の値を利用する際、最新値が反映されないことがあるので注意が必要です。
値が反映されないケース
例えば下のようなコードを書いたとします。
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
Button("Button") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text(isPresented ? "表示中" : "表示してないよ")
}
}
}
.sheet
は isPresented: Binding<Bool>
を引数として受け取り、@State var isPresented
の値が true
であれば、シートを表示します。
そのため、.sheet
内に実装された Text
でも 「表示中」 という文字が表示されそうですが...
実際には 「表示してないよ」 と表示されてしまいました。
ちなみに @State
を下のように分割しても
struct ContentView: View {
@State private var isPresented: Bool = false
@State private var text: String = "初期値"
var body: some View {
Button("Button") {
text = "表示中"
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text(text)
}
}
}
やはり同じように、値が更新されません。
値が反映されるケース
以下のような実装の場合は最新値がsheetの中に反映されます。
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
VStack {
Button("Button") {
isPresented = true
}
Text(isPresented.description)
}
.sheet(isPresented: $isPresented) {
Text(isPresented ? "表示中" : "表示してないよ")
}
}
}
.sheet
の外で @State
を使っている箇所がある場合は値が更新されそうです。
_printChanges()
を見てみる
_printChanges()
はViewの更新をトリガーした値の変更を調べることのできる開発者機能です。これを先ほど紹介した実装に仕込んでみます。
動かないケース
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
let _ = Self._printChanges()
Button("Button") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text(isPresented ? "表示中" : "表示してないよ")
}
}
}
動くケース
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
let _ = Self._printChanges()
VStack {
Button("Button") {
isPresented = true
}
Text(isPresented.description)
}
.sheet(isPresented: $isPresented) {
Text(isPresented ? "表示中" : "表示してないよ")
}
}
}
動かないケースではボタンを押してもデバッグコンソールに何も流れてきませんが、動くケースでは ContentView: _isPresented changed.
というログが表示されます。
.sheet
内だけで利用されている @State
の変更に関してはViewの更新が必要無いとみなされてしまい、古い値が .sheet
内のViewで利用されしまっていそうでした。
解決策
いくつか解決策を見つけたので紹介します。
.sheet(item:content:)
を使う
struct ContentView: View {
private struct Item: Identifiable {
var id = UUID()
var text: String
}
@State private var item: Item? = nil
var body: some View {
Button("Button") {
item = Item(text: "表示中")
}
.sheet(item: $item) { item in
Text(item.text)
}
}
}
.sheet(isPresented: Binding<Bool>, content: () -> View)
の代わりに .sheet(item: Binding<Identifiable?>, content: (Identifiable) -> View)
を使うことで、更新された値をクロージャの引数として受け取ることができるようになります。
@State
の状態を onChange
で監視する
struct ContentView: View {
@State var isPresented: Bool = false
var body: some View {
Button("Button") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
Text(isPresented ? "表示中" : "表示してないよ")
}
.onChange(of: isPresented, initial: false) {}
}
}
onChange(of:initial:action:)
を付けることで、sheetの外で @State
を利用する箇所ができるので、 isPresented
が更新された際にViewの更新をトリガすることができます
.sheet
内のViewのコンストラクタで Binding<Bool>
を受け取るようにする
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
Button("Button") {
isPresented = true
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
}
private struct SheetView: View {
@Binding var isPresented: Bool
var body: some View {
Text(isPresented ? "表示中" : "表示してないよ")
}
}
}
Binding<Bool>
を受け取ることによって、isPresented
が更新された場合にサブビューである SheetView
を更新することもできそうでした。