iosswiftswiftui

How to make new items editable immediately?


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)
        }
    }
}

Solution

  • 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.

    enter image description here