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]>
.
But, your toggle expects Binding<Bool>
, not Binding<[Course]>
.
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 theBinding<>
) - 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:
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:
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)
}
}
}
}
}
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:
- There's a
CheckmarkModel
that has an ID for each checkbox, and the state of that box StateManager
keeps an array of those models. It also has a custom binding for each index of the array. For thegetter
, it just returns the state of the model at that index. For thesetter
, 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- The
List
now gets an enumeration of thestate.checkmarks
-- usingenumerated
lets me keep your previous behavior of being able to pass an index number to the checkbox view - Inside the ForEach, the custom binding from before is created and passed to the subview
- 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
Using Uiapplicationdelegateadaptor to Get Callbacks from Userdidacceptcloudkitsharewith Not Working
Alamofire Type 'Parameterencoding' Has No Member 'Url' Swift 3
How to Prevent Timer Slowing Down in Background
What Is Trailing Closure Syntax in Swift
Detect Left and Right Click Events on Nsstatusitem (Swift)
Why Does the Following Code Crash on an iPhone 5 But Not an iPhone 5S
For-In Loop and Type Casting Only for Objects Which Match Type
How to Rotate Sprites Around a Joint
How to Call the More Specific Method of Overloading
Session.Datataskwithurl Completionhandler Never Called
Why Does Int(Float(Int.Max)) Give Me an Error