Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make List with single selection with SwiftUI

I am creating single selection list to use in different places in my app.


Questions:

Is there an easy solution I don't know?

If there isn't, how can I finish my current solution?


My goal:

  1. List with always only one item selected or one or none item selected (depending on configuration)
  2. Transparent background
  3. On item select - perform action which is set as parameter via init() method. (That action requires selected item info.)
  4. Change list data programmatically and reset selection to first item

My current solution looks like: List view with second item selected

I can't use Picker, because outer action (goal Nr. 3) is time consuming. So I think it wouldn't work smoothly. Most probably there is solution to my problem in SwiftUI, but either I missed it, because I am new to swift or as I understand not everything works perfectly in SwiftUI yet, for example: transparent background for List (which is why i needed to clear background in init()).

So I started implementing selection myself, and stoped here: My current solution does not update view when item(Button) is clicked. (Only going out and back to the page updates view). And still multiple items can be selected.

import SwiftUI

struct ModuleList: View {
    var modules: [Module] = []
    @Binding var selectionKeeper: Int
    var Action: () -> Void


    init(list: [Module], selection: Binding<Int>, action: @escaping () -> Void) {
        UITableView.appearance().backgroundColor = .clear
        self.modules = list
        self._selectionKeeper = selection
        self.Action = action
    }

    var body: some View {
        List(){
            ForEach(0..<modules.count) { i in
                ModuleCell(module: self.modules[i], action: { self.changeSelection(index: i) })
                }
        }.background(Constants.colorTransparent)
    }

    func changeSelection(index: Int){
        modules[selectionKeeper].isSelected =  false
        modules[index].isSelected = true
        selectionKeeper = index
        self.Action()
    }
}

struct ModuleCell: View {
    var module: Module
    var Action: () -> Void

    init(module: Module, action: @escaping () -> Void) {
        UITableViewCell.appearance().backgroundColor = .clear
        self.module = module
        self.Action = action
    }

    var body: some View {
        Button(module.name, action: {
            self.Action()
        })
            .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
            .modifier(Constants.CellSelection(isSelected: module.isSelected))
    }
}

class Module: Identifiable {
    var id = UUID()
    var name: String = ""
    var isSelected: Bool = false
    var address: Int

    init(name: String, address: Int){
        self.name = name
        self.address = address
    }
}

let testLines = [
    Module(name: "Line1", address: 1),
    Module(name: "Line2", address: 3),
    Module(name: "Line3", address: 5),
    Module(name: "Line4", address: 6),
    Module(name: "Line5", address: 7),
    Module(name: "Line6", address: 8),
    Module(name: "Line7", address: 12),
    Module(name: "Line8", address: 14),
    Module(name: "Line9", address: 11),
    Module(name: "Line10", address: 9),
    Module(name: "Line11", address: 22)
]

Testing some ideas:

Tried adding @State array of (isSelected: Bool) in ModuleList and binding it to Module isSelected parameter that MIGHT update view... But failed then populating this array in init(), because @State array parameter would stay empty after .append()... Maybe adding function setList would have solved this, and my goal Nr. 4. But I was not sure if this would really update my view in the first place.

struct ModuleList: View {
     var modules: [Module] = []
     @State var selections: [Bool] = []


     init(list: [String]) {
         UITableView.appearance().backgroundColor = .clear
         selections = [Bool] (repeating: false, count: list.count) // stays empty
         let test = [Bool] (repeating: false, count: list.count) // testing: works as it should
         selections = test
         for i in 0..<test.count { // for i in 0..<selections.count {
             selections.append(false)
             modules.append(Module(name: list[i], isSelected: $selections[i])) // Error selections is empty
         }
     }

     var body: some View {
         List{
             ForEach(0..<modules.count) { i in
                 ModuleCell(module: self.modules[i], action: { self.changeSelection(index: i) })
                 }
         }.background(Constants.colorTransparent)
     }
     func changeSelection(index: Int){
         modules[index].isSelected = true
     }
 }

 struct ModuleCell: View {
     var module: Module
     var Method: () -> Void

     init(module: Module, action: @escaping () -> Void) {
         UITableViewCell.appearance().backgroundColor = .clear
         self.module = module
         self.Method = action
     }

     var body: some View {
         Button(module.name, action: {
             self.Method()
         })
             .frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
             .modifier(Constants.CellSelection(isSelected: module.isSelected))
     }
 }

 struct Module: Identifiable {
     var id = UUID()
     var name: String = ""
     @Binding var isSelected: Bool

     init(name: String, isSelected: Binding<Bool>){
         self.name = name
         self._isSelected = isSelected
     }
 }

 let testLines = ["Line1","Line2","Line3","Line4"
 ]
like image 708
White_Tiger Avatar asked Oct 29 '19 18:10

White_Tiger


People also ask

How do I create a custom list in SwiftUI?

To begin, create a SwiftUI Xcode project, and create a struct , namely, Data . Let's get back in our ContentView. swift and populate some values into this struct . Now, inside your view, create a List, and use ForEach to add and see all your data in list form.

How do I search a list in SwiftUI?

SwiftUI displays the search bar under the navigation bar title and above the list that you'll filter. In multi-column view, you can choose in which view to display your search bar.


2 Answers

Here is a more generic approach, you can still extend answer according to your needs;

TLDR
https://gist.github.com/EnesKaraosman/d778cdabc98ca269b3d162896bea8aac


Detail

struct SingleSelectionList<Item: Identifiable, Content: View>: View {
    
    var items: [Item]
    @Binding var selectedItem: Item?
    var rowContent: (Item) -> Content
    
    var body: some View {
        List(items) { item in
            rowContent(item)
                .modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
                .contentShape(Rectangle())
                .onTapGesture {
                    self.selectedItem = item
                }
        }
    }
}

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

And to demonstrate;

struct PlaygroundView: View {
    
    struct Koko: Identifiable {
        let id = UUID().uuidString
        var name: String
    }
    
    var mock = Array(0...10).map { Koko(name: "Item - \($0)") }
    @State var selectedItem: Koko?
    
    
    var body: some View {
        VStack {
            Text("Selected Item: \(selectedItem?.name ?? "Select one")")
            Divider()
            SingleSelectionList(items: mock, selectedItem: $selectedItem) { (item) in
                HStack {
                    Text(item.name)
                    Spacer()
                }
            }
        }
    }
    
}

Final Result
enter image description here

like image 90
Enes Karaosman Avatar answered Sep 20 '22 08:09

Enes Karaosman


The easiest way to achieve this would be to have @State in the View containing the list with the selection and pass it as @Binding to the cells:

struct SelectionView: View {

    let fruit = ["apples", "pears", "bananas", "pineapples"]
    @State var selectedFruit: String? = nil

    var body: some View {
        List {
            ForEach(fruit, id: \.self) { item in
                SelectionCell(fruit: item, selectedFruit: self.$selectedFruit)
            }
        }
    }
}

struct SelectionCell: View {

    let fruit: String
    @Binding var selectedFruit: String?

    var body: some View {
        HStack {
            Text(fruit)
            Spacer()
            if fruit == selectedFruit {
                Image(systemName: "checkmark")
                    .foregroundColor(.accentColor)
            }
        }   .onTapGesture {
                self.selectedFruit = self.fruit
            }
    }
}
like image 25
LuLuGaGa Avatar answered Sep 22 '22 08:09

LuLuGaGa