How can I show only selected items in SwiftUI List?

955 Views Asked by At

How can I make a List show a set of selected items when editMode is .inactive and all selectable options when editMode is .active so the user can change the set of selected items? Here is what I have tried:

import SwiftUI

struct SelectionView: View {
    @Environment(\.editMode) var editMode
    @State var selectedItems = Set<String>(["1-item", "2-item", "3-item", "4-item"])
    let allItems = ["1-item", "2-item", "3-item", "4-item"]
    var items: [String] {
        if editMode?.wrappedValue == .inactive {
            return Array(selectedItems)
        }
        else {
            return allItems
        }
    }

    var body: some View {
        NavigationView {
            List(items, id: \.self, selection: $selectedItems) { item in
                Text(item)
            }
            .navigationBarItems(trailing: EditButton())
            .navigationBarTitle("Items")
        }
    }
}

The selected array is now unordered as it is generated from a set and I eventually want to have it in the correct order. But I'm first trying to get the list to work as it is.

1

There are 1 best solutions below

1
On

Here is approach how it works for me. Unfortunately it is not completely nice due to specifics of List internals (or bug)...

The idea is to switch containers of List filtering one of them based on selected items, and the switching itself depends on manual tracking of EditMode

Anyway it works (tested with Xcode 11.2/iOS 13.2, as well in Preview)

struct SelectionView: View {
    @State var editMode: EditMode = .inactive // ! Needed manual to track states

    @State var selectedItems = Set<String>(["1-item", "2-item", "3-item", "4-item"])
    let allItems = ["1-item", "2-item", "3-item", "4-item"]
    var items: [String] {
        allItems.filter { self.selectedItems.contains($0) }
    }

    @State var applyFilter = false
    var body: some View {
        NavigationView {
            VStack {
                List(applyFilter ? items : allItems, id: \.self,
                     selection: $selectedItems) { item in
                        Text(item)
                }
                .navigationBarItems(trailing: EditButton())
                .navigationBarTitle("Items")
            }
//            .environment(\.editMode, $editMode) // << this is how it should be, but crash
            .environment(\.editMode, Binding<EditMode>(get: { self.editMode },
               set: {
                    self.editMode = $0

                    // !!! below is needed workaround, because applying filter directly
                    // on close EditMode result in crash due to cached List internals
                    // until switch EditMode animation is finished completely
                    if $0 == .inactive {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                            self.applyFilter = true
                        }
                    } else {
                        self.applyFilter = false
                    }
            }))
        }
    }
}