📱

【SwiftUI】sheet(item:onDismiss...)のonDismissではitemに指定した値を使えない

2021/01/04に公開

SwiftUI でモーダルを表示して編集する目的で sheet(item:onDismiss:content:) を使ったら盛大にハマって時間を溶かしたのでメモ。

sheet(item:onDismiss:content:)onDismiss の closure 内では item に指定した値は使えないので注意が必要という話です。

やったこと

以下の動画のような操作で、モーダルを閉じたときに save する、みたいな動作を実現します。
Image from Gyazo

実現するために以下のようなコードを書きました。

class Item: ObservableObject, Identifiable {
    var id = ""
    @Published var name = ""
}

struct ContentView: View {
    @ObservedObject var item = Item(id: "xxx", name: "Foo")
    @State var editingItem: Item?

    var body: some View {
        List {
            ForEach([item]) { item in
                Button(action: {
                    // 編集中の item を保持
                    self.editingItem = item
                }) {
                    Text(item.name)
                }
            }
            // 編集対象を指定してモーダルを起動
            .sheet(item: self.$editingItem, onDismiss: {
                // 戻ってきたら保存する
                self.editingItem.map { item in item.save() } // ①
            }) { item in
                EditView(item: item)
            }
        }
    }
}

セルをタップ時にそのセルのデータを @State で指定した変数(editingItem)に入れておき、sheet でそれを指定してモーダルを立ち上げています。
モーダルを閉じたら onDismiss が呼ばれるので、その中で保存します。

これで良さそうに思いました。

onDismiss の時点で item: に指定した変数は nil になっている

実際に動かしてみると保存処理(item.save())が実行されません。
デバッガを使ってみると冒頭のコードで ① の item: に指定している変数(editingItem)が nil になっていました。

公式のドキュメントを見てもそのようなことは書かれていません。

おそらく、引数 item の型は Binding<Item?> で、ここが nil ではない値になったらモーダルを立ち上げる、という挙動なので、閉じたら逆に nil にする、という仕様なのかなと思いました。(ということに気づくまで1日かかった)

以下のサイトでもモーダルを閉じたら item に指定した変数は nil になるよ!と言っています。
Using alert() and sheet() with optionals

どうすればいいの?

立ち上げた先のビューで処理します。

struct EditView: View {
    @ObservedObject var item: Item
    var body: some View {
        NavigationView {
          ...}
        .onDisappear { // 閉じたときの処理
            item.save()
        }
    }
}

編集した画面側で保存しているのでむしろこちらのほうが直感的ですね。

代案としては呼び出し側のビューで sheet に渡す値とは別に変数を保持すればいいですがあまりスマートではなさそうです。
イメージとしては以下のような感じです。

struct ContentView: View {
    @ObservedObject var item = Item(id: "xxx", name: "Foo")
    @State var editingItem: Item?
    @State var editingItemToBeSaved: Item?

    var body: some View {
        List {
            ForEach([item]) { item in
                Button(action: {
                    self.editingItem = item
                    // sheet の引数に渡すのとは別の変数で保持する
                    self.editingItemToBeSaved = item
                }) {
                    Text(item.name)
                }
            }
            .sheet(item: self.$editingItem, onDismiss: {
                // sheet の引数に渡すのとは別の変数に入っているインスタンスで保存する
                self.editingItemToBeSaved.map { item in item.save() }
            }) { item in
                EditView(item: item)
            }
        }
    }
}

まとめ

以上、sheet(item:onDismiss:content:)onDismiss の closure 内では item に指定した値は使えないという話でした。

Xcode で補完すると onDismiss が候補に現れるので、閉じたときの処理はここでやればいいや、と思ってしまったのが敗因です。

SwiftUI についてはまだ勘で書いているところがあるので、たくさん書いて理解しておきたいところです。

Discussion