SwiftUI遍历实体数组构建树形结构电视节目表视图问题求助
实现方案
核心思路
原有代码问题在于:对每个SchedSlot单独生成HStack,自然无法把同一频道的节目放在同一行。正确的实现需要分两步:先把一维的日程数组按「日期→频道」二级分组,再按分组层级渲染视图。
步骤1:数据分组处理
无需自定义复杂的Node类,直接用Swift高阶函数完成分组,性能更优也更适配SwiftUI的声明式语法:
struct ScheduleDisplayView: View { var schedule: [SchedSlot] let dfdo = DateFormatter.dateOnly let chanwidth: CGFloat = 100.0 // 调整到适合显示频道ID+呼号的宽度 let programWidth: CGFloat = 200.0 // 二级分组计算属性:外层key是日期字符串,内层key是频道ID,value是对应节目数组 private var groupedSchedule: [(date: String, channels: [(channel: Channel, programs: [SchedSlot])])] { // 第一步:先按日期分组 let dateGrouped = Dictionary(grouping: schedule) { slot in dfdo.string(from: slot.startTime!) } // 第二步:每个日期内按频道分组 return dateGrouped.map { date, slots in let channelGrouped = Dictionary(grouping: slots) { slot in slot.channel! }.map { (channel: $0.key, programs: $0.value) } // 可以在这里给频道排序,比如按channelID升序 let sortedChannels = channelGrouped.sorted { $0.channel.channelID < $1.channel.channelID } return (date: date, channels: sortedChannels) }.sorted { $0.date < $1.date } // 按日期升序排列 } // 后续视图部分... }
如果要沿用之前自定义的Node类,组装逻辑如下:
func buildTree(from slots: [SchedSlot]) -> RootNode { let root = RootNode() let df = DateFormatter.dateOnly for slot in slots { let dateStr = df.string(from: slot.startTime!) // 查找已存在的日期节点 var dayNode = root.children.first { $0.date == dateStr } if dayNode == nil { let newDay = DayNode(date: dateStr) root.add(child: newDay) dayNode = newDay } guard let day = dayNode else { continue } let chanId = String(slot.channel!.channelID) // 查找已存在的频道节点 var chanNode = day.children.first { $0.channel == chanId } if chanNode == nil { let newChan = ChannelNode(channel: chanId) day.add(child: newChan) chanNode = newChan } chanNode?.add(child: slot) } return root }
步骤2:视图渲染实现
直接遍历分组后的二级结构,即可实现预期的排版效果:
var body: some View { List { // 遍历所有日期 ForEach(groupedSchedule, id: \.date) { dayItem in // 显示日期头 Text(dayItem.date) .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 8) // 遍历当前日期的所有频道 ForEach(dayItem.channels, id: \.channel.channelID) { chanItem in // 单个频道的行:同一频道的所有节目放在同一个HStack里 HStack(alignment: .top, spacing: 16) { // 左侧频道信息 VStack(spacing: 4) { Text(String(chanItem.channel.channelID)) Text(chanItem.channel.callSign ?? "") } .frame(width: chanwidth) // 右侧节目列表,超出屏幕可横向滚动 ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(chanItem.programs, id: \.programID) { program in Text(program.program?.title ?? "") .frame(width: programWidth, alignment: .leading) .border(Color.black) } } } } .padding(.vertical, 4) } // 不同日期之间加分隔 Divider() .padding(.vertical, 8) } } .listStyle(.plain) }
注意事项
- 示例代码中所有
!强制解包建议替换为可选绑定,避免CoreData数据为空时崩溃 - 如果不需要横向滚动,去掉
ScrollView即可,节目过多时会自动压缩宽度或换行 - 可根据需要调整频道宽度、节目宽度、间距等布局参数
内容的提问来源于stack exchange,提问作者Joe Bell
相关产品推荐
相关产品推荐

