SwiftUI视图首次加载执行两次及代码写法优化问题
问题解决方案
问题1:首次加载视图代码执行两次导致lastYPos异常
SwiftUI 的body是可重计算的,首次渲染、布局调整、状态变化时都会触发多次调用。你原来在body的ForEach循环里动态修改lastYPos和ls数组,会因为多次执行导致数据重复修改,出现异常。
解决核心:把数据计算和视图渲染彻底分离,提前计算好所有需要绘制的线条参数,避免在body里做带副作用的操作。
问题2:let _ = 不良写法的替代
let _ = 是为了在ViewBuilder闭包里“绕过”语法限制(闭包要求内容是View类型,而赋值/修改操作是Void类型),本质是在视图渲染流程里混入了数据修改逻辑,不符合SwiftUI的设计范式。
解决核心:将所有数据计算逻辑移到ViewBuilder之外,只在body里做纯渲染操作。
重构后的完整代码示例
1. 定义绘制数据模型
先封装每条线条的所有绘制参数,避免在渲染时动态计算:
struct LineDrawData: Identifiable { let id = UUID() let xStart: CGFloat let yStart: CGFloat let xEnd: CGFloat let yEnd: CGFloat let color: Color let lineWidth: CGFloat let shouldDrawStartCircle: Bool }
2. 提前计算所有绘制数据
添加计算属性,一次性生成所有需要渲染的线条数据,确保计算逻辑只依赖输入参数,无副作用:
struct LinesView: View { @Environment(\.modelContext) private var modelContext var t: Time? var xStep: CGFloat var yStep: CGFloat // 假设isPrevShared是外部传入或可访问的状态,这里根据实际情况调整 var isPrevShared: (Bool, Int) = (false, 0) // 提前计算所有绘制数据 private var lineDrawData: [LineDrawData] { guard let t = t else { return [] } // 替换成你实际的es、ar获取逻辑 let es = [someClass]() let ar = [someClass]() var lines: [Line] = [] // 初始化lastYPos,避免越界判断 var lastYPos: [CGFloat] = ar.indices.map { yStep * CGFloat($0 + 1) } var drawData: [LineDrawData] = [] for e in es { let xPos = xStep * CGFloat(e.position) for aIndex in ar.indices { let arc = ar[aIndex] let yPos = yStep * CGFloat(aIndex + 1) guard e.position > 1 else { continue } // 确定线条起点y坐标 let yStart: CGFloat = e.position == 2 ? yStep * CGFloat(isPrevShared.1 + 1) : lastYPos[aIndex] // 计算重叠线条数量 let overlappingLines = lines.filter { ($0.start == CGPoint(x: xPos - xStep, y: yStart) && $0.end == CGPoint(x: xPos, y: yPos)) || ($0.start == CGPoint(x: xPos, y: yPos) && $0.end == CGPoint(x: xPos - xStep, y: yStart)) }.count // 计算线条宽度 let width = 40 let lineWidth = yStart == yEnd ? CGFloat(width) / CGFloat(overlappingLines + 1) : CGFloat(width) / CGFloat(overlappingLines + 2) let color = Color(NSColor(hex: arc.color)) let shouldDrawStartCircle = yStart != yEnd || e.position == 2 // 添加到绘制数据数组 drawData.append(LineDrawData( xStart: xPos - xStep, yStart: yStart, xEnd: xPos, yEnd: yPos, color: color, lineWidth: lineWidth, shouldDrawStartCircle: shouldDrawStartCircle )) // 更新线条记录和lastYPos lines.append(Line(start: CGPoint(x: xPos - xStep, y: yStart), end: CGPoint(x: xPos, y: yPos))) lastYPos[aIndex] = yPos } } return drawData } var body: some View { ZStack { // 直接渲染提前计算好的数据 ForEach(lineDrawData) { data in Canvas(rendersAsynchronously: false) { context, size in // 绘制起点圆(如果需要) if data.shouldDrawStartCircle { context.fill( Path(ellipseIn: CGRect( origin: CGPoint(x: data.xStart - data.lineWidth/2, y: data.yStart - data.lineWidth/2), size: CGSize(width: data.lineWidth, height: data.lineWidth) )), with: .color(data.color) ) } // 绘制终点圆 context.fill( Path(ellipseIn: CGRect( origin: CGPoint(x: data.xEnd - data.lineWidth/2, y: data.yEnd - data.lineWidth/2), size: CGSize(width: data.lineWidth, height: data.lineWidth) )), with: .color(data.color) ) // 绘制线条 context.stroke(Path { path in path.move(to: CGPoint(x: data.xStart, y: data.yStart)) path.addLine(to: CGPoint(x: data.xEnd, y: data.yEnd)) }, with: .color(data.color), lineWidth: data.lineWidth) } } } .opacity(t != nil ? 1 : 0) // t为空时隐藏视图 } } // 保留你原有的Line定义 struct Line { let start: CGPoint let end: CGPoint }
关键优化点说明
- 数据计算与渲染分离:所有动态计算逻辑移到
lineDrawData计算属性中,该属性仅在依赖参数(t、xStep、yStep等)变化时重新计算,不受body多次调用影响,彻底解决首次加载重复执行导致的数组异常。 - 移除
let _ =写法:不再在ViewBuilder闭包里做变量修改操作,所有状态更新都在计算属性中完成,符合SwiftUI的视图渲染范式。 - 安全初始化
lastYPos:用ar.indices.map直接初始化数组,避免原代码中判断索引是否存在的冗余逻辑,同时防止数组越界。
内容的提问来源于stack exchange,提问作者Paul Martin
相关产品推荐
相关产品推荐

