There is a list with two sections: chosen options, that are reorderable and deletable and suggested options, that can be added to chosen. How to make new items be draggable and removable instantly? Screenshot shows how it looks, I want "five" and "six" be editable instantly when added.
It feels very weird to me that when I use ForEach(suggested, id: \.self)
instead of ForEach(filteredSuggestions(), id: \.self)
, chosen section looks as expected, but suggested obviously does not.
struct EditorView: View {
@State var chosen = ["one", "two", "three", "four"]
let suggested = ["five", "six", "seven", "eight"]
var body: some View {
List {
Section {
ForEach(chosen, id: \.self) { item in Text(item) }
.onDelete{ _ in }
.onMove{ _, _ in }
} header: { Text("chosen") }
Section {
ForEach(filteredSuggestions(), id: \.self) { item in
HStack(spacing: 14) {
Button { chosen.append(item) }
label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
.foregroundStyle(.green)
}
Text(item)
}
}
} header: { Text("suggested") }
}
.environment(\.editMode, .constant(.active))
}
private func filteredSuggestions() -> [String] {
return suggested.filter { item in
!chosen.contains(item)
}
}
}
When using filteredSuggestions()
in the ForEach
, it gets added as if EditMode is disabled.
Because EditMode
comes from the environment, when you use the filtered array (filteredSuggestions()
) in the ForEach, the filtered list may be recalculated every time the view updates, causing unexpected behavior possibly in regards to the timing of when the list is updated and when the EditMode
is applied to the first section.
On the other hand, when you use the suggested list directly (without filtering), it works because the data doesn’t change dynamically, and the ForEach can easily maintain the expected state of the list.
The solution is to keep the filtered suggestions in a state and control when the filtering happens in a SwiftUI-compatible way.
So create a filtered
state that is initially an empty array, populate it with the ouput of the filteredSuggestions()
function on appear, and use .onChange
modifier to update the state whenever the contents of chosen
change:
@State private var filtered: [String] = []
...
List {
...
}
.onAppear {
filtered = filteredSuggestions() // <- populate on appear
}
.onChange(of: chosen.count) {
filtered = filteredSuggestions() // <- update when appending to chosen
}
Here's the full code:
import SwiftUI
struct EditorView: View {
//Sample data
let suggested = ["five", "six", "seven", "eight"]
//State values
@State private var chosen = ["one", "two", "three", "four"]
@State private var filtered: [String] = []
//Environment values
@Environment(\.editMode) private var editMode
//Body
var body: some View {
List {
//Chosen
Section {
ForEach(chosen, id: \.self) { item in
Text(item)
}
.onDelete { indices in
chosen.remove(atOffsets: indices)
}
.onMove { indices, newOffset in }
} header: {
Text("chosen")
}
//Filtered
Section {
ForEach(filtered, id: \.self) { item in
HStack(spacing: 14) {
Button {
withAnimation {
chosen.append(item)
}
} label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
.foregroundStyle(.green)
}
Text(item)
}
}
} header: {
Text("suggested")
}
}
.onAppear {
filtered = filteredSuggestions() // <- populate on appear
}
.onChange(of: chosen.count) {
filtered = filteredSuggestions() // <- update when appending to chosen
}
.environment(\.editMode, editMode)
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
//Filtering function
private func filteredSuggestions() -> [String] {
return suggested.filter { !chosen.contains($0) }
}
}
//Preview
#Preview {
@Previewable @State var editMode: EditMode = .active
NavigationStack {
EditorView()
.environment(\.editMode, $editMode)
}
}
Note: After adding a suggestion, try removing it from the chosen list, it should reappear as a suggestion.