Swift自定义饼图-从多个UIBezier路径切割透明圆的奇怪行为

Swift自定义饼图-从多个UIBezier路径切割透明圆的奇怪行为,swift,uikit,Swift,Uikit,使用Swift创建自定义饼图/油炸圈饼样式图时,我在尝试从油炸圈饼上切出洞时遇到了一个奇怪的问题。我尝试了第二个UIBezierPath的圆心和半径的变化,但是我没有能够从圆心完成一个干净的孔。任何帮助都将不胜感激 UIView的子类: import UIKit public class DoughnutView: UIView { public var data: [Float]? { didSet { setNeedsDisplay() } } public var color

使用Swift创建自定义饼图/油炸圈饼样式图时,我在尝试从油炸圈饼上切出洞时遇到了一个奇怪的问题。我尝试了第二个UIBezierPath的圆心和半径的变化,但是我没有能够从圆心完成一个干净的孔。任何帮助都将不胜感激

UIView的子类:

import UIKit

public class DoughnutView: UIView {

public var data: [Float]? {
    didSet { setNeedsDisplay() }
}

public var colors: [UIColor]? {
    didSet { setNeedsDisplay() }
}

@IBInspectable public var spacerWidth: CGFloat = 2 {
    didSet { setNeedsDisplay() }
}

@IBInspectable public var thickness: CGFloat = 20 {
    didSet { setNeedsDisplay() }
}

public override func draw(_ rect: CGRect) {
    guard data != nil && colors != nil else {
        return
    }

    guard data?.count == colors?.count else {
        return
    }

    let center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
    let radius = min(bounds.size.width, bounds.size.height) / 2.0
    let total = data?.reduce(Float(0)) { $0 + $1 }
    var startAngle = CGFloat(Float.pi)

    UIColor.clear.setStroke()

    for (key, value) in data!.enumerated() {
        let endAngle = startAngle + CGFloat(2.0 * Float.pi) * CGFloat(value / total!)

        let doughnut = UIBezierPath()
        doughnut.move(to: center)
        doughnut.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

        let hole = UIBezierPath(arcCenter: center, radius: radius - thickness, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        hole.move(to: center)
        doughnut.append(hole)
        doughnut.usesEvenOddFillRule = true

        doughnut.close()
        doughnut.lineWidth = spacerWidth
        colors?[key].setFill()

        doughnut.fill()
        doughnut.stroke()

        startAngle = endAngle
    }
}

public override func layoutSubviews() {
    super.layoutSubviews()
    self.backgroundColor = UIColor.clear
    setNeedsDisplay()
}
}
然后是ViewController:

class ViewController: UIViewController {

@IBOutlet weak var doughnut: DoughnutView!

override func viewDidLoad() {
    super.viewDidLoad()

    doughnut.data = [3, 14, 5]
    doughnut.colors = [UIColor.red, UIColor.yellow, UIColor.blue]

    view.backgroundColor = .purple

}

}
结果是:

所以你想要这个:

问题是
addArc
创建的是弧,而不是楔块。也就是说,它创建了一条跟踪圆的一部分的路径,而没有径向线段进入或离开圆的中心。由于您没有仔细添加这些径向线段,因此在调用
close()
时,您会在不需要它们的地方得到直线

我猜您正试图通过
move(to:)
调用添加这些径向段,但您还没有完成所有必要的工作

无论如何,这可以做得更简单。从跟踪切片外边缘的圆弧开始,然后添加一条沿相反方向跟踪切片内边缘的圆弧
UIBezierPath
将自动用一条直线将第一条弧的末端连接到第二条弧的起点,而
close()
将用另一条直线将第二条弧的末端连接到第一条弧的起点。因此:

let slice = UIBezierPath()
slice.addArc(withCenter: center, radius: radius,
    startAngle: startAngle, endAngle: endAngle, clockwise: true)
slice.addArc(withCenter: center, radius: radius - thickness,
    startAngle: endAngle, endAngle: startAngle, clockwise: false)
slice.close()
也就是说,我们可以通过其他一些方式改进您的
draw(:)
方法:

  • 我们可以使用
    guard
    数据和
    颜色重新绑定到非可选项
  • 我们还可以
    保护
    数据不为空
  • 我们可以将
    半径
    减少
    间隔宽度
    ,以避免剪切笔划边界。(您将问题代码中的笔划颜色更改为
    .clear
    ,但图像显示为
    .white
  • 我们可以统一使用
    CGFloat
    来减少转换
  • 我们可以将
    total
    除以2π一次,而不是将每个
    value
    乘以2π
  • 我们可以
    将(颜色、数据)
    压缩成一个序列,而不是使用
    枚举()
    和订阅
    颜色
因此:


罗布,这是一个很好的回答。感谢您不仅为这个问题提供了一个漂亮的解决方案,还感谢您的解释,以及关于如何改进守卫声明、铸造和几何数学的建议。这是迄今为止我收到的关于StackOverflow的最好答案。
public override func draw(_ rect: CGRect) {
    guard
        let data = data, !data.isEmpty,
        let colors = colors, data.count == colors.count
        else { return }

    let center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
    let radius = min(bounds.size.width, bounds.size.height) / 2.0 - spacerWidth
    let total: CGFloat = data.reduce(0) { $0 + CGFloat($1) } / (2 * .pi)
    var startAngle = CGFloat.pi

    UIColor.white.setStroke()

    for (color, value) in zip(colors, data) {
        let endAngle = startAngle + CGFloat(value) / total

        let slice = UIBezierPath()
        slice.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        slice.addArc(withCenter: center, radius: radius - thickness, startAngle: endAngle, endAngle: startAngle, clockwise: false)
        slice.close()

        color.setFill()
        slice.fill()

        slice.lineWidth = spacerWidth
        slice.stroke()

        startAngle = endAngle
    }
}