List の編集 - onMove, onDelete etc
onMove, onDelete 使用するときに、あっちを立てるとこっちが立たない、みたいな状態になる。
アプリによってやり方変わると思うので、「これが正解」とせず、雑多なまま残しておく。
並べ替え可能にする
swift
List {
ForEach(elems, id: \self) { elem in
...
}
.onMove { src, dst in
}
}
注意点
ForEach を間に挟む必要があって、以下の方式だと onMove は動作しない
List(elems, id: \self) { elem in
...
}
編集モードにするボタンを配置
swift
EditButton()
編集モード固定する
swift
List {
}
.environment(\.editMode, .constant(EditMode.active))
→ 並べ替えボタンを常時表示したい場合はこれ
編集モードをコードから参照
swift
@Environment(\.editMode) var editMode
..
var isEditing: Bool {
editMode?.wrappedValue.isEditing ?? false
}
onMove 注意点
onMove
つけてると編集モードでなくとも並べ替え出来てしまう(並べ替え用のボタンは出ないが)
↓
moveDisabled(!editActive)
で 防ぐ
swift
@State var editActive: Bool = false
..
Button(editActive ? "Done" : "Edit") {
editActive.toggle()
}
..
List {
ForEach {
}
.onDelete { indexes in
appData.routePoints.remove(atOffsets: indexes)
}
.onMove { src, dst in
appData.books.move(fromOffsets: src, toOffset: dst)
}
.moveDisabled(!editActive)
// .deleteDisabled(true)
}
.environment(\.editMode, .constant(editActive ? EditMode.active : EditMode.inactive))
onMove 注意点 (逆手にとる)
並べ替え用ボタンを出したいので 常時編集モードにしておく、とやった場合、
delete ボタンや Swipe Aciton との共存が出来ないのが難点
→ onMove はアイコンが出ないだけで、編集モードでなくても実は動く
→ アイコンだけ自前で出せば良い
swift
HStack {
...
Image(systemName: "line.3.horizontal")
.foregroundStyle(.tertiary)
}
複数選択の行 削除の例
配列から IndexSet
に変換している
swift
Button("Delete Selected Rows", role: .destructive) {
let indexes = IndexSet(selectedRoutePoints.compactMap({ appData.routePoints.firstIndex(of: $0) }))
appData.routePoints.remove(atOffsets: indexes)
}
SwiftData での onMove, onDelete
WaypointList というクラスを扱うとして、
swift
@Query(sort: \Waypoint.order) private var waypoints: [Waypoint]
このように直接してしまうと、画面での並べ替えタイミングと、
SwiftData の order 列の反映タイミングでラグが発生してうまくいかなかったので、
順番とオブジェクトを保持するクラスを別途用意することにした。
swift
class WaypointContainer: Identifiable, Hashable {
var order: Int
var waypoint: Waypoint
...
}
テンプレ
swift
@State var waypointContainers: [WaypointContainer] = [] // onLoad や onChange 等の必要なタイミングで読み直す
...
List {
ForEach(waypointContainers) { waypointContainer in
}
.onMove { src, dst in
moveItems(src, dst)
}
.onDelete { idxs in
deleteItems(idxs)
}
}
.environment(\.editMode, .constant(EditMode.active))
...
func moveItems(_ src: IndexSet, _ dst: Int) {
waypointContainers.move(fromOffsets: src, toOffset: dst)
for (i, item) in waypointContainers.enumerated() {
item.order = i+1 // こちらが並べ替えで使われて
item.waypoint.order = i+1 // こちらは SwiftData で保存される
}
try? modelContext.save()
}
func deleteItems(_ idxs: IndexSet) {
let removedItems = idxs.map { waypointContainers[$0] }
waypointContainers.remove(atOffsets: idxs)
for item in removedItems {
modelContext.delete(item.waypoint)
}
try? modelContext.save()
for (i, item) in waypointContainers.enumerated() {
item.order = i+1
item.waypoint.order = i+1
}
}
削除 : onDelete の UI 悩みどころ
- 編集モードにしておくと、左側に 通行禁止のアイコンが出る
- 並べ替えボタンのために 常時 編集モードにしておくと、見た目がかなり煩雑
- かといって編集モード切り替えるのも直感的でない
- 削除は Swipe または Context Menu にするのが良いかも
削除 : Swipe Action での代替
onDelete
や、swipeAction
の role: .destructive
だと表示上、即時消えてしまう
→ これだと確認画面を出したいときに困る
→ swipeAction
で .tint(.red)
すると良い
swipeAction
だと EditMode.active
設定すると反応しない
swift
@State private var isDeleteConfirmationDialogPresented: Bool = false
...
List {
ForEach(eventLocations) { eventLocation in
HStack {
...
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button("Delete", role: .destructive) {
isDeleteConfirmationDialogPresented = true
}
}
}
削除 : Context Menu での代替
Context Menu で代替するのも手
swift
.contextMenu {
Button("Delete", role: .destructive) {
}
}