(2022/02/06 更新)
NavigationViewでの編集モード(editMode)取得に関する問題についてまとめました。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.5.2
【iOS】15.2
【macOS】Monterey バージョン 12.2
ケース1(正常動作)
Editボタンの操作でViewの編集モードを切り替えるサンプルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
struct ContentView: View { /// Viewの編集モードを取得 @Environment(\.editMode) var envEditMode var body: some View { VStack { EditButton() /// 編集モードによって表示を変える if envEditMode?.wrappedValue.isEditing ?? false { Text("編集モード") } else { Text("表示モード") } /// 編集モードの時に削除が有効になる List { ForEach(["かぴばら"], id: \.self) { item in Text(item) } .onDelete(perform: { _ in }) } } } } |
Text、List共に編集モードの切り替えによって表示が変わります。
これは想定通りの動きです。
ケース2(うまく動かない)
先程と同じ機能をNavigationViewを使って再現します。
Editボタンはナビゲーションバーに配置しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct ContentView: View { /// Viewの編集モードを取得 @Environment(\.editMode) var envEditMode var body: some View { NavigationView { VStack { /// 編集モードによって表示を変える if envEditMode?.wrappedValue.isEditing ?? false { Text("編集モード") } else { Text("表示モード") } /// 編集モードの時に削除が有効になる List { ForEach(["かぴばら"], id: \.self) { item in Text(item) } .onDelete(perform: { _ in }) } } .toolbar { EditButton() } } } } |
こちらは、Editボタンを押すと、Listは表示が切り替わりますが、なぜかTextの方は変化しません。
考察
どうやら環境変数の編集モード "@Environment(\.editMode)" はアプリ全体で共有されているわけではなく、画面単位で別々に保持されているようです。
NavigationViewは囲んだViewを別の画面として切り替えるしくみですので、NavigationViewで囲まれたViewとContentViewは内部的には別の画面になり、編集モードも各々個別に保持されます。
EditボタンとListは、NavigationView内の子画面が保持する同一の編集モードを参照するので、連動して動作しますが、Textは親画面であるContentViewの編集モード(envEditMode)を参照している為、連動しないのが理由と考えられます。
対処方法
親画面と子画面で環境変数が共有されていないので、共有可能な環境変数を代わりに提供します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
struct ContentView: View { /// ①独自の editMode 状態変数を定義する @State var editMode: EditMode = .inactive var body: some View { NavigationView { VStack { /// ②編集モードによって表示を変える if editMode.isEditing { Text("編集モード") } else { Text("表示モード") } /// 編集モードの時に削除が有効になる List { ForEach(["かぴばら"], id: \.self) { item in Text(item) } .onDelete(perform: { _ in }) } } .toolbar { EditButton() } /// ③定義した独自の editMode 変数を子画面の環境変数に設定する .environment(\.editMode, $editMode) } } } |
ポイントは以下の3点です。
①独自の editMode 状態変数を @State 変数として定義します。
②Textは上で定義した編集モードを参照して表示を変えるようにします。
③定義した @State 変数を、environmentモディファイアを使って子画面の環境変数(.editMode)に設定します。
これで期待通り表示が切り替わるようになります。
疑問点(未解決事項)
「共有可能な環境変数を代わりに用意するのではなく、親画面の編集モードをそのまま子画面に設定すれば良いのでは?」と考え、次のようなコードを書いてみましたが、うまくいきませんでした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct ContentView: View { @Environment(\.editMode) var envEditMode var body: some View { NavigationView { VStack { if envEditMode?.wrappedValue.isEditing ?? false { Text("編集モード") } else { Text("表示モード") } List { ForEach(["かぴばら"], id: \.self) { item in Text(item) } .onDelete(perform: { _ in }) } } .toolbar { EditButton() } /// ★親画面の編集モードを子画面の環境変数に設定する .environment(\.editMode, envEditMode) } } } |
しかし、次のようにenviromentモディファイアの記述場所を変更してやると、意図した動きになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct ContentView: View { @Environment(\.editMode) var envEditMode var body: some View { NavigationView { VStack { if envEditMode?.wrappedValue.isEditing ?? false { Text("編集モード") } else { Text("表示モード") } List { ForEach(["かぴばら"], id: \.self) { item in Text(item) } .onDelete(perform: { _ in }) } } .toolbar { EditButton() /// ★親画面の編集モードを子画面の環境変数に設定する .environment(\.editMode, envEditMode) } } } } |
なぜ、このような動きの違いがおきるのか説明つかない為、実践では使いにくいなと思っています。
理由がわかる方、情報いただけるとうれしいです。
ケース3(うまく動かない)
次のケースは、ちょっと不可解な動きで、編集モードへのアクセスが、全く関係なさそうな部分に影響を及ぼす例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: NextView()) { Text("NextView") } .toolbar { ToolbarItemGroup(placement: .bottomBar) { MyEditButton() } } } } } struct NextView: View { @State private var showingAlert = false var body: some View { Button("ALert") { showingAlert = true } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button("Tap me!") { print("Tapped!") } } } .alert("アラート表示", isPresented: $showingAlert) {} } } struct MyEditButton: View { @Environment(\.editMode) private var editMode var body: some View { if editMode?.wrappedValue.isEditing ?? false { Button("Done") { editMode?.wrappedValue = .inactive } } else { Button("Edit") { editMode?.wrappedValue = .active } } } } |
NavigationLinkで遷移した画面で、一度でもアラートを表示すると、ボトムバー上のボタンが使えなくなります。
①「NextView」で画面遷移
②「Tap me!」をタップすると、デバグ画面に"Tapped!"が表示される
③「Alert」をタップして、アラートを表示させてから閉じる。
④以降は「Tap me!」をタップしても反応しない。
対処方法
ケース2と同様にNavigationView内側のViewに対して独自の状態変数(editMode)を定義すると、正常に動作します。
正直、全く関連性がわかりませんが、NavigationViewの内側でeditModeを使う時は必ず宣言しておくくらいでも良いのかもしれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct ContentView: View { @State var editMode: EditMode = .inactive var body: some View { NavigationView { NavigationLink(destination: NextView()) { Text("NextView") } .toolbar { ToolbarItemGroup(placement: .bottomBar) { MyEditButton() } } .environment(\.editMode, $editMode) } } } |
参考リンク
本エントリーを執筆するにあたり、参考にさせてもらった外部サイトです。