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

SwiftUI中Path绘制的自定义Shape无法正确填充问题求助

SwiftUI自定义Shape填充异常修复

我在SwiftUI中使用Path绘制了自定义Shape(HalfCirclesSidesLine),该Shape的描边(stroke)显示正常,但填充(fill)时无法达到预期效果。预览中描边表现正常,但填充结果异常,预期效果与当前效果可参考附图。

问题代码

struct HalfCirclesSidesLine: Shape {
    var circleDiameter: CGFloat = 10
    let distanceBetweenCircles: CGFloat = 10
    let cornerDistance: CGFloat = 22

    func path(in rect: CGRect) -> Path {
        var path = Path()

        // Drawing left side
        path = drawSide(path: path, rect: rect, isLeftSide: true)

        // Drawing right side
        path = drawSide(path: path, rect: rect, isLeftSide: false)

        path = drawCorners(on: path, rect: rect, radius: circleDiameter / 2)
        path = drawHorizontalLines(on: path, rect: rect, cornerDistance: cornerDistance)
        return path
    }

    func drawSide(path: Path, rect: CGRect, isLeftSide: Bool) -> Path {
        var newPath = path
        let lengthY = rect.height
        let circleCount = ((lengthY - cornerDistance * 2) / (circleDiameter + distanceBetweenCircles)).rounded(.towardZero)
        let radius = circleDiameter / 2
        let totalHeightForOneElement = distanceBetweenCircles + circleDiameter

        for i in 0..<Int(circleCount) {
            let th = totalHeightForOneElement * CGFloat(i)
            let xPosition = isLeftSide ? rect.minX : rect.maxX

            newPath.move(to: .init(x: xPosition, y: rect.minY + cornerDistance + th))
            newPath.addLine(to: .init(x: xPosition, y: rect.minY + distanceBetweenCircles + cornerDistance + th))

            let arcCenter = CGPoint(x: xPosition, y: rect.minY + distanceBetweenCircles + cornerDistance + radius + th)

            newPath.move(to: .init(x: xPosition, y: rect.minY + distanceBetweenCircles + cornerDistance + radius * 2 + th))
            newPath.addArc(center: arcCenter, radius: radius, startAngle: .degrees(90), endAngle: .degrees(-90), clockwise: isLeftSide)

        }

        newPath.move(to: .init(x: isLeftSide ? rect.minX : rect.maxX, y: rect.minY + cornerDistance + totalHeightForOneElement * circleCount))
        newPath.addLine(to: .init(x: isLeftSide ? rect.minX : rect.maxX, y: rect.maxY - cornerDistance))

        if isLeftSide {
            newPath.move(to: .init(x: rect.minX + cornerDistance, y: rect.maxY))
            newPath.addArc(center: .init(x: rect.minX, y: rect.maxY), radius: cornerDistance, startAngle: .degrees(0), endAngle: .degrees(270), clockwise: true)
        } else {
            newPath.move(to: .init(x: rect.maxX - cornerDistance, y: rect.maxY))
            newPath.addArc(center: .init(x: rect.maxX, y: rect.maxY), radius: cornerDistance, startAngle: .degrees(180), endAngle: .degrees(-90), clockwise: false)
        }
        return newPath
    }

    func drawCorners(on path: Path, rect: CGRect, radius: CGFloat) -> Path {
        var newPath = path

        // bottom left
        newPath.move(to: .init(x: rect.minX + cornerDistance, y: rect.maxY))
        newPath.addArc(center: .init(x: rect.minX, y: rect.maxY), radius: cornerDistance, startAngle: .degrees(0), endAngle: .degrees(270), clockwise: true)

        // top left
        newPath.move(to: .init(x: rect.minX + cornerDistance, y: rect.minY))
        newPath.addArc(center: .init(x: rect.minX, y: rect.minY), radius: cornerDistance, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: false)

        // bottom right
        newPath.move(to: .init(x: rect.maxX - cornerDistance, y: rect.maxY))
        newPath.addArc(center: .init(x: rect.maxX, y: rect.maxY), radius: cornerDistance, startAngle: .degrees(180), endAngle: .degrees(-90), clockwise: false)

        // top right
        newPath.move(to: .init(x: rect.maxX - cornerDistance, y: rect.minY))
        newPath.addArc(center: .init(x: rect.maxX, y: rect.minY), radius: cornerDistance, startAngle: .degrees(180), endAngle: .degrees(90), clockwise: true)
        return newPath
    }

    func drawHorizontalLines(on path: Path, rect: CGRect, cornerDistance: CGFloat) -> Path {
        var newPath = path
        // Line between top corners
        newPath.move(to: CGPoint(x: rect.minX + cornerDistance, y: rect.minY))
        newPath.addLine(to: CGPoint(x: rect.maxX - cornerDistance, y: rect.minY))

        // Line between bottom corners
        newPath.move(to: CGPoint(x: rect.minX + cornerDistance, y: rect.maxY))
        newPath.addLine(to: CGPoint(x: rect.maxX - cornerDistance, y: rect.maxY))
        return newPath
    }
}


struct DashedCircleLine_Previews: PreviewProvider {
    static var previews: some View {
        VStack(spacing: 40) {
            HalfCirclesSidesLine().fill(Color.red)
                .frame(width: 200, height: 200)
            HalfCirclesSidesLine()
                .stroke(lineWidth: 1)
                .frame(width: 200, height: 200)
        }
    }
}

问题原因

当前代码的核心问题是:使用了大量move(to:)指令,将整个图形拆分成了多个独立的子路径。描边可以正常渲染所有子路径,但填充操作需要识别连续闭合的路径区域,分散的子路径会导致填充逻辑错误,只会填充那些意外形成闭合的小区域,而非预期的整体区域。

修复方案

重构Path的构建逻辑,确保整个图形是单一的连续闭合路径,去掉不必要的move(to:),让线条、半圆、角落圆弧连贯衔接,形成完整的封闭回路。

修改后的完整代码

struct HalfCirclesSidesLine: Shape {
    var circleDiameter: CGFloat = 10
    let distanceBetweenCircles: CGFloat = 10
    let cornerDistance: CGFloat = 22

    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // 从左上角角落开始,构建连续路径
        path.move(to: CGPoint(x: rect.minX + cornerDistance, y: rect.minY))
        
        // 绘制顶部水平线
        path.addLine(to: CGPoint(x: rect.maxX - cornerDistance, y: rect.minY))
        
        // 绘制右上角圆弧
        path.addArc(center: CGPoint(x: rect.maxX, y: rect.minY), 
                    radius: cornerDistance, 
                    startAngle: .degrees(180), 
                    endAngle: .degrees(90), 
                    clockwise: true)
        
        // 绘制右侧边(含半圆)
        path = drawRightSide(path: path, rect: rect)
        
        // 绘制右下角圆弧
        path.addArc(center: CGPoint(x: rect.maxX, y: rect.maxY), 
                    radius: cornerDistance, 
                    startAngle: .degrees(90), 
                    endAngle: .degrees(0), 
                    clockwise: true)
        
        // 绘制底部水平线
        path.addLine(to: CGPoint(x: rect.minX + cornerDistance, y: rect.maxY))
        
        // 绘制左下角圆弧
        path.addArc(center: CGPoint(x: rect.minX, y: rect.maxY), 
                    radius: cornerDistance, 
                    startAngle: .degrees(0), 
                    endAngle: .degrees(270), 
                    clockwise: true)
        
        // 绘制左侧边(含半圆)
        path = drawLeftSide(path: path, rect: rect)
        
        // 闭合路径回到起点
        path.closeSubpath()
        
        return path
    }
    
    private func drawRightSide(path: Path, rect: CGRect) -> Path {
        var newPath = path
        let radius = circleDiameter / 2
        let totalHeightPerElement = circleDiameter + distanceBetweenCircles
        let availableHeight = rect.height - cornerDistance * 2
        let circleCount = Int((availableHeight / totalHeightPerElement).rounded(.towardZero))
        
        // 从右上角圆弧终点开始
        var currentY = rect.minY + cornerDistance
        
        for _ in 0..<circleCount {
            // 绘制垂直线段
            newPath.addLine(to: CGPoint(x: rect.maxX, y: currentY))
            currentY += distanceBetweenCircles
            
            // 绘制半圆(向右凸)
            let arcCenter = CGPoint(x: rect.maxX, y: currentY + radius)
            newPath.addArc(center: arcCenter, 
                          radius: radius, 
                          startAngle: .degrees(90), 
                          endAngle: .degrees(-90), 
                          clockwise: false)
            
            currentY += circleDiameter
        }
        
        // 绘制最后一段垂直线到右下角圆弧起点
        newPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerDistance))
        
        return newPath
    }
    
    private func drawLeftSide(path: Path, rect: CGRect) -> Path {
        var newPath = path
        let radius = circleDiameter / 2
        let totalHeightPerElement = circleDiameter + distanceBetweenCircles
        let availableHeight = rect.height - cornerDistance * 2
        let circleCount = Int((availableHeight / totalHeightPerElement).rounded(.towardZero))
        
        // 从左下角圆弧终点开始
        var currentY = rect.maxY - cornerDistance
        
        for _ in 0..<circleCount {
            // 绘制垂直线段
            newPath.addLine(to: CGPoint(x: rect.minX, y: currentY))
            currentY -= distanceBetweenCircles
            
            // 绘制半圆(向左凸)
            let arcCenter = CGPoint(x: rect.minX, y: currentY - radius)
            newPath.addArc(center: arcCenter, 
                          radius: radius, 
                          startAngle: .degrees(-90), 
                          endAngle: .degrees(90), 
                          clockwise: false)
            
            currentY -= circleDiameter
        }
        
        // 绘制最后一段垂直线到左上角起点
        newPath.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerDistance))
        
        return newPath
    }
}

struct DashedCircleLine_Previews: PreviewProvider {
    static var previews: some View {
        VStack(spacing: 40) {
            HalfCirclesSidesLine().fill(Color.red)
                .frame(width: 200, height: 200)
            HalfCirclesSidesLine()
                .stroke(lineWidth: 1)
                .frame(width: 200, height: 200)
        }
    }
}

关键修改说明

  1. 连续路径构建:从左上角起点开始,依次绘制顶部水平线、右上角圆弧、右侧边(含半圆)、右下角圆弧、底部水平线、左下角圆弧、左侧边(含半圆),最后闭合路径,形成完整的封闭回路。
  2. 移除多余move(to:):所有绘制操作都基于当前路径的终点,不再随意跳转位置,保证路径连贯性。
  3. 侧边半圆逻辑调整:左右侧边的半圆绘制时,直接衔接前后的垂直线段,无需额外移动路径起点。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 22:02:03