You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

关键优化点说明

  1. 数据计算与渲染分离:所有动态计算逻辑移到lineDrawData计算属性中,该属性仅在依赖参数(t、xStep、yStep等)变化时重新计算,不受body多次调用影响,彻底解决首次加载重复执行导致的数组异常。
  2. 移除let _ = 写法:不再在ViewBuilder闭包里做变量修改操作,所有状态更新都在计算属性中完成,符合SwiftUI的视图渲染范式。
  3. 安全初始化lastYPos:用ar.indices.map直接初始化数组,避免原代码中判断索引是否存在的冗余逻辑,同时防止数组越界。

内容的提问来源于stack exchange,提问作者Paul Martin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 03:35:15