0

I'm trying to create a List and allow only one item to be selected at a time. How would I do so in a ForEach loop? I can select multiple items just fine, but the end goal is to have only one checkmark in the selected item in the List. It may not even be the proper way to handle what I'm attempting.

struct ContentView: View {

    var body: some View {
        NavigationView {
            List((1 ..< 4).indices, id: \.self) { index in
                CheckmarkView(index: index)
                    .padding(.all, 3)
            }
            .listStyle(PlainListStyle())
            .navigationBarTitleDisplayMode(.inline)
            //.environment(\.editMode, .constant(.active))
        }
    }
}

struct CheckmarkView: View {
    let index: Int
    @State var check: Bool = false
    
    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)
                }
            }
        }
    }
}

enter image description here

1

3 Answers 3

6

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)
Sign up to request clarification or add additional context in comments.

1 Comment

Great explanation and excellent example especially for someone just learning.
0

We can benefit from binding collections of Swift 5.5.

import SwiftUI

struct CheckmarkModel: Identifiable, Hashable {
    var id = UUID()
    var state = false
}

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

struct SingleSelectionList<Content: View>: View {
    
    @Binding var items: [CheckmarkModel]
    @Binding var selectedItem: CheckmarkModel?
    var rowContent: (CheckmarkModel) -> Content
    @State var previouslySelectedItemNdx: Int?
    
    var body: some View {
        List(Array($items.enumerated()), id: \.1.id) { (ndx, $item) in
            rowContent(item)
                .modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
                .contentShape(Rectangle())
                .onTapGesture {
                    if let prevIndex = previouslySelectedItemNdx {
                        items[prevIndex].state = false
                    }
                    self.selectedItem = item
                    item.state = true
                    previouslySelectedItemNdx = ndx
                }
        }
    }
}

struct CheckmarkModifier: ViewModifier {
    var checked: Bool = false
    func body(content: Content) -> some View {
        Group {
            if checked {
                ZStack(alignment: .trailing) {
                    content
                    Image(systemName: "checkmark")
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundColor(.green)
                        .shadow(radius: 1)
                }
            } else {
                content
            }
        }
    }
}

struct ContentView: View {
    
    @ObservedObject var state = StateManager()
    @State private var selectedItem: CheckmarkModel?
    
    
    var body: some View {
        VStack {
            Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
            Divider()
            SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem) { item in
                HStack {
                    Text(item.id.description + " " + item.state.description)
                    Spacer()
                }
            }
        }
    }
    
}

A bit simplified version

struct ContentView: View {

    @ObservedObject var state = StateManager()
    @State private var selection: CheckmarkModel.ID?

    var body: some View {
        List {
            ForEach($state.checkmarks) { $item in
                SelectionCell(item: $item, selectedItem: $selection)
                    .onTapGesture {
                        if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
                        state.checkmarks[ndx].state = false
                        }
                        selection = item.id
                        item.state = true
                    }
            }
        }
        .listStyle(.plain)
    }
}

struct SelectionCell: View {

    @Binding var item: CheckmarkModel
    @Binding var selectedItem: CheckmarkModel.ID?

    var body: some View {
        HStack {
            Text(item.id.description + " " + item.state.description)
            Spacer()
            if item.id == selectedItem {
                Image(systemName: "checkmark")
                    .foregroundColor(.accentColor)
            }
        }
    }
}

A version that uses internal List's selected mark and selection:

import SwiftUI

struct CheckmarkModel: Identifiable, Hashable {
    var name: String
    var state: Bool = false
    var id = UUID()
}

class StateManager : ObservableObject {
    @Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]
}

struct ContentView: View {
    
    @ObservedObject var state = StateManager()
    @State private var selection: CheckmarkModel.ID?
    @State private var selectedItems = [CheckmarkModel]()
    
    var body: some View {
        
        VStack {
            Text("Items")
            List($state.checkmarks, selection: $selection) { $item in
                Text(item.name + " " + item.state.description)
            }
            .onChange(of: selection) { s in
                for index in state.checkmarks.indices {
                    if state.checkmarks[index].state == true {
                        state.checkmarks[index].state = false
                    }
                }
                selectedItems = []
                
                if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
                    state.checkmarks[ndx].state = true
                    selectedItems = [state.checkmarks[ndx]]
                    print(selectedItems)
                }
            }
            .environment(\.editMode, .constant(.active))
            
            Divider()
            List(selectedItems) {
                Text($0.name + " " + $0.state.description)
            }
        }
        Text("\(selectedItems.count) selections")
    }
}

Comments

0
List {
    ForEach(0 ..< RemindTimeType.allCases.count) {
        index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
            .padding(.all, 3)
    }.listRowBackground(Color.clear)
}

struct CheckmarkView: View {
    let title: String
    let index: Int
    @Binding var markIndex: Int
    
    var body: some View {
        Button(action: {
            markIndex = index
        }) {
            HStack {
                Text(title)
                    .foregroundColor(Color.white)
                    .font(.custom(FontEnum.Regular.fontName, size: 14))
                Spacer()
                if index == markIndex {
                    Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(Color(hex: 0xe6c27c))
                }
            }
        }
    }
}

2 Comments

Please don't post only code as an answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes.
Even though this answer lacks some explanation, it works really well and is simple!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.