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
相关产品推荐
相关产品推荐

