Skip to main content

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 や、swipeActionrole: .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) {
}
}