Toggle Selection in a List - Swiftui

Toggle selection in a list - SwiftUI

Your @State private var toggle: Bool = false doesn't make sense. You have many courses, not a single course. Each course should have it's own toggle on/off, which is what you started to do with:

struct Course: Codable, Identifiable {
var isToggled = false /// here!

...
}

To use this, you can reference each course's isToggled inside the ForEach, like this:

ForEach(courses) { course in

Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(course.courseName, id: \.name) { item in

/// here!
Toggle(isOn: course.isToggled, label: {
Text(item.name)
})

}
}
}

However, this won't work. course.isToggled is a Bool, not a Binding<Bool>, which the Toggle expects.

Where can you get Binding<Bool>? From the @State var courses: [Course], of course! sorry for pun



The Binding<> part comes from the @State declaration.

Properties that are marked with @State, like your @State var courses: [Course], include a projectedValue that has the Binding<> type.

You can access the projectedValue by adding a $ to the property. So, if you write $courses, that will have type Binding<[Course]>.

Xcode autocompletion for $courses

But, your toggle expects Binding<Bool>, not Binding<[Course]>.

Toggle(isOn: Binding, label: { Text("A cool toggle") })

This is where the Bool part comes in.

You will need to replace the Binding's value, [Course], with a Bool. Well, we had a Bool before, right?

struct Course: Codable, Identifiable {
var isToggled = false /// this is a Bool!

Each course has a isToggled, which is a Bool. From earlier on in this answer, we got this inside the ForEach:

ForEach(courses) { course in

...

/// getting the Bool, which unfortunately doesn't work (yet)
Toggle(isOn: course.isToggled, label: {

... We need to somehow combine the Binding<> with the Bool. This means that we must

  • reference $courses (to get the Binding<>)
  • get each courses' isToggled

And... tada!

$courses[index].isToggled /// has type Binding<Bool>

To get index, we'll need to loop over courses.indices instead of directly looping over courses.

ForEach(courses.indices) { index in

...

/// this works!
Toggle(isOn: $courses[index].isToggled, label: {

Then, just replace every occurrence of course in your old code's ForEach with courses[index]. Here's the full working example:

ForEach(courses.indices) { index in
Section(header: Text(courses[index].title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(courses[index].courseName, id: \.name) { item in

/// $courses[index].isToggled is a Binding<Bool>
Toggle(isOn: $courses[index].isToggled, label: {
Text(item.name)
})
}
}
}

As a convenience so you don't have to do courses[index] every time you want the current course, you can use Array(zip as shown in this answer to loop over a (Int, Course). This also assigns a unique id for every Section inside the loop, so any transitions you add will work out smoothly.

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in

Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(course.courseName, id: \.name) { item in

Toggle(isOn: $courses[index].isToggled, label: {
Text(item.name)
})
}
}
}

Well (Int, Course) is actually (Range<Array<Course>.Index>.Element, Course) but that's pretty much the same thing.

Final result:

Toggles inside each row of the ForEach working

Edit for isToggled inside Content, not Course:

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in
Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(Array(zip(course.courseName.indices, course.courseName)), id: \.1.id) { (itemIndex, item) in

/// here!
Toggle(isOn: $courses[index].courseName[itemIndex].isToggled, label: {
Text(item.name)
})
}
}
}

List Item with Toggle subview and selection gesture

First, your [ItemDataModel] array should be moved outside the body, so it's not recreated every time:

struct ListView: View {
@State var selected = Set<Int>()
let items = (1...10).map(ItemDataModel.init) // move outside the `body`

var body: some View {
VStack {
Text(String(describing: selected))
List(items) { item in
ListItemView(dataModel: item)
.onTapGesture { if !(selected.remove(item.id) != .none) { selected.insert(item.id) }}
}
}
}
}

Then, make sure that the Toggle in your ListItemView doesn't take all the space (that's the default behaviour) and attach onTapGesture to override the parent's gesture:

struct ListItemView: View {
@ObservedObject var dataModel: ItemDataModel

var body: some View {
HStack {
Text(dataModel.title)
// Text("toggle") // if necessary add Toggle's label as `Text`
Spacer()
Toggle("", isOn: $dataModel.isOn) // use another initialiser
.fixedSize() // limit Toggle's width
.background(Color.red) // debug-only, to see the real frame
.onTapGesture {} // override tap gestures
}
.contentShape(Rectangle()) // make empty space *clickable*
}
}

SwiftUI: Get toggle state from items inside a List


struct ObjectOrder: Identifiable {
var id = UUID()
var order = ""
var isToggled = false

init(order: String) {
self.order = order
}
}

struct ContentView: View {

@State var pOrder = [
ObjectOrder(order: "Order1"),
ObjectOrder(order: "Order2"),
ObjectOrder(order: "Order3"),
ObjectOrder(order: "Order4"),
ObjectOrder(order: "Order5")
]

var body: some View {

List(pOrder.indices) { index in
HStack {
Text("\(self.pOrder[index].order)")
Toggle("", isOn: self.$pOrder[index].isToggled)
}
}
}
}

Try using .indices would give you an index to your object in the array. Adding a property to track the toggle (isToggled in the example above) would allow you to store and keep track of the objects toggled status.

SwiftUI clear button for all Toggles in a List

I made a small project to address your issue. The main takeaway is that you need to let SwiftUI know when to redraw the view after the model or model element's properties change. That can be done with @Published and/or objectWillChange which is available to any class conforming to the ObservableObject protocol.

import SwiftUI

struct ContentView: View {
@StateObject var model = Model()

class Model: ObservableObject {
@Published var items = [
Item(zone: "America", count: 589),
Item(zone: "Asia", count: 67),
Item(zone: "Africa", count: 207),
Item(zone: "Oceania", count: 9)
]

class Item: ObservableObject, Hashable {
static func == (lhs: ContentView.Model.Item, rhs: ContentView.Model.Item) -> Bool {
lhs.id == rhs.id
}

let id = UUID()
let zone: String
let count: Int

@Published var selected: Bool = false

init(zone: String, count: Int) {
self.zone = zone
self.count = count
}

func toggle() {
selected.toggle()
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}

var body: some View {
VStack {
ForEach(Array(model.items.enumerated()), id: \.element) { index, item in
HStack {
Text(item.zone)
Spacer()
Text(String(item.count))
Button {
item.toggle()
print(index, item.selected)
} label: {
Image(systemName: item.selected ? "checkmark.square" : "square")
.resizable()
.frame(width: 24, height: 24)
.font(.system(size: 20, weight: .regular, design: .default))
}
}
.padding(10)
.onReceive(item.$selected) { isOn in
model.objectWillChange.send()
}
}

Button {
model.items.forEach { item in
item.selected = false
}
model.objectWillChange.send()
} label: {
Text("Clear")
}

}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

https://github.com/aibo-cora/SwiftUI/blob/main/ResetCheckBoxes.md

How does one enable selections in SwiftUI's List

Depending on what you want, there are two ways to do this:

If you want to do this in "Edit mode":

You must enable "Edit mode" on the list before a selection matters. From the interface for List:

    /// Creates an instance.
///
/// - Parameter selection: A selection manager that identifies the selected row(s).
///
/// - See Also: `View.selectionValue` which gives an identifier to the rows.
///
/// - Note: On iOS and tvOS, you must explicitly put the `List` into Edit
/// Mode for the selection to apply.
@available(watchOS, unavailable)
public init(selection: Binding<Selection>?, content: () -> Content)

You do that by adding an EditButton to your view somewhere. After that, you just need to bind a var for something that implements SelectionManager(you don't need to roll your own here :D)

var demoData = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]

struct SelectionDemo : View {
@State var selectKeeper = Set<String>()

var body: some View {
NavigationView {
List(demoData.identified(by: \.self), selection: $selectKeeper){ name in
Text(name)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
}

This approach looks like this:
Sample Image

If you don't want to use "Edit mode":

At this point, we're going to have to roll our own.
Note: this implementation has a bug which means that only the Text will cause a selection to occur. It is possible to do this with Button but because of the change in Beta 2 that removed borderlessButtonStyle() it looks goofy, and I haven't figured out a workaround yet.

struct Person: Identifiable, Hashable {
let id = UUID()
let name: String
}

var demoData = [Person(name: "Phil Swanson"), Person(name: "Karen Gibbons"), Person(name: "Grant Kilman"), Person(name: "Wanda Green")]

struct SelectKeeper : SelectionManager{
var selections = Set<UUID>()

mutating func select(_ value: UUID) {
selections.insert(value)
}

mutating func deselect(_ value: UUID) {
selections.remove(value)
}

func isSelected(_ value: UUID) -> Bool {
return selections.contains(value)
}

typealias SelectionValue = UUID

}

struct SelectionDemo : View {
@State var selectKeeper = Set<UUID>()

var body: some View {
NavigationView {
List(demoData) { person in
SelectableRow(person: person, selectedItems: self.$selectKeeper)
}
.navigationBarTitle(Text("Selection Demo \(selectKeeper.count)"))
}
}
}

struct SelectableRow: View {
var person: Person

@Binding var selectedItems: Set<UUID>
var isSelected: Bool {
selectedItems.contains(person.id)
}

var body: some View {
GeometryReader { geo in
HStack {
Text(self.person.name).frame(width: geo.size.width, height: geo.size.height, alignment: .leading)
}.background(self.isSelected ? Color.gray : Color.clear)
.tapAction {
if self.isSelected {
self.selectedItems.remove(self.person.id)
} else {
self.selectedItems.insert(self.person.id)
}
}
}
}
}

Sample Image

SwiftUI List single selectable item

You'll need something to store all of the states instead of storing it per-checkmark view, because of the requirement to just have one thing checked at a time. I made a little example where the logic is handled in an ObservableObject and passed to the checkmark views through a custom Binding that handles checking/unchecking states:

struct CheckmarkModel {
var id = UUID()
var state = false
}

class StateManager : ObservableObject {
@Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]

func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.checkmarks[index].state
} set: { (newValue) in
self.checkmarks = self.checkmarks.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.state = newValue
} else {
//not the same index
if newValue {
itemCopy.state = false
}
}
return itemCopy
}
}
}
}

struct ContentView: View {
@ObservedObject var state = StateManager()

var body: some View {
NavigationView {
List(Array(state.checkmarks.enumerated()), id: \.1.id) { (index, item) in //<-- here
CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
}
}
}

struct CheckmarkView: View {
let index: Int
@Binding var check: Bool //<-- Here

var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}

What's happening:

  1. There's a CheckmarkModel that has an ID for each checkbox, and the state of that box
  2. StateManager keeps an array of those models. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the checkbox array. Any time a checkbox is set, it unchecks all of the other boxes. I also kept your original behavior of allowing nothing to be checked
  3. The List now gets an enumeration of the state.checkmarks -- using enumerated lets me keep your previous behavior of being able to pass an index number to the checkbox view
  4. Inside the ForEach, the custom binding from before is created and passed to the subview
  5. In the subview, instead of using @State, @Binding is used (this is what the custom Binding is passed to)

SwiftUI - Not able to search and toggle in a list

You iterate over indices but after filtering they do not correspond to original array, but just ordered indices of filtered array. You have to work with id of cars instead

Here is a sketch of idea:

ForEach(self.cars.filter {
self.searchText.isEmpty ? true : $0.name.lowercased().contains(self.searchText.lowercased())
}) { car in
self.row(for: car)
}

...

func row(for car: Car) -> some View {
let idx = self.cars.firstIndex(of car)!

return ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(Color.white).shadow(color:.green, radius: 5).padding(.leading).padding(.trailing)
HStack {
VStack (alignment: .leading) {
Text(self.cars[idx].name)
CatalogAvailable()
}.padding(.leading).padding(.trailing)
Toggle(isOn: self.$cars[idx].selected) {
Text("")
}.toggleStyle(CheckboxToggleStyle()).labelsHidden()
.frame(width: 10, height: 10, alignment: .trailing).padding()
}.padding().frame(maxWidth: .infinity, alignment: .leading)

}.onTapGesture {
self.cars[idx].selected.toggle()
}

}

Show view only if items are selected in list using EditButton and multiple selection in SwiftUI

Here is a way for you:

struct ContentView: View {

@State private var itemSelection = Set<String>()

let names = [ "Orange", "Apple", "Grape", "Watermelon"]

var body: some View {
NavigationView {
VStack{
List(names, id: \.self, selection: $itemSelection) { name in
Text(name)
}
.navigationTitle("Item List")
.toolbar {
EditButton()
}

if !itemSelection.isEmpty {

Button("Deselect All"){
print("Items: \(itemSelection)")
itemSelection.removeAll()
}
.transition(AnyTransition.move(edge: .bottom))

}

}
.animation(.default, value: itemSelection.isEmpty)
}
}
}

Here is second way for you with this advantage that if you select some items and then you did not deselect items, then app would consider that you do not wanted to have any selection anymore. In case you want have selection even after ending editing then the first answer is for you not this second answer here:

PS: I found a strange bug, editMode is not readable or usable when we have NavigationView, therefore I defined a stand alone view for edit button.

struct ContentView: View {

@State private var names: [String] = ["Orange", "Apple", "Grape", "Watermelon"]
@State private var itemSelection = Set<String>()

var body: some View {
NavigationView {
VStack{
List(names, id: \.self, selection: $itemSelection) { name in
Text(name)
}
.navigationTitle("Item List")
.toolbar { EditModeView(itemSelection: $itemSelection) }

if !itemSelection.isEmpty {

Button("Deselect All"){
print("Items: \(itemSelection)")
itemSelection.removeAll()
}
.transition(AnyTransition.move(edge: .bottom))
}
}
.animation(.default, value: itemSelection.isEmpty)
}
}
}

struct EditModeView: View {

@Environment(\.editMode) var editMode
@Binding var itemSelection: Set<String>

var body: some View {
EditButton()
.onChange(of: editMode?.wrappedValue.isEditing, perform: { newValue in

if let unwrappedNewValue: Bool = newValue, (!unwrappedNewValue) {
itemSelection.removeAll()
}

})
}
}


Related Topics



Leave a reply



Submit