4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIの .sheet 内で @State の値を使う時の注意

Last updated at Posted at 2024-03-08

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 ? "表示中" : "表示してないよ")
        }
    }
}

.sheetisPresented: Binding<Bool> を引数として受け取り、@State var isPresented の値が true であれば、シートを表示します。

そのため、.sheet 内に実装された Text でも 「表示中」 という文字が表示されそうですが...

output.gif

実際には 「表示してないよ」 と表示されてしまいました。

ちなみに @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)
        }
    }
}

output.gif

やはり同じように、値が更新されません。

値が反映されるケース

以下のような実装の場合は最新値が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 ? "表示中" : "表示してないよ")
        }
    }
}

output.gif

.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 ? "表示中" : "表示してないよ")
        }
    }
}

output.gif

動くケース

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 ? "表示中" : "表示してないよ")
        }
    }
}

output.gif

動かないケースではボタンを押してもデバッグコンソールに何も流れてきませんが、動くケースでは 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 を更新することもできそうでした。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?