SwiftUI中如何为比赛结果列表添加条件行号(含DNF处理)
问题:比赛结果列表行号添加(DNF选手不编号)的实现问题
我想给比赛结果列表添加行号,要求未完赛(DNF,通过dateTime == 999.999判断)的选手不显示行号,当前所有选手都显示为第1名。
我尝试用ObservableObject维护一个计数器,代码如下:
全局状态定义
class GlobalSettings: ObservableObject { @Published var counter: Int = 0 }
列表调用代码
Section { List { ForEach(entrants) { entrant in ResultsCell(entrant: entrant) } } }
列表行实现
struct ResultsCell: View { let entrant: EntrantsDatabase1 @EnvironmentObject var globalSettings: GlobalSettings var body: some View { HStack { Spacer() .frame(width: 5) if entrant.dateTime != 999.999 { globalSettings.counter += 1 Text("\(globalSettings.counter).") .frame(width: 20, height: 40, alignment: .leading) .bold() } else { Text("-") .frame(width: 20, height: 40, alignment: .leading) .bold() } Spacer() .frame(width: 5) CircledText(text: String(entrant.number)) .frame(width: 48, height: 40, alignment: .leading) .bold() Spacer() .frame(width: 10) Text(entrant.name) .frame(width: 160, height: 40, alignment: .leading) .bold() Spacer() .frame(width: 10) if entrant.dateTime != 999.999 { Text("\(entrant.dateTime, specifier: \"%.3f\")") .frame(width: 70, height: 40, alignment: .trailing) .bold() } else { Text("DNF") .frame(width: 70, height: 40, alignment: .trailing) .bold() } } .listRowBackground(entrant.dateTime == 999.999 ? Color.red: Color(.tertiarySystemBackground)) } }
运行时出现编译错误:
'buildExpression' is unavailable: this expression does not conform to 'View'
请问这个实现方案是否可行?
解答
这个方案不可行,核心原因是违反了SwiftUI的声明式设计原则:
SwiftUI的View.body是用来描述UI结构的,不能在其中直接执行状态修改这类副作用代码(比如globalSettings.counter += 1)。每次视图刷新都会重新执行body中的代码,这种写法不仅会触发编译错误,还可能导致无限循环、计数混乱等不可预测的行为。
正确实现方式:提前计算行号
不需要全局计数器,而是提前为每个完赛选手计算好对应的行号,有两种常用方式:
方式1:遍历实时计算(适合小规模数据)
在父视图的ForEach中,实时统计当前选手之前的完赛数量作为行号:
Section { List { // 用enumerated()获取索引,同时遍历 ForEach(Array(entrants.enumerated()), id: \.element.id) { index, entrant in // 统计从开头到当前索引中完赛的选手数量 let rank = entrants[0...index].filter { $0.dateTime != 999.999 }.count ResultsCell(entrant: entrant, rank: rank) } } }
修改ResultsCell,去掉EnvironmentObject,直接接收计算好的行号:
struct ResultsCell: View { let entrant: EntrantsDatabase1 let rank: Int // 直接传入行号 var body: some View { HStack { Spacer().frame(width: 5) if entrant.dateTime != 999.999 { Text("\(rank).") .frame(width: 20, height: 40, alignment: .leading) .bold() } else { Text("-") .frame(width: 20, height: 40, alignment: .leading) .bold() } // 剩余UI代码保持不变 Spacer().frame(width: 5) CircledText(text: String(entrant.number)) .frame(width: 48, height: 40, alignment: .leading) .bold() Spacer().frame(width: 10) Text(entrant.name) .frame(width: 160, height: 40, alignment: .leading) .bold() Spacer().frame(width: 10) if entrant.dateTime != 999.999 { Text("\(entrant.dateTime, specifier: \"%.3f\")") .frame(width: 70, height: 40, alignment: .trailing) .bold() } else { Text("DNF") .frame(width: 70, height: 40, alignment: .trailing) .bold() } } .listRowBackground(entrant.dateTime == 999.999 ? Color.red: Color(.tertiarySystemBackground)) } }
方式2:预处理数据(适合大规模数据,性能更优)
在ViewModel或父视图中提前遍历数据,为每个选手计算好行号,避免每次渲染重复计算:
// 在父视图/ViewModel中定义预处理后的数组 var rankedEntrants: [(entrant: EntrantsDatabase1, rank: Int)] { var currentRank = 0 return entrants.map { entrant in if entrant.dateTime != 999.999 { currentRank += 1 return (entrant, currentRank) } else { return (entrant, 0) // 0代表无行号 } } }
然后在列表中使用预处理后的数组:
Section { List { ForEach(rankedEntrants, id: \.entrant.id) { item in ResultsCell(entrant: item.entrant, rank: item.rank) } } }
ResultsCell的修改和方式1一致。
内容的提问来源于stack exchange,提问作者Edward Hasted
相关产品推荐
相关产品推荐

