关于列表整体的设置,请查看:List
ListRow
。所以看见以 listRow
开头的修饰符,就应该是加在每个行元素后面的有一个专用的 listItemTint()
修饰符,用于控制列表如何为其行着色。确切的行为取决于您的应用程序运行的平台,但代码是相同的
Text("All Projects")
// 例如,这会将偶数行染成红色,奇数行染成绿色
.listItemTint(i.isMultiple(of: 2) ? .red : .green)
// 但该修饰符具体的作用取决于平台:
// 在 iOS 上,这会将图标更改为红色和绿色,但保留文本的原色
// 在 macOS 上,这将图标更改为红色和绿色,还会覆盖用户首选的强调色
// 在 watchOS 上,会将行背景颜色(称为“背景盘”)更改为红色或绿色
// 在 tvOS 上它什么也不做。
// 在 macOS 上,您可以尊重用户的强调色,同时添加您自己喜欢的列表行色调,如下所示:
List(1..<51) { i in
Label("Row \\(i)", systemImage: "\\(i).circle")
.listItemTint(.preferred(i.isMultiple(of: 2) ? .red : .green))
}
SwiftUI 会自动调整列表行分隔符的间距,使其与文本前缘对齐,并提供了 .listRowSeparatorLeading
和 .listRowSeparatorTrailing
的对齐辅助线,让你可以根据需要自定义。
// 例如,您可以将分隔符的前缘设置为整行的前缘,这意味着分隔符将与所有内容对齐,而不仅仅是文本部分:
List(0..<51) { i in
Label("Row \\(i)", systemImage: "\\(i).circle")
.alignmentGuide(.listRowSeparatorLeading) { d in
d[.leading]
}
}
// 该示例中,使用 0 而不是 d[.leading] 应该具有相同的效果
// 或者,您可以根据与您的设计相匹配的值使用自定义值:
List(0..<51) { i in
Label("Row \\(i)", systemImage: "\\(i).circle")
.alignmentGuide(.listRowSeparatorLeading) { _ in
100
}
}
// 您还可以自定义列表行分隔符的后缘,作为前导行分隔符插入的补充或替代。
// 例如,此代码通过将 “分隔符末尾间距和内容边缘对齐” ,从而实现行分隔符仅位于行文本下方:
List(0..<51) { i in
Label("Row \\(i)", systemImage: "\\(i).circle")
.alignmentGuide(.listRowSeparatorTrailing) { d in
d[.trailing]
}
}
当【表单Form】或【列表List】中有行时,SwiftUI 喜欢假设【整行】本身是可点击的。这使得用户可以更轻松地进行选择,因为他们可以点击行中的任意位置来触发其中的按钮。在下面例子中,我们有多个按钮,因此 SwiftUI 按顺序点击所有按钮 - rating
设置为 1,然后是 2,然后是 3、4 和 5,这就是为什么它最终都是 5 。
// contentView
@State var rating: Int = 3
Form{
Section("Write a review"){
RatingView(rating: $rating)
}
}
// subView
@Binding var rating: Int
HStack {
ForEach(1..<maximumRating + 1, id: \\.self) {
number in
Button {
rating = number
print(rating)
} label: {
image(for: number).foregroundStyle(number > rating ? offColor : onColor)
}
}
}
// 这时,可以通过在子视图中附加到整个 HStack 的额外修饰符,来禁用整个“点击行以触发其按钮”行为:
HStack { ... }
.buttonStyle(.plain)
如果您只想让用户以滑动的方式从数组中删除项目,而不添加任何其他逻辑,那么这个删除方法非常有效。具体按照以下两步操作:
List
使用数据绑定,绑定到具体的数组List
设置 editActions
参数// 设置完成后,用户可以立即通过滑动来删除行,并且 users 数组将在用户执行此操作时更新
struct ContentView: View {
@State private var users = ["Glenn", "Malcolm", "Nicola", "Terri"]
var body: some View {
NavigationStack {
List($users, id: \\.self, editActions: .delete) { $user in
Text(user)
}
}
}
}
// 如果希望在数组中移动项目,而不添加任何额外逻辑,那 editActions 参数的值改成
List($users, id: \\.self, editActions: .move)
// 如果希望列表既支持删除,也支持移动,那 editActions 参数的值改成
List($users, id: \\.self, editActions: .all)
// 如果想禁用某一行的删除,请在具体行后面使用 deleteDisabled() 修饰符
// 例如可以说,只要始终至少剩余 1 行,用户就可以从列表中自由删除
struct ContentView: View {
@State private var users = ["Glenn", "Malcolm", "Nicola", "Terri"]
var body: some View {
NavigationStack {
List($users, id: \\.self, editActions: .delete) { $user in
Text(user)
.deleteDisabled(users.count < 2)
}
}
}
}
// 如果想禁用某一行的移动,请使用 moveDisabled() 修饰符
// 这时这一行就无法长按拖动,利用这个我们可以制作 “比如置顶的项目就不支持拖动排序” 的效果
Text(user)
.moveDisabled(user == "Glenn")
具体参见:单个列表行设置
onDelete(perform:)
和 onMove(perform:)
修饰符附加到 ForEach
视图ForEach
的修饰符存在,不能直接用于 List
。因为列表可以包含静态行,这些静态行当然不能被删除和移动ForEach
视图。 因此,这些功能的修饰符仅能添加到 ForEach
视图上,而不能添加到 List
列表视图上。// 该处理程序需要具有接受要删除的多个索引的特定签名,如下所示:
struct ContentView: View {
@State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationStack {
List {
ForEach(users, id: \\.self) { user in
Text(user)
}
// 删除方法
.onDelete(perform: delete)
// 移动方法
.onMove(perform: move)
}
.navigationTitle("Users")
}
}
// 删除方法:
// 通常需要调用 remove(atOffsets:) 方法来从序列中删除请求的行
// 由于 SwiftUI 正在监视您的状态,因此所做的任何更改都会自动反映在 UI 中
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
// 移动方法:
// 移动多个项目时,最好先移动后面的项目,这样可以避免移动其他项目并导致索引混乱
// 幸运的是,Swift 的序列有一种内置的方法来为我们移动索引集,因此我们只需传递参数即可使其正常工作
func move(from source: IndexSet, to destination: Int) {
users.move(fromOffsets: source, toOffset: destination)
}
}
swipeActions()
修饰符让您可向列表行添加一个或多个滑动操作按钮,并且控制它们属于哪一侧,以及是否应使用完全滑动来触发它们。
// 默认情况下,按钮将放置在行的右边缘,并且没有任何颜色
// 因此当您从右向左滑动时,将显示一个灰色按钮:
List {
Text("Taylor Swift")
.swipeActions {
Button("Send message", systemImage: "message") {
print("Hi")
}
}
}
// 如果要定义按钮位置,请使用 edge 参数
// 如果想在行的任一侧添加不同的滑动操作,只需使用不同的边缘调用 swipeActions() 两次即可
.swipeActions(edge: .leading) { ... }
.swipeActions(edge: .trailing) { ... }
// 如果要自定义菜单按钮的颜色:一是使用 Tint() 修饰符,一是通过指定按钮的语义角色
// 对于真正具有破坏性的按钮,应该使用 Button(role: .destructive) 而不是仅仅指定红色
List {
Text("Taylor Swift")
.swipeActions {
Button("Delete", systemImage: "minus.circle", role: .destructive) {
print("Deleting")
}
.tint(.orange)
}
}
// 默认情况下,如果用户滑动得足够远,将自动触发第一个滑动操作。
// 如果您想禁用此功能,请在创建滑动操作时将 allowsFullSwipe 设置为 false
List {
ForEach(friends, id: \\.self) { friend in
Text(friend)
.swipeActions(allowsFullSwipe: false) {
Button {
print("Muting conversation")
} label: {
Label("Mute", systemImage: "bell.slash.fill")
}
.tint(.indigo)
Button(role: .destructive) {
print("Deleting conversation")
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
<aside>
💡 滑动操作是添加额外功能的绝佳功能,但它们与之前使用的 onDelete()
修饰符不能很好配合。因此,需要手动设置“删除”功能。
在使用 swipeActions
操作具体列表对象时,往往需要搭配 tag
修饰符,这个非常重要。原因如下:
</aside>
tag
修饰符:是为了标识可选择的视图,例如 Picker
或 TabView
中的可能值,或者在 List
中标识选定的项目。
为什么要加上 tag
修饰符呢?因为在 List
中,我们需要标识每个项目以便进行选择。tag
修饰符允许将 prospect
对象与 List
中的选定项目进行关联,从而使得我们可以在用户与列表交互时识别和处理所选项目。
List(filteredProspects, selection: $selectedProspects) {
prospect in
NavigationLink {
ProspectDetailView(prospect: prospect)
} label: {
// Your view content here
}
.swipeActions {
// Your swipe actions here
}
.tag(prospect) // 使用tag修饰符标识每个prospect对象
}
<aside>
💡 当用户与列表中的项目进行交互(如选择、滑动等)时,SwiftUI需要知道具体是哪个项目被选中或操作。添加 tag
修饰符可以帮助识别和跟踪每个项目。我的理解是,这里列表的 selection 参数绑定的是一个集合 set ,这部分没有 ID 标识出来,因此需要额外地添加 tag
修饰符,来支持 .swipeActions
的删除操作
</aside>
有时候点击列表项,只是需要先做“选择”,以便选择完后再采取统一操作。List
的项目支持单选和多选,但仅当列表处于编辑模式时才支持。
让列表项支持单选一般需要 3 个步骤:
要支持单选,请先声明与 “在列表中使用的类型” 相同的可选状态属性。
struct ContentView: View {
// 例如:以下列表展示的是 String 数据,那声明的【选择项】应该为 String?
// 默认情况下它不会选择任何内容,但当点击具体项时,它会包含用户姓名
@State private var selection: String?
let users = ["Tohru", "Yuki", "Kyo", "Momiji"]
var body: some View {
// users 数组也是字符串
List(users, id: \\.self) { user in
Text(user)
}
}
}
接下来需要告诉列表在点击一行时更新该状态属性,即双向绑定。这代表点击具体行会更新【选择项】,而更新【选择项】也会选择具体行。
List(users, id: \\.self, selection: $selection){
user in
Text(user)
}
最后,可以以某种方式使用该【选择项】数据。
//例如,如果【选择项】有数据,我们可以在列表下方显示文本:
if let selection {
Text("You selected \\(selection)")
}
List 还可以让用户同时选择多行,并且一次性操作它们。如果想让 List 支持选择多个项目,则需要做以下更改:
更改 selection
状态属性的类型,以便它能存储一组值(而不是单个值),这里统一将其声明为集合 Set 。
为什么声明为集合而不是数组?因为集合是无序的,且不能重复,符合选择列表项的要求。
//例如,把状态属性的类型改成集合。默认情况下可以为空,这意味着未选择任何内容:
@State private var selection = Set<String>()
真正的挑战是:如何启用多项选择?因为默认当用户点击一行时,SwiftUI 会自动取消上一个选择项。iOS 有一个隐藏的手势来激活多选模式:如果用两根手指在数据上水平滑动,它将激活。如果使用模拟器,则需要按住 Option 键以启用两指模式,然后按住 Shift 键以启用滑动,然后在列表上从左向右滑动。虽然这两种方法都有效,但都不是很明显。
因此更好的方法是:添加 EditButton
,它会自动处理启用或禁用编辑,因此也启用或禁用多选模式。
// 显示时,我们可以在集合上调用 formatted() 将所有名称显示为单个字符串:
if selection.isEmpty == false {
Text("You selected \\(selection.formatted())")
}
// 最后,只需要将以下代码放在布局中的某个位置:
// 现在您应该能够自由地进入和退出多选模式,然后点击每个列表行旁边的复选框将其添加到您的选择中。然后您如何处理您的选择取决于您!
EditButton()
//1. 首先添加一个状态属性,储存选择的行的集合
@State private var selectedProspects = Set<Prospect>()
//2. 然后将该选择绑定到列表:
List(prospects, selection: $selectedProspects) { prospect in ...}
//重要提示:最后为了帮助 SwiftUI 了解 List 中的每一行对应一个潜在对象,需要添加以下代码:
List(prospects, selection: $selectedProspects) {
prospect in
NavigationLink{ ... }
.tag(prospect)
}
//使用时,定义一个【删除所有选择行的】函数
func delete() {
for prospect in selectedProspects {
modelContext.delete(prospect)
}
}
//在Toolbar中添加相应的按钮进行调用(使用EditButton打开多选模式)
ToolbarItem(placement: .topBarLeading) {
EditButton()
}
if selectedProspects.isEmpty == false {
ToolbarItem(placement: .bottomBar) {
Button("Delete Selected", action: delete)
}
}
区别于放在列表后面的修饰符,以下修饰符都是放在列表的【每一行视图】后面的。另外如果有四行元素都要设置背景,是需要逐个添加的,没有办法添加到父元素 List
上,不过在 ForEach
视图下就设置一次就可以。ForEach
实际上类似于给它们包了一层。
修饰符代码示例 | 说明 | |
---|---|---|
隐藏每行的分割线 | .listRowSeparator(.hidden) | |
设置每行的分割线颜色 | .listRowSeparatorTint(.red) | |
设置每行的边距 | .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0)) | 列表中每行是有默认边距的,可以设置四个方向上的缩进值 |
.listRowInsets(EdgeInsets()) | 可以通过设置空的 EageInsets 对象,实现去掉默认边距 | |
设置每行的背景色 | .listRowBackground(Color.blue) | 列表中每行是有默认背景色 |
.listRowBackground(Color.clear) | 可以设置背景色为 clear 达到去掉背景色的效果 | |
设置每行元素的颜色 | .listItemTint() | 参见:‣ |
调整 Section 标题 | .headerProminence(.increased) | 指定更大、更粗的 Section 标题文本 |
设置每行的 badget | .badge("On") | 添加的 badge 徽章会自动显示为辅助颜色的右对齐文本 |