1
1

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のmoveDisabledとeditModeを併用したときにList右端の三本線(リストの並べ替えハンドル)が消える現象を対策した

Last updated at Posted at 2024-04-26

概要

タイトルの内容に関して一時的にとはいえ解決策を考えたのでメモとして残すついでに共有します。
困っている人の助けになれば幸いです。

環境

MacBook M1
MacOS Sonoma 14.4.1
Xcode 15.3

詳細

実装したい画面

実装しようとする画面の要件は以下の通りでした。

  • editMode.inactiveの状態でリストの項目をドラッグ&ドロップで動かせない
  • editMode.activeの状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる

具体的なイメージを先に共有しておくと、以下のような画面を実装することになっていました。

まず、editMode.activeの状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
という要件を満たすには以下のようなコードを書くとその通りの動きをしてくれます。

ContentView
struct ContentView: View {
    @State var fruits = ["Apple", "Banana", "Orange"]
    @State var editMode: EditMode = .inactive
    var body: some View {
        NavigationView {
            List {
                ForEach(fruits, id: \.self) { fruit in
                    HStack {
                        Image(systemName: "leaf.fill")
                            .foregroundStyle(.green)
                        Text(fruit)
                    }
                }
                .onMove(perform: moveFruits)
            }
            .navigationBarTitle("Fruits")
            .navigationBarItems(
                leading: Button(action: {
                    addFruit()
                }) {
                    Image(systemName: "plus.circle.fill")
                    Text("Add Fruit")
                },
                trailing: EditButton()
            )
            .environment(\.editMode, $editMode)
        }
    }

    private func moveFruits(from source: IndexSet, to destination: Int) {
        fruits.move(fromOffsets: source, toOffset: destination)
    }
    
    private func deleteFruits(at offsets: IndexSet) {
        fruits.remove(atOffsets: offsets)
    }
    
    private func addFruit() {
        fruits.append("New Fruit \(fruits.count + 1)")
    }
}

しかし、このコードだと以下のGIFのようにeditMode.inactiveの状態でもリストの並び順をドラッグ&ドロップで変更できてしまいます。

Simulator Screen Recording - iPhone 15 - 2024-04-25 at 12.57.49.gif

これでは
editMode.inactiveの状態でリストの項目をドラッグ&ドロップで動かせない
という要件を満たせていません。

そこで、onMove()moveDisabled()を組み合わせるといいよ!という内容の記事があったため、以下のようなコードでデバッグをしてみました。

ContentView
import SwiftUI

struct ContentView: View {
    @State var fruits = ["Apple", "Banana", "Orange"]
    @State var editMode: EditMode = .inactive

    var body: some View {
        NavigationView {
            List {
                ForEach(fruits, id: \.self) { fruit in
                    HStack {
                        Image(systemName: "leaf.fill")
                            .foregroundStyle(.green)
                        Text(fruit)
                    }
+                    .moveDisabled(!editMode.isEditing)
                }
                .onMove(perform: moveFruits)
            }
            .navigationBarTitle("Fruits")
            .navigationBarItems(
                leading: Button(action: {
                    addFruit()
                }) {
                    Image(systemName: "plus.circle.fill")
                    Text("Add Fruit")
                },
                trailing: EditButton()
            )
            .environment(\.editMode, $editMode)
        }
    }
    
    private func moveFruits(from source: IndexSet, to destination: Int) {
        fruits.move(fromOffsets: source, toOffset: destination)
    }
    
    private func deleteFruits(at offsets: IndexSet) {
        fruits.remove(atOffsets: offsets)
    }
    
    private func addFruit() {
        fruits.append("New Fruit \(fruits.count + 1)")
    }
}

このコードに修正したところ、
editMode.inactiveの状態でリストの項目をドラッグ&ドロップで動かせない
という要件を満たすような挙動に変わりました。

しかし、同時に元々リストの右端に表示されていた三本線(リストの並べ替えハンドル)が表示されなくなるという問題点が見つかりました。

この状態でも編集はできるのですが、編集モードであるかどうかが画面右上のEditDoneの文言で判断することになります。
何より、
editMode.activeの状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
という要件を満たせていません。

そこで色々と試行錯誤をしてみたところ、以下のようなコードで想定通りの動きをしてくれることが分かりました。

ContentView
import SwiftUI

struct ContentView: View {
+    @State var fruits = [
+        Fruit(name: "Apple", sticky: false),
+        Fruit(name: "Banana", sticky: false),
+        Fruit(name: "Orange", sticky: false)
+    ]
    @State var editMode: EditMode = .inactive
    
    var body: some View {
        NavigationView {
            List {
                ForEach(fruits, id: \.id) { fruit in
+                    if fruit.sticky{
+                        HStack {
+                           Image(systemName: "leaf.fill")
+                               .foregroundStyle(.green)
+                            Text(fruit.name)
+                        }
+                    }else{
+                        HStack {
+                            Image(systemName: "leaf.fill")
+                                .foregroundColor(.green)
+                            Text(fruit.name)
+                        }
+                        .moveDisabled(true)
                    }
                }
                .onMove(perform: moveFruits)
            }
            .navigationBarTitle("Fruits")
            .navigationBarItems(
                leading: Button(action: {
                    addFruit()
                }) {
                    Image(systemName: "plus.circle.fill")
                    Text("Add Fruit")
                },
+                trailing: Button(action: {
+                    withAnimation(.easeInOut(duration: 0.3)) {
+                        editMode = editMode.isEditing ? .inactive : .active
+                        setStickyFlag()
+                    }
+                }) {
+                    Text(editMode.isEditing ? "Done" : "Edit")
+                }
            )
            .environment(\.editMode, $editMode)
        }
    }
    
    private func moveFruits(from source: IndexSet, to destination: Int) {
        fruits.move(fromOffsets: source, toOffset: destination)
    }
    
    private func deleteFruits(at offsets: IndexSet) {
        fruits.remove(atOffsets: offsets)
    }
    
    private func addFruit() {
        fruits.append(Fruit(name: "New Fruit \(fruits.count + 1)", sticky: true))
    }
    
+    // stickyフラグをセットする関数
+    private func setStickyFlag() {
+        for index in fruits.indices {
+            fruits[index].sticky.toggle()
+        }
+    }
}


+struct Fruit: Identifiable {
+    var id = UUID()
+    var name: String
+    var sticky: Bool  // このフルーツが移動可能かどうかのフラグ
+}

大きく変更した点は以下の通りです。

  • FruitをStruct化し、移動可能かどうかのフラグ(sticky)を持つようにした
  • Edit(Done)ボタンをタップしたタイミングでFruits内の全ての要素のStickyを反転させる処理を追加した
  • View内にstickyのフラグで表示する部品を切り替える分岐を追加した

修正した後の動きは以下のとおりです。

当初の予定通り、

  • editMode.inactiveの状態でリストの項目をドラッグ&ドロップで動かせない
  • editMode.activeの状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
    の両方の要件を満たしています。

ただ、これは想定された書き方と異なる書き方である(と個人的には思っている)ので、もっと綺麗な書き方を知っている方がいたら教えていただけると非常に助かります。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?