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

Swift中ContentView外更新变量并递归绘制谢尔宾斯基三角路径问题

问题描述
  • 业余学习Swift,此前仅使用命令行模板开发,不熟悉视图与图形绘制相关知识。
  • 学习《Recursive Book of Recursion》分形绘制章节时,尝试递归实现谢尔宾斯基三角,无法在ContentView内递归调用绘图函数,多次触发Cannot conform to View错误。
  • 当前采用全局数组listArray预生成20000条递归结果,再通过ForEach遍历绘制的临时方案,存在两个缺陷:一是方案不够优雅,二是无法根据最小可绘制三角尺寸自动终止(只能依赖固定次数)。
  • 不愿使用@State变量,因为不想每次重绘整个界面,希望保留已绘制线条,逐步生成更密集的图形。

现有代码

ContentView代码

struct ContentView: View {
    
    var ax = 50.0
    var ay = 50.0
    var bx = 350.0
    var by = 650.0
    var cx = 650.0
    var cy = 50.0
    
    var body: some View {
        ZStack {
            
            let temp = drawTriangle(ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy)
            
            ForEach(listArray, id: \.self) { item in
                Path() {path in
                    path.move(to: CGPoint(x: item[0],y: item[1]))
                    path.addLine(to: CGPoint(x: item[2],y:item[3]))
                    path.move(to: CGPoint(x: item[2],y:item[3]))
                    path.addLine(to: CGPoint(x: item[4],y:item[5]))
                    path.move(to: CGPoint(x:item[4],y:item[5]))
                    path.addLine(to: CGPoint(x:item[0],y:item[1]))
                
                }
                .stroke(Color.blue)
            }
            .frame(width: 700,height: 700)
        } 
    }
}

drawTriangle函数

func drawTriangle(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double)->[Double] {
    if counter > 20000 {return [ax,ay,bx,by,cx,cy]}
    if isTooSmall(ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy) {
        //base case
        return ([0,0,0,0,0,0])
    } else {
        //recursive case
        
        //calculate midpoints between points A,B,C
        let mid_ab = midpoint(startx: ax, starty: ay, endx: bx, endy: by)
        let mid_bc = midpoint(startx: bx, starty: by, endx: cx, endy: cy)
        let mid_ca = midpoint(startx: cx, starty: cy, endx: ax, endy: ay)
        //draw the tree inner triangles
        listArray.append(drawTriangle(ax: ax, ay: ay, bx: mid_ab.0, by: mid_ab.1, cx: mid_ca.0, cy: mid_ca.1))
        listArray.append(drawTriangle(ax: mid_ab.0, ay: mid_ab.1, bx: bx, by: by, cx: mid_bc.0, cy: mid_bc.1))
        listArray.append(drawTriangle(ax: mid_ca.0, ay: mid_ca.1, bx: mid_bc.0, by: mid_bc.1, cx: cx, cy: cy))
        //print(listArray)
        counter += 1
    }
    return ([ax,ay,bx,by,cx,cy])
}
var counter = 0
var listArray = [[Double]]()

辅助函数

let min_size = 4.0 //try changing this to decrease/increase the amount of recursion

func midpoint(startx:Double, starty:Double, endx:Double, endy:Double)->(Double, Double) {
    //return the x,y coordinate in the middle of the four given paramaters
    let xDiff = abs(startx - endx)
    let yDiff = abs(starty - endy)
    return (min(startx,endx) + (xDiff / 2.0), min(starty,endy)+(yDiff/2.0))
}

func isTooSmall(ax: Double,ay:Double,bx:Double,by:Double,cx:Double,cy:Double)->Bool {
    //determine if triangle is too small to draw
    let width = max(ax,bx,cx) - min(ax,bx,cx)
    let height = max(ay,by,cy) - min(ay,by,cy)
    return width < min_size || height < min_size
}

解决方案

核心思路

将递归生成路径的逻辑放到Path闭包内部执行,而非在View层级递归调用(这会触发View协议匹配错误)。同时通过控制递归深度,实现逐步绘制、保留已有线条的效果。

步骤1:重构递归路径生成函数

修改递归逻辑,直接在Path中添加子路径,无需依赖全局数组:

func sierpinskiTriangle(path: inout Path, ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double) {
    // 基准条件:三角过小则绘制当前三角并返回
    guard !isTooSmall(ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy) else {
        path.move(to: CGPoint(x: ax, y: ay))
        path.addLine(to: CGPoint(x: bx, y: by))
        path.addLine(to: CGPoint(x: cx, y: cy))
        path.closeSubpath()
        return
    }
    
    // 计算三个边的中点
    let midAB = midpoint(startx: ax, starty: ay, endx: bx, endy: by)
    let midBC = midpoint(startx: bx, starty: by, endx: cx, endy: cy)
    let midCA = midpoint(startx: cx, starty: cy, endx: ax, endy: ay)
    
    // 递归绘制三个子三角
    sierpinskiTriangle(path: &path, ax: ax, ay: ay, bx: midAB.0, by: midAB.1, cx: midCA.0, cy: midCA.1)
    sierpinskiTriangle(path: &path, ax: midAB.0, ay: midAB.1, bx: bx, by: by, cx: midBC.0, cy: midBC.1)
    sierpinskiTriangle(path: &path, ax: midCA.0, ay: midCA.1, bx: midBC.0, by: midBC.1, cx: cx, cy: cy)
}

步骤2:在ContentView中实现逐步绘制

通过@State控制递归深度,配合动画实现逐步生成密集图形的效果,每次仅新增线条而非重绘全部内容:

struct ContentView: View {
    let ax = 50.0, ay = 50.0
    let bx = 350.0, by = 650.0
    let cx = 650.0, cy = 50.0
    @State private var recursionDepth = 0
    let maxDepth = 6 // 根据min_size调整,控制最大递归层级
    
    var body: some View {
        ZStack {
            Path { path in
                // 根据当前深度生成对应层级的分形
                drawSierpinskiByDepth(path: &path, ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy, currentDepth: 0, targetDepth: recursionDepth)
            }
            .stroke(Color.blue)
            .frame(width: 700, height: 700)
            
            // 控制逐步绘制的按钮
            Button("增加细节") {
                withAnimation(.easeInOut(duration: 0.5)) {
                    if recursionDepth < maxDepth {
                        recursionDepth += 1
                    }
                }
            }
            .padding()
            .background(Color.white)
            .cornerRadius(8)
        }
    }
    
    // 带深度控制的递归函数,支持逐步绘制
    func drawSierpinskiByDepth(path: inout Path, ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double, currentDepth: Int, targetDepth: Int) {
        // 绘制当前层级的三角
        path.move(to: CGPoint(x: ax, y: ay))
        path.addLine(to: CGPoint(x: bx, y: by))
        path.addLine(to: CGPoint(x: cx, y: cy))
        path.closeSubpath()
        
        // 基准条件:达到目标深度或三角过小则停止递归
        guard currentDepth < targetDepth, !isTooSmall(ax: ax, ay: ay, bx: bx, by: by, cx: cx, cy: cy) else {
            return
        }
        
        let midAB = midpoint(startx: ax, starty: ay, endx: bx, endy: by)
        let midBC = midpoint(startx: bx, starty: by, endx: cx, endy: cy)
        let midCA = midpoint(startx: cx, starty: cy, endx: ax, endy: ay)
        
        drawSierpinskiByDepth(path: &path, ax: ax, ay: ay, bx: midAB.0, by: midAB.1, cx: midCA.0, cy: midCA.1, currentDepth: currentDepth + 1, targetDepth: targetDepth)
        drawSierpinskiByDepth(path: &path, ax: midAB.0, ay: midAB.1, bx: bx, by: by, cx: midBC.0, cy: midBC.1, currentDepth: currentDepth + 1, targetDepth: targetDepth)
        drawSierpinskiByDepth(path: &path, ax: midCA.0, ay: midCA.1, bx: midBC.0, by: midBC.1, cx: cx, cy: cy, currentDepth: currentDepth + 1, targetDepth: targetDepth)
    }
}

步骤3:复用原有辅助函数

原midpoint和isTooSmall函数可直接保留,仅优化midpoint的计算逻辑(更简洁):

let min_size = 4.0

func midpoint(startx: Double, starty: Double, endx: Double, endy: Double) -> (Double, Double) {
    return ((startx + endx) / 2.0, (starty + endy) / 2.0)
}

func isTooSmall(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double) -> Bool {
    let width = max(ax, bx, cx) - min(ax, bx, cx)
    let height = max(ay, by, cy) - min(ay, by, cy)
    return width < min_size || height < min_size
}

关键说明

  • 避免View层级递归:SwiftUI的View是结构体,递归返回View会触发类型不匹配错误,正确做法是在Path闭包内执行递归路径生成。
  • 无需全局数组:直接在Path中累积子路径,性能更优、逻辑更清晰。
  • 逐步绘制实现:通过@State控制递归深度,配合动画平滑过渡,每次仅新增当前层级的线条,保留已有绘制内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 07:27:03