How to make List with single selection with SwiftUI
Selection
SwiftUI does not currently have a built in way to select one row of a list and change its appearance accordingly. But you're actually very close to your answer. In fact, your selection is in fact already working, but just isn't being used in any way.
To illustrate, add the following line right after ModuleCell(...)
in your ForEach
:
.background(i == self.selectionKeeper ? Color.red : nil)
In other words, "If my current row (i
) matches the value stored in selectionKeeper
, color the cell red, otherwise, use the default color." You'll see that as you tap on different rows, the red coloring follows your taps, showing the underlying selection is in fact changing.
Deselection
If you wanted to enable deselection, you could pass in a Binding<Int?>
as your selection, and set it to nil
when the currently selected row is tapped:
struct ModuleList: View {
var modules: [Module] = []
// this is new ------------------v
@Binding var selectionKeeper: Int?
var Action: () -> Void
// this is new ----------------------------v
init(list: [Module], selection: Binding<Int?>, action: @escaping () -> Void) {
...
func changeSelection(index: Int){
if selectionKeeper != index {
selectionKeeper = index
} else {
selectionKeeper = nil
}
self.Action()
}
}
Deduplicating State and Separation of Concerns
On a more structural level, you really want a single source of truth when using a declarative UI framework like SwiftUI, and to cleanly separate your view from your model. At present, you have duplicated state — selectionKeeper
in ModuleList
and isSelected
in Module
both keep track of whether a given module is selected.
In addition, isSelected should really be a property of your view (ModuleCell
), not of your model (Module
), because it has to do with how your view appears, not the intrinsic data of each module.
Thus, your ModuleCell
should look something like this:
struct ModuleCell: View {
var module: Module
var isSelected: Bool // Added this
var Action: () -> Void
// Added this -------v
init(module: Module, isSelected: Bool, action: @escaping () -> Void) {
UITableViewCell.appearance().backgroundColor = .clear
self.module = module
self.isSelected = isSelected // Added this
self.Action = action
}
var body: some View {
Button(module.name, action: {
self.Action()
})
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.modifier(Constants.CellSelection(isSelected: isSelected))
// Changed this ------------------------------^
}
}
And your ForEach
would look like
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i],
isSelected: i == self.selectionKeeper,
action: { self.changeSelection(index: i) })
}
SwiftUI: Is there a way to create a list with single selection (like in iOS Settings)?
Meanwhile found an answer thanks to example code on hackingwithswift.com
struct ContentView: View {
var strengths = ["Mild", "Medium", "Mature"]
@State private var selectedStrength = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedStrength, label: Text("Strength")) {
ForEach(0 ..< strengths.count) {
Text(self.strengths[$0])
}
}
}
}.navigationBarTitle("Select your cheese")
}
}
}
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: Create a list of mx2 column grid of images on iOS 13 with single selection
I was able to solve this using this library WaterFallGrid
struct MyListView: View {
var myCard: [ImageDataModel] = []
var cardImage: Image = Image("placeholder")
@State var selection = ""
var body: some View {
ScrollView {
WaterfallGrid(myCard) { card in
CardImageView(myCard: card, cardImage: cardImage)
.border(selection == "\(card.id)" ? Color.blue : Color.white, width: 2.0)
.cornerRadius(5)
.onTapGesture {
print("Tapped on updated image = \(card.card)")
selection = "\(card.id)"
}
}
.gridStyle(
columns: 2
)
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
struct CardImageView: View {
@State var myCard: ImageDataModel
@State var cardImage: Image
var body: some View {
VStack {
cardImage
.resizable()
.frame(width: (UIScreen.main.bounds.width)/2.5)
.aspectRatio(5/3, contentMode: .fit)
.cornerRadius(5)
.onAppear {
getImage(cardModel: myCard)
}
}.padding(5)
}
struct ImageDataModel: Identifiable {
let id = UUID()
let imageUrl: String
}
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)
}
}
}
}
}
How can I set and use the argument selection in List in SwiftUI
How can I change to single selection List?
@State var selectKeeper: String? = nil // << default, no selection
What is Set()...?
A container for selected items, in your case strings from demoData
how I can set the argument "selection" and set the type as well
One variant is in .onAppear
as below
List(demoData, id: \.self, selection: $selectKeeper){ name in
Text(name)
}
.onAppear {
self.selectKeeper = [demoData[0]]
}
Type of selected is detected by type of state variable, it if Set that it is multi-selection, if it is optional, then single selection.
Related Topics
Change Tabview Indicator Swiftui
Insertion-Order Dictionary (Like Java's Linkedhashmap) in Swift
Timedmetadata' Deprecated. Another Method? <Updated>
Hstack with Sf Symbols Image Not Aligned Centered
Getting Unresolved Identifier 'Self' in Swiftui Code Trying to Use Timer.Scheduledtimer
Cast to a Metatype Type in Swift
Self.Image.Frame.Width = 20 Give Get Only Property Error
App Crashes When Trying to Append Data to a Child Value
Count Number of Decimal Places in a Float (Or Decimal) in Swift
Filter by Multiple Array Conditions
Binary Operator '+' Cannot Be Applied to Two 'T' Operands
Calculate Area of Mkpolygon in an Mkmapview
Firebase Sign Out Not Working in Swift
Undefined Behavior, Or: Does Swift Have Sequence Points
Use Uipangesturerecognizer to Drag Uiview Inside Limited Area