How to Make List with Single Selection with Swiftui

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:

  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: 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:
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

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



Leave a reply



Submit