在很多应用场景中,需要把同类数据按某种规则分组展示,例如:
聊天应用:按日期显示消息;
财务应用:按交易日期显示流水;
任务管理:按状态(未完成/完成分组)。
SwiftUI 提供 List、ForEach等工具,可以实现分组列表。

核心思路
分组列表本质上是两个步骤:
1、数据分组
将数据按照某个条件(例如日期、类型、状态)分组成字典或数组结构。
2、界面渲染
用嵌套的ForEach或Section来显示分组标题和组内数据。
1、数据分组
Swift提供了Dictionary(grouping:by:)方法,可以将数据分组:
let names = ["Anna", "Alex", "Brian", "Jack"]
let grouped = Dictionary(grouping: names) { $0.first! }
// grouped 的结果是:["A": ["Anna", "Alex"], "B": ["Brian"], "J": ["Jack"]]
这个例子把字符串数组names按照首字母分组,结果是一个以首字符为key的字典。
在实际应用中,通常会按照日期进行分组,例如:
struct Transaction {
    let date: Date
    let title: String
    let amount: Double
}
// 示例数据
let transactions: [Transaction] = [...]
// 按日期分组(忽略时间,只看年月日)
let calendar = Calendar.current
let groupedTransactions = Dictionary(grouping: transactions) { record in
    calendar.startOfDay(for: record.date)
}
返回一个 [Date:[Transaction]] 的字典,每个key是一天的日期,每个 value 是当天的所有记录。
calendar.startOfDay(for:) 的作用为,将对应的记录的Date对象“归零到当天的开始日期”,如果不使用 calendar.startOfDay(for:),就会导致同一天的时间也会被认为是不同的Date:
2025-11-03 10:05:00  → 组1
2025-11-03 15:42:17  → 组2  // 判断为不是同一组
因为日期分组只关心哪一天,而不关心具体时间,因此可以使用 calendar.startOfDay(for:),返回当天的零点,这样同一天的不同时间都会被归为同一个零点。
如果需要按照日期顺序显示,可以将字典转换为数组并排序:
let sortedGroups = groupedTransactions
    .map { (date: $0.key, records: $0.value) }
    .sorted { $0.date > $1.date } // 按日期倒序
如果用图来理解:
原始字典:
{
  2025-11-03: [r1,r2],
  2025-11-02: [r3,r4]
}
map 后:
[
  (date: 2025-11-03, records: [r1,r2]),
  (date: 2025-11-02, records: [r3,r4])
]
sorted 后:
[
  (date: 2025-11-03, records: [r1,r2]),
  (date: 2025-11-02, records: [r3,r4])
]
2、SwiftUI显示分组列表
有两种主要方式:
1、List + Section
List {
    ForEach(sortedGroups, id: \.date) { group in
        Section(header: Text(group.date.formatted(.dateTime.weekday(.wide).month().day()))) {
            ForEach(group.records, id: \.title) { item in
                HStack {
                    Text(item.title)
                    Spacer()
                    Text("$\(item.amount)")
                }
            }
        }
    }
}
.listStyle(.plain) // 可选择 group 或 plain
自带分组样式、支持系统样式、不支持定制样式。

2、ScrollView + VStack
ScrollView {
    LazyVStack (spacing: 15) {
        ForEach(sortedGroups, id: \.date) { group in
            // 分组标题
            HStack {
                let dateString = group.date.formatted(.dateTime.weekday(.wide).month().day())
                Text(dateString)
                    .font(.caption)
                    .foregroundColor(.gray)
                Spacer()
            }
            .padding(.vertical, 5)
            // 分组内容
            ForEach(group.records, id: \.title) { item in
                HStack {
                    Text(item.title)
                    Spacer()
                    Text("$\(item.amount)")
                }
                .padding()
                .background(Color.white)
                .cornerRadius(10)
            }
        }
    }
    .padding()
    .background(Color.gray.opacity(0.1))
}
支持自定义每个分组和单元格样式,在数据量大时,可以滚动性能略低于List。
注意事项
1、日期归一化
使用 Calendar.startOfDay(for:) 或自己提取年、月、日,保证同一天的数据不会被分到不同组。
2、排序
数据分组后,通常希望按时间顺序展示:
最新日期在上:.sorted { $0.date > $1.date }
最旧日期在上:.sorted { $0.date < $1.date }
3、标识唯一性
ForEach 的 id 必须唯一:
对象自身有唯一 ID(如 SwiftData persistentModelID);
或组合字段(如日期 + 索引)。
4、Scroll冲突
ScrollView无法和Scroll一起使用,如果已经使用了ScrollView,就可以放弃Scroll,配合ForEach输出数据。
如果数据足够大,可以配合LazyVStack输出内容。
总结
分组列表的核心是“先分组,再渲染”。
SwiftUI 提供两种方式:
1、List + Section(系统风格,快速)
2、ScrollView + VStack(高度自定义)
数据分组时注意日期归一化和排序,使用唯一 ID 或索引作为 ForEach 的 id,避免重复和渲染问题。
参考文章
Swift Dictionary字典初始化器:https://fangjunyu.com/2024/10/30/swift-dictionary-%e5%88%9d%e5%a7%8b%e5%8c%96%e5%99%a8/
扩展知识
折叠/展开分组:
@State private var collapsedDates: Set<Date> = []
// 分组标题
HStack {
    Text(dateString)
    Spacer()
    Button(action: {
        if collapsedDates.contains(group.date) {
            collapsedDates.remove(group.date)
        } else {
            collapsedDates.insert(group.date)
        }
    }) {
        Image(systemName: collapsedDates.contains(group.date) ? "chevron.down" : "chevron.right")
    }
}
if !collapsedDates.contains(group.date) {
    ForEach(group.records) { item in ... }
}
											
				    