如何使用 SwiftUI 单选制作列表

Whi*_*ger 11 list selection swift ios13 swiftui

我正在创建单个选择列表以在我的应用程序的不同位置使用。


问题:

有没有我不知道的简单解决方案?

如果没有,我该如何完成当前的解决方案?


我的目标:

  1. 始终仅选择一项或一项或未选择一项的列表(取决于配置)
  2. 透明背景
  3. 在项目选择上 - 执行通过 init() 方法设置为参数的操作。(该操作需要所选项目信息。)
  4. 以编程方式更改列表数据并将选择重置为第一项

我当前的解决方案如下: 选择第二项的列表视图

我不能使用 Picker,因为外部操作(目标 3)很耗时。所以我认为它不会顺利进行。很可能在 SwiftUI 中有我的问题的解决方案,但是我错过了它,因为我是 swift 的新手,或者我理解并不是所有的东西都在 SwiftUI 中完美运行,例如:List 的透明背景(这就是为什么我需要清除init() 中的背景)。

所以我开始自己实现选择,并在此处停止:单击 item(Button) 时,我当前的解决方案不会更新视图。(只有出去和回到页面更新视图)。并且仍然可以选择多个项目。

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)
]
Run Code Online (Sandbox Code Playgroud)

测试一些想法:

尝试在 ModuleList 中添加 (isSelected: Bool) 的 @State 数组并将其绑定到可能会更新视图的 Module isSelected 参数...但在 init() 中填充此数组失败,因为 @State 数组参数在 .append() 之后将保持为空)... 也许添加函数 setList 可以解决这个问题,而我的目标是 Nr。4. 但我不确定这是否真的会首先更新我的观点。

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"
 ]
Run Code Online (Sandbox Code Playgroud)

LuL*_*aGa 35

实现这一点的最简单方法是在包含带有选择的列表的视图中使用@State,并将其作为@Binding 传递给单元格:

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
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我必须添加 `.background(Color.white)` 或 `HStack` 与 Text 宽度相同,然后无法单击整个视图。还可以设置 HStack 宽度来实现这一点,无论如何,这是一个很好的答案。 (2认同)

Joh*_* M. 8

选择

SwiftUI 目前没有内置的方式来选择列表的一行并相应地更改其外观。但你实际上非常接近你的答案。事实上,您的选择实际上已经有效,只是没有以任何方式使用。

为了说明,ModuleCell(...)在您的之后添加以下行ForEach

.background(i == self.selectionKeeper ? Color.red : nil)
Run Code Online (Sandbox Code Playgroud)

换句话说,“如果我的当前行 ( i) 与存储在 中的值匹配selectionKeeper,则将单元格着色为红色,否则,使用默认颜色。” 您会看到,当您点击不同的行时,红色会跟随您的点击,表明底层选择实际上正在发生变化。

取消选择

如果您想启用取消选择,您可以传入 aBinding<Int?>作为您的选择,并将其设置nil为点击当前选定的行时:

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

去重状态和关注点分离

在更结构化的层面上,在使用像 SwiftUI 这样的声明式 UI 框架时,您确实需要一个单一的事实来源,并将您的视图与模型干净地分开。目前,你有重复的状态-selectionKeeperModuleListisSelectedModule是否选择一个给定的模块的两个跟踪。

此外, isSelected 应该真正是视图 ( ModuleCell)的属性,而不是模型 ( Module)的属性,因为它与视图的显示方式有关,而不是每个模块的固有数据。

因此,您ModuleCell应该看起来像这样:

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 ------------------------------^
    }
}
Run Code Online (Sandbox Code Playgroud)

ForEach会看起来像

ForEach(0..<modules.count) { i in
    ModuleCell(module: self.modules[i],
               isSelected: i == self.selectionKeeper,
               action: { self.changeSelection(index: i) })
}
Run Code Online (Sandbox Code Playgroud)


Ene*_*man 6

这是一种更通用的方法,您仍然可以根据需要扩展答案;

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


细节

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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并进行演示;

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()
                }
            }
        }
    }
    
}
Run Code Online (Sandbox Code Playgroud)

最后结果
在此处输入图片说明


Eon*_*nil 5

使用与Optional类型选择变量的绑定。 List将只允许选择一个元素。

struct ContentView: View {
    // Use Optional for selection.
    // Instead od Set or Array like this...
    // @State var selection = Set<Int>()
    @State var selection = Int?.none
    var body: some View {
        List(selection: $selection) {
            ForEach(0..<128) { _ in
                Text("Sample")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 在 macOS 10.15.2 (19C57) 上针对 Mac 应用程序使用 Xcode 版本 11.3 (11C29) 进行了测试。