I'm trying to find a way to limit how many items in a list that is selectable (with EditMode), lets say I have a List with 6 items and I want the user to be able to select 3 of them, when 3 are selected I want to disable the ones that are not selected. However, I want the items that are selected to continue to be de-selectable so the other items can be selected instead. The selectedItems Set is later converted to a data model and saved to database.
struct SelectItemListView: View {
var items = ["one", "two", "three", "four", "five", "six"]
var numberOfselectedItems = 3 // <- controlled by user input in real project
@State var selectedItems = Set<String>()
var body: some View {
List(items, id: \.self, selection: $selectedItems) { item in // <- id is \.id from datamodel in real project
Text(item)
}
.disabled(selectedItems.count >= numberOfselectedItems)
.environment(\.editMode, .constant(EditMode.active))
}
}
The code compiles but it disables the whole list, not the individual rows. Is that even possible in SwiftUI?
Unfortunately as of now there is no way to do what you want out of the box as far as I know.
However, it is doable by creating a custom ListItemRow
that would take any content (a generic content basically) but it will take care of selecting and deselecting the row.
struct CustomListRow<Content> : View where Content : View {
@Binding var isSelectable: Bool
@State private var isSelected: Bool = false
var onSelectionChanged :(_ isSelected:Bool) -> () = {_ in}
var content: () -> Content
var body: some View {
HStack {
Circle()
.overlay(
Circle()
.stroke(self.isSelected ? Color.blue : Color(red: 0.741, green: 0.741, blue: 0.749), lineWidth: 1)
.overlay(Image(systemName: "checkmark").resizable().scaledToFit().frame(width: 12, height: 12).foregroundColor(Color.white))
)
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(self.isSelected ? Color.blue : Color.clear)
self.content()
}
.onTapGesture {
if(self.isSelectable) {
self.isSelected.toggle()
self.onSelectionChanged(self.isSelected)
}
}
}
func onSelectionChanged(callback: @escaping (_ isSelected: Bool) -> ()) -> some View {
CustomListRow(isSelectable: self.$isSelectable, onSelectionChanged: callback, content: self.content)
}
}
and this is how to use it
struct ContentView: View {
var items = ["one", "two", "three", "four", "five", "six"]
var numberOfselectedItems = 3 // <- controlled by user input in real project
@State var selectedItems = Set<String>()
var body: some View {
List(items, id: \.self) { item in // <- id is \.id from datamodel in real project
CustomListRow(isSelectable: Binding(get: {
return (self.itemExist(item: item) || self.selectedItems.count < self.numberOfselectedItems)
}, set: { _ in})) {
Text("item: \(item) - Exist: \(self.itemExist(item: item) ? "true" : "false") - count: \(self.selectedItems.count)")
}
.onSelectionChanged { isSelected in
if(isSelected == true) {
self.selectedItems.insert(item)
} else {
self.selectedItems.remove(item)
}
}
}
}
func itemExist(item: String) -> Bool{
return self.selectedItems.contains(item)
}
}
You can take this even further and create a custom List
that would make use of this CustomListItemRow
and you would pass couple extra variables in the initializer such as Data
and MaxSelectionSize
and take care of controlling selection in the custom view to make this a reusable List
in different projects. But this is beyond the scope of this question.