如何使用 SwiftUI 制作单选列表
How to make List with single selection with SwiftUI
我正在创建单个 selection 列表以在我的应用程序的不同位置使用。
问题:
有没有我不知道的简单解决方案?
如果没有,我该如何完成当前的解决方案?
我的目标:
- 列表始终只有一项 selected 或一项或 none 项 selected(取决于配置)
- 透明背景
- 关于项目 select - 执行通过 init() 方法设置为参数的操作。 (该操作需要 selected 项目信息。)
- 以编程方式更改列表数据并将 selection 重置为第一项
我目前的解决方案如下:
List view with second item selected
我不能使用 Picker,因为外部操作(目标 3)很耗时。所以我认为它不会顺利进行。
很可能在 SwiftUI 中有解决我的问题的方法,但要么我错过了它,因为我是 swift 的新手,要么据我所知,在 SwiftUI 中并不是所有的东西都能完美运行,例如:List 的透明背景(这就是为什么我需要清除 init() 中的背景。
所以我开始自己实现 selection,到此为止:
单击项目(按钮)时,我当前的解决方案不会更新视图。 (只能出去和回到页面更新视图)。而且仍然可以 select 编辑多个项目。
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)
]
测试一些想法:
尝试在 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"
]
选择
SwiftUI 目前没有内置的方法来 select 列表的一行并相应地改变它的外观。但你实际上非常接近你的答案。事实上,您的 selection 实际上已经在工作,只是没有以任何方式使用。
为了说明,请在 ForEach
中的 ModuleCell(...)
之后添加以下行:
.background(i == self.selectionKeeper ? Color.red : nil)
换句话说,"If my current row (i
) matches the value stored in selectionKeeper
, color the cell red, otherwise, use the default color."您会看到当您点击不同的行时,红色会随着您的点击而变化,表明底层 select离子实际上正在发生变化。
Deselection
如果你想启用deselection,你可以传入一个Binding<Int?>
作为你的selection,并在当前selected 行被点击:
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()
}
}
去重状态和关注点分离
在更结构化的层面上,当使用像 SwiftUI 这样的声明性 UI 框架时,您确实需要一个单一的事实来源,并清楚地将您的视图与模型分开。目前,您有重复的状态 — ModuleList
中的 selectionKeeper
和 Module
中的 isSelected
都跟踪给定模块是否被 selected。
此外,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 ------------------------------^
}
}
你的ForEach
看起来像
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i],
isSelected: i == self.selectionKeeper,
action: { self.changeSelection(index: i) })
}
实现此目的的最简单方法是在包含带有选择的列表的视图中使用@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
}
}
}
使用绑定 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")
}
}
}
}
- 在 macOS 10.15.2 (19C57) 上针对 Mac 应用程序使用 Xcode 版本 11.3 (11C29) 进行了测试。
这是一个更通用的方法,您仍然可以根据需要扩展答案;
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
}
}
}
}
并进行演示;
struct PlaygroundView: View {
struct Koko: Identifiable {
let id = UUID().uuidString
var name: String
}
var mock = Array(0...10).map { Koko(name: "Item - \([=11=])") }
@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()
}
}
}
}
}
最终结果
我正在创建单个 selection 列表以在我的应用程序的不同位置使用。
问题:
有没有我不知道的简单解决方案?
如果没有,我该如何完成当前的解决方案?
我的目标:
- 列表始终只有一项 selected 或一项或 none 项 selected(取决于配置)
- 透明背景
- 关于项目 select - 执行通过 init() 方法设置为参数的操作。 (该操作需要 selected 项目信息。)
- 以编程方式更改列表数据并将 selection 重置为第一项
我目前的解决方案如下: List view with second item selected
我不能使用 Picker,因为外部操作(目标 3)很耗时。所以我认为它不会顺利进行。 很可能在 SwiftUI 中有解决我的问题的方法,但要么我错过了它,因为我是 swift 的新手,要么据我所知,在 SwiftUI 中并不是所有的东西都能完美运行,例如:List 的透明背景(这就是为什么我需要清除 init() 中的背景。
所以我开始自己实现 selection,到此为止: 单击项目(按钮)时,我当前的解决方案不会更新视图。 (只能出去和回到页面更新视图)。而且仍然可以 select 编辑多个项目。
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)
]
测试一些想法:
尝试在 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"
]
选择
SwiftUI 目前没有内置的方法来 select 列表的一行并相应地改变它的外观。但你实际上非常接近你的答案。事实上,您的 selection 实际上已经在工作,只是没有以任何方式使用。
为了说明,请在 ForEach
中的 ModuleCell(...)
之后添加以下行:
.background(i == self.selectionKeeper ? Color.red : nil)
换句话说,"If my current row (i
) matches the value stored in selectionKeeper
, color the cell red, otherwise, use the default color."您会看到当您点击不同的行时,红色会随着您的点击而变化,表明底层 select离子实际上正在发生变化。
Deselection
如果你想启用deselection,你可以传入一个Binding<Int?>
作为你的selection,并在当前selected 行被点击:
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()
}
}
去重状态和关注点分离
在更结构化的层面上,当使用像 SwiftUI 这样的声明性 UI 框架时,您确实需要一个单一的事实来源,并清楚地将您的视图与模型分开。目前,您有重复的状态 — ModuleList
中的 selectionKeeper
和 Module
中的 isSelected
都跟踪给定模块是否被 selected。
此外,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 ------------------------------^
}
}
你的ForEach
看起来像
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i],
isSelected: i == self.selectionKeeper,
action: { self.changeSelection(index: i) })
}
实现此目的的最简单方法是在包含带有选择的列表的视图中使用@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
}
}
}
使用绑定 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")
}
}
}
}
- 在 macOS 10.15.2 (19C57) 上针对 Mac 应用程序使用 Xcode 版本 11.3 (11C29) 进行了测试。
这是一个更通用的方法,您仍然可以根据需要扩展答案;
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
}
}
}
}
并进行演示;
struct PlaygroundView: View {
struct Koko: Identifiable {
let id = UUID().uuidString
var name: String
}
var mock = Array(0...10).map { Koko(name: "Item - \([=11=])") }
@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()
}
}
}
}
}
最终结果