概要
タイトルの内容に関して一時的にとはいえ解決策を考えたのでメモとして残すついでに共有します。
困っている人の助けになれば幸いです。
環境
MacBook M1
MacOS Sonoma 14.4.1
Xcode 15.3
詳細
実装したい画面
実装しようとする画面の要件は以下の通りでした。
-
editMode
が.inactive
の状態でリストの項目をドラッグ&ドロップで動かせない -
editMode
が.active
の状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
具体的なイメージを先に共有しておくと、以下のような画面を実装することになっていました。
まず、editMode
が.active
の状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
という要件を満たすには以下のようなコードを書くとその通りの動きをしてくれます。
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
の状態でもリストの並び順をドラッグ&ドロップで変更できてしまいます。
これでは
editMode
が.inactive
の状態でリストの項目をドラッグ&ドロップで動かせない
という要件を満たせていません。
そこで、onMove()
とmoveDisabled()
を組み合わせるといいよ!という内容の記事があったため、以下のようなコードでデバッグをしてみました。
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
の状態でリストの項目をドラッグ&ドロップで動かせない
という要件を満たすような挙動に変わりました。
しかし、同時に元々リストの右端に表示されていた三本線(リストの並べ替えハンドル)が表示されなくなるという問題点が見つかりました。
この状態でも編集はできるのですが、編集モードであるかどうかが画面右上のEdit
とDone
の文言で判断することになります。
何より、
editMode
が.active
の状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
という要件を満たせていません。
そこで色々と試行錯誤をしてみたところ、以下のようなコードで想定通りの動きをしてくれることが分かりました。
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
の状態で編集モードを表す三本線がリストの右端に表示され、任意の箇所にドラッグ&ドロップで移動可能となる
の両方の要件を満たしています。
ただ、これは想定された書き方と異なる書き方である(と個人的には思っている)ので、もっと綺麗な書き方を知っている方がいたら教えていただけると非常に助かります。