使用UIBezierPath绘制图形曲线

20

我在我的应用程序中绘制图形。我的问题是,我想将连接顶点的直线绘制成曲线。目前,我使用UIBezierPath的函数addLineToPoint:来绘制它们。我想将它们绘制成曲线。我很清楚UIBezierPath有以下两个函数来支持这个特性。

三次曲线: addCurveToPoint:controlPoint1:controlPoint2: 二次曲线: addQuadCurveToPoint:controlPoint:

但问题是我没有控制点。我只有两个端点。我也没有找到一种确定控制点的方法/公式。有人能帮帮我吗?如果有人能提供一些替代方案,我将不胜感激...

10个回答

28

在Abid Hussain于2012年12月5日的答案上进行扩展。

我实现了代码,它可以工作,但结果看起来像这样:enter image description here

稍加调整,我就能得到我想要的结果:enter image description here

我所做的是将addQuadCurveToPoint添加到中点,使用中间点作为控制点。以下是基于Abid示例的代码;希望对某些人有所帮助。

+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    NSValue *value = points[0];
    CGPoint p1 = [value CGPointValue];
    [path moveToPoint:p1];

    if (points.count == 2) {
        value = points[1];
        CGPoint p2 = [value CGPointValue];
        [path addLineToPoint:p2];
        return path;
    }

    for (NSUInteger i = 1; i < points.count; i++) {
        value = points[i];
        CGPoint p2 = [value CGPointValue];

        CGPoint midPoint = midPointForPoints(p1, p2);
        [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
        [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];

        p1 = p2;
    }
    return path;
}

static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
    return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}

static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
    CGPoint controlPoint = midPointForPoints(p1, p2);
    CGFloat diffY = abs(p2.y - controlPoint.y);

    if (p1.y < p2.y)
        controlPoint.y += diffY;
    else if (p1.y > p2.y)
        controlPoint.y -= diffY;

    return controlPoint;
}

1
你的结果看起来不错。那时候我使用了https://github.com/amogh-talpallikar/smooth-curve-algorithm来绘制平滑的正弦曲线。虽然你的努力让人敬佩。 - Abid Hussain
如果您有连续减少的两个点,会是什么样子呢?例如,如果您的数据不是1、5、3、10,而是3、1、5、10,那么曲线是否能够正确地显示平滑增长呢? - Fogmeister
1
拖放解决方案!非常好的答案。 - Jacopo Penzo
我已经将你的代码转换为Swift 3,链接在这里:https://dev59.com/LmYr5IYBdhLWcg3wYZSD#43813458,它运行得非常好!感谢你提供的优秀代码。 - aBikis
1
所以Roman的立方曲线解决了阶梯问题,使拟合曲线更好。我还修补了它以处理可变的x增量。 - mackworth
显示剩余3条评论

26
使用@user1244109的答案,我在Swift 5中实现了更好的算法。 使用我的实现方式的图表
class GraphView: UIView {

    var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3] {
        didSet {
            setNeedsDisplay()
        }
    }

    func coordYFor(index: Int) -> CGFloat {
        return bounds.height - bounds.height * data[index] / (data.max() ?? 0)
    }

    override func draw(_ rect: CGRect) {

        let path = quadCurvedPath()

        UIColor.black.setStroke()
        path.lineWidth = 1
        path.stroke()
    }

    func quadCurvedPath() -> UIBezierPath {
        let path = UIBezierPath()
        let step = bounds.width / CGFloat(data.count - 1)

        var p1 = CGPoint(x: 0, y: coordYFor(index: 0))
        path.move(to: p1)

        drawPoint(point: p1, color: UIColor.red, radius: 3)

        if (data.count == 2) {
            path.addLine(to: CGPoint(x: step, y: coordYFor(index: 1)))
            return path
        }

        var oldControlP: CGPoint?

        for i in 1..<data.count {
            let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
            drawPoint(point: p2, color: UIColor.red, radius: 3)
            var p3: CGPoint?
            if i < data.count - 1 {
                p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
            }

            let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)

            path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)

            p1 = p2
            oldControlP = antipodalFor(point: newControlP, center: p2)
        }
        return path;
    }

    /// located on the opposite side from the center point
    func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
        guard let p1 = point, let center = center else {
            return nil
        }
        let newX = 2 * center.x - p1.x
        let diffY = abs(p1.y - center.y)
        let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)

        return CGPoint(x: newX, y: newY)
    }

    /// halfway of two points
    func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
    }

    /// Find controlPoint2 for addCurve
    /// - Parameters:
    ///   - p1: first point of curve
    ///   - p2: second point of curve whose control point we are looking for
    ///   - next: predicted next point which will use antipodal control point for finded
    func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
        guard let p3 = p3 else {
            return nil
        }

        let leftMidPoint  = midPointForPoints(p1: p1, p2: p2)
        let rightMidPoint = midPointForPoints(p1: p2, p2: p3)

        var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)

        if p1.y.between(a: p2.y, b: controlPoint.y) {
            controlPoint.y = p1.y
        } else if p2.y.between(a: p1.y, b: controlPoint.y) {
            controlPoint.y = p2.y
        }


        let imaginContol = antipodalFor(point: controlPoint, center: p2)!
        if p2.y.between(a: p3.y, b: imaginContol.y) {
            controlPoint.y = p2.y
        }
        if p3.y.between(a: p2.y, b: imaginContol.y) {
            let diffY = abs(p2.y - p3.y)
            controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
        }

        // make lines easier
        controlPoint.x += (p2.x - p1.x) * 0.1

        return controlPoint
    }

    func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
        let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
        color.setFill()
        ovalPath.fill()
    }

}

extension CGFloat {
    func between(a: CGFloat, b: CGFloat) -> Bool {
        return self >= Swift.min(a, b) && self <= Swift.max(a, b)
    }
}

@mackworth 嗯,看起来可以简化,至少在我的测试中是一样的。 - Daniel Ryan
1
这个答案很好用,到目前为止没有任何问题,不像我测试过的其他答案。 我甚至将其转换为SwiftUI,它也能正常工作。 - Daniel Ryan
1
@DanielRyan 你愿意在某个地方发布你的SwiftUI版本吗? - Gargoyle
如果我想要画一条接着一条的线,即每隔一秒钟添加一个值并用动画绘制线条...我该怎么做呢?我想要像这个链接中所示的那样进行操作:https://stackoverflow.com/questions/68131375/draw-line-graph-with-dynamic-values-objective-c-swift - Zღk
你们这样的人让 Stack Overflow 取得了成功。 - Eric
显示剩余3条评论

12
如果你只有两个点,那么你实际上无法将它们外推成曲线。
如果你有超过两个点(即曲线上的几个点),那么你可以粗略地绘制一条曲线来跟随它们。
如果你按照以下步骤进行操作...
好的,假设你有5个点。p1、p2、p3、p4和p5。 你需要计算出每对点之间的中点。 m1 = p1和p2的中点(以此类推)...
于是你有了m1、m2、m3和m4。
现在你可以将中点用作曲线各段的端点,将点用作二次曲线的控制点...
所以...
移动到m1点。 添加一个带有p2作为控制点的二次曲线到m2点。
添加一个带有p3作为控制点的二次曲线到m3点。
添加一个带有p4作为控制点的二次曲线到m4点。
以此类推...
这将让你得到曲线除了两端(我暂时记不起如何得到它们了,抱歉)。

但兄弟,如何处理增减效果呢?例如,点的y坐标顺序为y1=10,y2=15,y3=8。现在,曲线y1->y2应该在该条线的上方,而曲线y2->y3应该在该条线的下方。这意味着我们不能仅将线的中间点作为控制点,我们还需要向上/向下移动中间点的Y值。 - Abid Hussain
你说得对,这只是曲线的估计值。点越接近,曲线就越准确。此外,曲线梯度变化越陡峭,离实际点就越远。虽然如此,这仍然是获得相当准确估计的最快、最简单的方法。要获得更高的精度需要更困难的数学和更多的计算。在代码中推断曲线比用肉眼观察要困难得多:D - Fogmeister
你好@Fogmeister,我正在做与你上面建议的相同的事情,但是我的曲线不够平滑。这是链接https://dev59.com/a3rZa4cB1Zd3GeqP7dUj。有什么更好的方法吗?我请求你帮助我。等待你的回复。 - Ranjit

8

所以我发现了一种解决方法,基于@Fogmeister的回答。

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path setLineWidth:3.0];
    [path setLineCapStyle:kCGLineCapRound];
    [path setLineJoinStyle:kCGLineJoinRound];

    // actualPoints are my points array stored as NSValue

    NSValue *value = [actualPoints objectAtIndex:0];
    CGPoint p1 = [value CGPointValue];
    [path moveToPoint:p1];

    for (int k=1; k<[actualPoints count];k++) {

        NSValue *value = [actualPoints objectAtIndex:k];
        CGPoint p2 = [value CGPointValue];

        CGPoint centerPoint = CGPointMake((p1.x+p2.x)/2, (p1.y+p2.y)/2);

        // See if your curve is decreasing or increasing
        // You can optimize it further by finding point on normal of line passing through midpoint 

        if (p1.y<p2.y) {
             centerPoint = CGPointMake(centerPoint.x, centerPoint.y+(abs(p2.y-centerPoint.y)));
        }else if(p1.y>p2.y){
             centerPoint = CGPointMake(centerPoint.x, centerPoint.y-(abs(p2.y-centerPoint.y)));
        }

        [path addQuadCurveToPoint:p2 controlPoint:centerPoint];
        p1 = p2;
    }

    [path stroke];

5

我在这方面做了一些工作,并且从Catmull-Rom样条中得到了相当不错的结果。这里有一个方法,它接受一个CGPoint数组(存储为NSValues),并将线条添加到给定的视图中。

- (void)addBezierPathBetweenPoints:(NSArray *)points
                        toView:(UIView *)view
                     withColor:(UIColor *)color
                andStrokeWidth:(NSUInteger)strokeWidth
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    float granularity = 100;

    [path moveToPoint:[[points firstObject] CGPointValue]];

    for (int index = 1; index < points.count - 2 ; index++) {

        CGPoint point0 = [[points objectAtIndex:index - 1] CGPointValue];
        CGPoint point1 = [[points objectAtIndex:index] CGPointValue];
        CGPoint point2 = [[points objectAtIndex:index + 1] CGPointValue];
        CGPoint point3 = [[points objectAtIndex:index + 2] CGPointValue];

        for (int i = 1; i < granularity ; i++) {
            float t = (float) i * (1.0f / (float) granularity);
            float tt = t * t;
            float ttt = tt * t;

            CGPoint pi;
            pi.x = 0.5 * (2*point1.x+(point2.x-point0.x)*t + (2*point0.x-5*point1.x+4*point2.x-point3.x)*tt + (3*point1.x-point0.x-3*point2.x+point3.x)*ttt);
            pi.y = 0.5 * (2*point1.y+(point2.y-point0.y)*t + (2*point0.y-5*point1.y+4*point2.y-point3.y)*tt + (3*point1.y-point0.y-3*point2.y+point3.y)*ttt);

            if (pi.y > view.frame.size.height) {
                pi.y = view.frame.size.height;
            }
            else if (pi.y < 0){
                pi.y = 0;
            }

            if (pi.x > point0.x) {
                [path addLineToPoint:pi];
            }
        }

        [path addLineToPoint:point2];
    }

    [path addLineToPoint:[[points objectAtIndex:[points count] - 1] CGPointValue]];

    CAShapeLayer *shapeView = [[CAShapeLayer alloc] init];

    shapeView.path = [path CGPath];

    shapeView.strokeColor = color.CGColor;
    shapeView.fillColor = [UIColor clearColor].CGColor;
    shapeView.lineWidth = strokeWidth;
    [shapeView setLineCap:kCALineCapRound];

    [view.layer addSublayer:shapeView];
 }

我在这里使用它 https://github.com/johnyorke/JYGraphViewController


4

这里是@user1244109的答案转换为Swift 3:

private func quadCurvedPath(with points:[CGPoint]) -> UIBezierPath {
   let path = UIBezierPath()
   var p1 = points[0]

   path.move(to: p1)

   if points.count == 2 {
     path.addLine(to: points[1])
     return path
   }

   for i in 1..<points.count {
    let mid = midPoint(for: (p1, points[i]))
     path.addQuadCurve(to: mid,
                     controlPoint: controlPoint(for: (mid, p1)))
     path.addQuadCurve(to: points[i],
                    controlPoint: controlPoint(for: (mid, points[i])))
     p1 = points[i]
   }
  return path
}


private func midPoint(for points: (CGPoint, CGPoint)) -> CGPoint {
   return CGPoint(x: (points.0.x + points.1.x) / 2 , y: (points.0.y + points.1.y) / 2)
}


private func controlPoint(for points: (CGPoint, CGPoint)) -> CGPoint {
   var controlPoint = midPoint(for: points)
   let diffY = abs(points.1.y - controlPoint.y)

   if points.0.y < points.1.y {
     controlPoint.y += diffY
   } else if points.0.y > points.1.y {
     controlPoint.y -= diffY
   }
   return controlPoint
}

3

在分析@roman-filippov的代码时,我对代码进行了一些简化。下面是完整的Swift playground版本和ObjC版本。

我发现如果x增量不规则,则当点在x轴上靠近时,原始算法会创建一些不幸的逆行线。仅将控制点限制为不超过以下x值似乎可以解决该问题,尽管我没有数学理由,只是通过试验得出的结论。有两个标记为//**添加的部分实现了这个改变。

import UIKit
import PlaygroundSupport

infix operator °
func °(x: CGFloat, y: CGFloat) -> CGPoint {
    return CGPoint(x: x, y: y)
}

extension UIBezierPath {
    func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
        let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
        color.setFill()
        ovalPath.fill()
    }

    func drawWithLine (point: CGPoint, color: UIColor) {
        let startP = self.currentPoint
        self.addLine(to: point)
        drawPoint(point: point, color: color, radius: 3)
        self.move(to: startP)
    }

}

class TestView : UIView {
    var step: CGFloat = 1.0;
    var yMaximum: CGFloat = 1.0
    var xMaximum: CGFloat = 1.0
    var data: [CGPoint] = [] {
        didSet {
            xMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.x) })
            yMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.y) })
            setNeedsDisplay()
        }
    }

    func scale(point: CGPoint) -> CGPoint {
        return  CGPoint(x: bounds.width * point.x / xMaximum ,
                        y: (bounds.height - bounds.height * point.y / yMaximum))
    }

    override func draw(_ rect: CGRect) {
        if data.count <= 1 {
            return
        }
        let path = cubicCurvedPath()

        UIColor.black.setStroke()
        path.lineWidth = 1
        path.stroke()
    }

    func cubicCurvedPath() -> UIBezierPath {
        let path = UIBezierPath()

        var p1 = scale(point: data[0])
        path.drawPoint(point: p1, color: UIColor.red, radius: 3)
        path.move(to: p1)
        var oldControlP = p1

        for i in 0..<data.count {
            let p2 = scale(point:data[i])
            path.drawPoint(point: p2, color: UIColor.red, radius: 3)
            var p3: CGPoint? = nil
            if i < data.count - 1 {
                p3 = scale(point:data [i+1])
            }

            let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3)
            //uncomment the following four lines to graph control points
            //if let controlP = newControlP {
            //    path.drawWithLine(point:controlP, color: UIColor.blue)
            //}
            //path.drawWithLine(point:oldControlP, color: UIColor.gray)

            path.addCurve(to: p2, controlPoint1: oldControlP , controlPoint2: newControlP ?? p2)
            oldControlP = imaginFor(point1: newControlP, center: p2) ?? p1
            //***added to algorithm
            if let p3 = p3 {
                if oldControlP.x > p3.x { oldControlP.x = p3.x }
            }
            //***
            p1 = p2
        }
        return path;
    }

    func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? {
        //returns "mirror image" of point: the point that is symmetrical through center.
        //aka opposite of midpoint; returns the point whose midpoint with point1 is center)
        guard let p1 = point1, let center = center else {
            return nil
        }
        let newX = center.x + center.x - p1.x
        let newY = center.y + center.y - p1.y

        return CGPoint(x: newX, y: newY)
    }

    func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
    }

    func clamp(num: CGFloat, bounds1: CGFloat, bounds2: CGFloat) -> CGFloat {
        //ensure num is between bounds.
        if (bounds1 < bounds2) {
            return min(max(bounds1,num),bounds2);
        } else {
            return min(max(bounds2,num),bounds1);
        }
    }

    func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? {
        guard let p3 = p3 else {
            return nil
        }

        let leftMidPoint  = midPointForPoints(p1: p1, p2: p2)
        let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
        let imaginPoint = imaginFor(point1: rightMidPoint, center: p2)

        var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginPoint!)

        controlPoint.y = clamp(num: controlPoint.y, bounds1: p1.y, bounds2: p2.y)

        let flippedP3 = p2.y + (p2.y-p3.y)

        controlPoint.y = clamp(num: controlPoint.y, bounds1: p2.y, bounds2: flippedP3);
        //***added:
        controlPoint.x = clamp (num:controlPoint.x, bounds1: p1.x, bounds2: p2.x)
        //***
        // print ("p1: \(p1), p2: \(p2), p3: \(p3), LM:\(leftMidPoint), RM:\(rightMidPoint), IP:\(imaginPoint), fP3:\(flippedP3), CP:\(controlPoint)")
        return controlPoint
    }

}

let u = TestView(frame: CGRect(x: 0, y: 0, width: 700, height: 600));
u.backgroundColor = UIColor.white

PlaygroundPage.current.liveView = u
u.data = [0.5 ° 1, 1 ° 3, 2 ° 5, 4 ° 9, 8 ° 15, 9.4 ° 8, 9.5 ° 10, 12 ° 4, 13 ° 10, 15 ° 3, 16 ° 1]

使用 Objective-C 编写相同的代码(或者差不多相同)。这并不包括绘制例程本身,并且允许点数组包含缺失数据。

+ (UIBezierPath *)pathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) open {
    //based on Roman Filippov code: https://dev59.com/LmYr5IYBdhLWcg3wYZSD#40203583
    //open means allow gaps in path.
    UIBezierPath *path = [UIBezierPath bezierPath];
    CGPoint p1 = [points[0] CGPointValue];
    [path moveToPoint:p1];
    CGPoint oldControlPoint = p1;
    for (NSUInteger pointIndex = 1; pointIndex< points.count; pointIndex++) {
        CGPoint p2 = [points[pointIndex]  CGPointValue];  //note: mark missing data with CGFloatMax

        if (p1.y >= CGFloatMax || p2.y >= CGFloatMax) {
            if (open) {
                [path moveToPoint:p2];
            } else {
                [path addLineToPoint:p2];
            }
            oldControlPoint = p2;
        } else {
            CGPoint p3 = CGPointZero;
            if (pointIndex +1 < points.count) p3 = [points[pointIndex+1] CGPointValue] ;
            if (p3.y >= CGFloatMax) p3 = CGPointZero;
            CGPoint newControlPoint = controlPointForPoints2(p1, p2, p3);
            if (!CGPointEqualToPoint( newControlPoint, CGPointZero)) {
                [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: newControlPoint];
                oldControlPoint = imaginForPoints( newControlPoint,  p2);
                //**added to algorithm
                if (! CGPointEqualToPoint(p3,CGPointZero)) {
                   if (oldControlPoint.x > p3.x ) {
                       oldControlPoint.x = p3.x;
                   }
                //***
            } else {
                [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: p2];
                oldControlPoint = p2;
            }
        }
        p1 = p2;
    }
    return path;
}

static CGPoint imaginForPoints(CGPoint point, CGPoint center) {
    //returns "mirror image" of point: the point that is symmetrical through center.
    if (CGPointEqualToPoint(point, CGPointZero) || CGPointEqualToPoint(center, CGPointZero)) {
        return CGPointZero;
    }
    CGFloat newX = center.x + (center.x-point.x);
    CGFloat newY = center.y + (center.y-point.y);
    if (isinf(newY)) {
        newY = BEMNullGraphValue;
    }
    return CGPointMake(newX,newY);
}

static CGFloat clamp(CGFloat num, CGFloat bounds1, CGFloat bounds2) {
    //ensure num is between bounds.
    if (bounds1 < bounds2) {
        return MIN(MAX(bounds1,num),bounds2);
    } else {
        return MIN(MAX(bounds2,num),bounds1);
    }
}

static CGPoint controlPointForPoints2(CGPoint p1, CGPoint p2, CGPoint p3) {
    if (CGPointEqualToPoint(p3, CGPointZero)) return CGPointZero;
    CGPoint leftMidPoint = midPointForPoints(p1, p2);
    CGPoint rightMidPoint = midPointForPoints(p2, p3);
    CGPoint imaginPoint = imaginForPoints(rightMidPoint, p2);
    CGPoint controlPoint = midPointForPoints(leftMidPoint, imaginPoint);

    controlPoint.y = clamp(controlPoint.y, p1.y, p2.y);

    CGFloat flippedP3 = p2.y + (p2.y-p3.y);

    controlPoint.y = clamp(controlPoint.y, p2.y, flippedP3);
   //**added to algorithm
    controlPoint.x = clamp(controlPoint.x, p1.x, p2.x);
   //**
    return controlPoint;
}

3

如果你需要在每个点附近有“水平”曲线:

let path = createCurve(from: points, withSmoothness: 0.5) // 0.5 smoothness

enter image description here

let path = createCurve(from: points, withSmoothness: 0) // 0 smoothness

enter image description here

    /// Create UIBezierPath
    ///
    /// - Parameters:
    ///   - points: the points
    ///   - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
    private func createCurve(from points: [CGPoint], withSmoothness smoothness: CGFloat, addZeros: Bool = false) -> UIBezierPath {

        let path = UIBezierPath()
        guard points.count > 0 else { return path }
        var prevPoint: CGPoint = points.first!
        let interval = getXLineInterval()
        if addZeros {
            path.move(to: CGPoint(x: interval.origin.x, y: interval.origin.y))
            path.addLine(to: points[0])
        }
        else {
            path.move(to: points[0])
        }
        for i in 1..<points.count {
            let cp = controlPoints(p1: prevPoint, p2: points[i], smoothness: smoothness)
            path.addCurve(to: points[i], controlPoint1: cp.0, controlPoint2: cp.1)
            prevPoint = points[i]
        }
        if addZeros {
            path.addLine(to: CGPoint(x: prevPoint.x, y: interval.origin.y))
        }
        return path
    }

    /// Create control points with given smoothness
    ///
    /// - Parameters:
    ///   - p1: the first point
    ///   - p2: the second point
    ///   - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
    /// - Returns: two control points
    private func controlPoints(p1: CGPoint, p2: CGPoint, smoothness: CGFloat) -> (CGPoint, CGPoint) {
        let cp1: CGPoint!
        let cp2: CGPoint!
        let percent = min(1, max(0, smoothness))
        do {
            var cp = p2
            // Apply smoothness
            let x0 = max(p1.x, p2.x)
            let x1 = min(p1.x, p2.x)
            let x = x0 + (x1 - x0) * percent
            cp.x = x
            cp2 = cp
        }
        do {
            var cp = p1
            // Apply smoothness
            let x0 = min(p1.x, p2.x)
            let x1 = max(p1.x, p2.x)
            let x = x0 + (x1 - x0) * percent
            cp.x = x
            cp1 = cp
        }
        return (cp1, cp2)
    }

    /// Defines interval width, height (not used in this example) and coordinate of the first interval.
    /// - Returns: (x0, y0, step, height)
    internal func getXLineInterval() -> CGRect {
        return CGRect.zero
    }

getXLineInterval 方法是做什么用的? - Xernox
1
@Xernox 它可以返回 CGRect.zero。解决方案更加复杂,使用了 getXLineInterval 来布局带有自定义填充的 Ox 标签(与曲线点略有不同,但接近它们)。 - Alexander Volkov

3
一个好的解决方案是通过你的点创建一条直线UIBezierPath,然后使用样条曲线使线条弯曲。请查看这篇回答,其中提供了一个执行Catmull-Rom样条的UIBezierPath类别。Drawing Smooth Curves - Methods Needed

3
这个答案是 Roman Filippov 的答案的修改版,为了在 SwiftUI 上运行而进行了修改。在我测试的版本中,Roman 的版本是最好的,并且能够正常工作。
extension Path {
    
    /// This draws a curved path from a path of points given.
    /// Modified from https://dev59.com/LmYr5IYBdhLWcg3wYZSD
    mutating func quadCurvedPath(from data: [CGPoint]) {
        var prevousPoint: CGPoint = data.first!
        
        self.move(to: prevousPoint)
        
        if (data.count == 2) {
            self.addLine(to: data[1])
            return
        }
        
        var oldControlPoint: CGPoint?
        
        for i in 1..<data.count {
            let currentPoint = data[i]
            var nextPoint: CGPoint?
            if i < data.count - 1 {
                nextPoint = data[i + 1]
            }
            
            let newControlPoint = controlPointForPoints(p1: prevousPoint, p2: currentPoint, next: nextPoint)
            
            self.addCurve(to: currentPoint, control1: oldControlPoint ?? prevousPoint, control2: newControlPoint ?? currentPoint)
            
            prevousPoint = currentPoint
            oldControlPoint = antipodalFor(point: newControlPoint, center: currentPoint)
        }
    }
    
    /// Located on the opposite side from the center point
    func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
        guard let p1 = point, let center = center else {
            return nil
        }
        let newX = 2.0 * center.x - p1.x
        let newY = 2.0 * center.y - p1.y
        
        return CGPoint(x: newX, y: newY)
    }
    
    /// Find the mid point of two points
    func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
    }
    
    /// Find control point
    /// - Parameters:
    ///   - p1: first point of curve
    ///   - p2: second point of curve whose control point we are looking for
    ///   - next: predicted next point which will use antipodal control point for finded
    func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
        guard let p3 = p3 else {
            return nil
        }
        
        let leftMidPoint  = midPointForPoints(p1: p1, p2: p2)
        let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
        
        var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)
        
        if p1.y.between(a: p2.y, b: controlPoint.y) {
            controlPoint.y = p1.y
        } else if p2.y.between(a: p1.y, b: controlPoint.y) {
            controlPoint.y = p2.y
        }
        
        let imaginContol = antipodalFor(point: controlPoint, center: p2)!
        if p2.y.between(a: p3.y, b: imaginContol.y) {
            controlPoint.y = p2.y
        }
        if p3.y.between(a: p2.y, b: imaginContol.y) {
            let diffY = abs(p2.y - p3.y)
            controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
        }
        
        // make lines easier
        controlPoint.x += (p2.x - p1.x) * 0.1
        
        return controlPoint
    }
}

extension CGFloat {
    func between(a: CGFloat, b: CGFloat) -> Bool {
        return self >= Swift.min(a, b) && self <= Swift.max(a, b)
    }
}

接下来你可以在SwiftUI代码中像这样使用它:

    ZStack(alignment: .topLeading) {
        // Draw the line.
        Path { path in
            path.quadCurvedPath(from: linePoints)
        }.stroke(graphData.colour, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接