ioscharts

How to draw Apple heath App like ring chart for iOS?


How could I customize the pie chart to draw the ring chart like below:

enter image description here

I gonna use pie chart from https://github.com/ChartsOrg/Charts, is it possible to achieve this? Or I've to draw this with CoreGraphics?

How should I deal with the gap and the start position?

import Charts

class ViewController: UIViewController {
    
    var pieChartView: PieChartView!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // 初始化 PieChartView
        pieChartView = PieChartView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
        pieChartView.center = self.view.center
        self.view.addSubview(pieChartView)
        
        // 设置环形图样式
        setupPieChart()
        setPieChartData()
    }
    
    func setupPieChart() {
        pieChartView.holeRadiusPercent = 0.7   // 中心空洞的大小
        pieChartView.transparentCircleRadiusPercent = 0.75   // 外层透明圆环的大小
        pieChartView.chartDescription?.enabled = false   // 关闭描述
        pieChartView.drawHoleEnabled = true   // 开启中心空洞
        pieChartView.rotationAngle = 270.0    // 旋转到顶部
        pieChartView.rotationEnabled = false  // 禁用旋转
        pieChartView.isUserInteractionEnabled = false    // 禁用用户交互
        pieChartView.legend.enabled = false   // 隐藏图例
    }
    
    func setPieChartData() {
        let entries = [
            PieChartDataEntry(value: 60), // 已使用部分
            PieChartDataEntry(value: 40)  // 剩余部分
        ]
        
        let dataSet = PieChartDataSet(entries: entries, label: nil)
        dataSet.colors = [UIColor.red, UIColor.lightGray]  // 设置不同部分的颜色
        dataSet.sliceSpace = 2  // 设置分片之间的间隙
        
        let data = PieChartData(dataSet: dataSet)
        pieChartView.data = data
        
        // 中心内容
        pieChartView.centerText = "1081"
        pieChartView.centerAttributedText = NSAttributedString(string: "还可以吃")
    }
}

This code seems not working as expected.

//Edit in 2024/10/12

finally, I finish it like this. leave it here, hope it will help someone.

#import "JFDiaryCalorieGrpahView.h"

@interface JFDiaryCalorieGrpahView ()
@property (nonatomic, strong) UIColor* trackTintColor;
@property (nonatomic, strong) UIColor* progressTintColor;

@property (nonatomic, assign) CGFloat insets;
@property (nonatomic, assign) CGFloat lineWidth;

@property (nonatomic, assign) CGFloat startAngle;
@property (nonatomic, assign) CGFloat endAngle;
@property (nonatomic, assign) CGFloat rotateAngle;
@property (nonatomic, assign) BOOL clockwise;
@end

@implementation JFDiaryCalorieGrpahView

-(void)setProgress:(CGFloat)progress {
    _progress = progress;
    [self setNeedsDisplay];
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:CGRectZero];
    self.backgroundColor = [UIColor clearColor];
    if (self) {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        _trackTintColor = NA_Color_Name(@"#FEE9E5");
        _progressTintColor = NA_Color_Name(@"#FF5753");
        
        _insets = 2;
        _lineWidth = 10;
        
        _startAngle = 0;
        _endAngle = 250;
        _rotateAngle = 216;
        _clockwise = YES;
    }
    return self;
}



-(void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);
    CGContextTranslateCTM(context, rect.size.width/2, rect.size.height/2);
    [self drawTrack:rect];
    [self drawProgress:rect];
    CGContextRestoreGState(context);
}

-(void)drawTrack:(CGRect)rect {
    CGFloat radius = MIN(rect.size.width, rect.size.height)/2 - self.lineWidth/2 - self.insets;
    CGFloat rotation = [self convetAngleToRadian:self.rotateAngle];
    CGFloat modifiedStart = [self convetAngleToRadian:self.startAngle] - rotation;
    CGFloat modifiedEnd = [self convetAngleToRadian:self.endAngle] - rotation;
    UIBezierPath* trackPath = [UIBezierPath bezierPathWithArcCenter:CGPointZero radius:radius startAngle:modifiedStart endAngle:modifiedEnd clockwise:self.clockwise];
    trackPath.lineWidth = self.lineWidth;
    trackPath.lineCapStyle = kCGLineCapRound;
    [self.trackTintColor setStroke];
    [trackPath stroke];
}

-(CGFloat)convetAngleToRadian:(CGFloat)angle {
    return angle / 180.0 * M_PI;
}

-(void)drawProgress:(CGRect)rect {
    if (self.progress == 0)  return;
    
    CGFloat radius = MIN(rect.size.width, rect.size.height)/2 - self.lineWidth/2 - self.insets;
    CGFloat rotation = [self convetAngleToRadian:self.rotateAngle];
    CGFloat modifiedStart = [self convetAngleToRadian:self.startAngle] - rotation;
    CGFloat deta = [self convetAngleToRadian:(self.endAngle - self.startAngle) * self.progress];
    CGFloat modifiedEnd = modifiedStart + deta;
    
    // Main arc with flat ends
    UIBezierPath* trackPath = [UIBezierPath bezierPathWithArcCenter:CGPointZero radius:radius startAngle:modifiedStart endAngle:modifiedEnd clockwise:self.clockwise];
    trackPath.lineWidth = self.lineWidth;
    trackPath.lineCapStyle = kCGLineCapButt; // Flat ends
    [self.progressTintColor setStroke];
    [trackPath stroke];
    
    // Add round cap at the start or end
    if (self.progress > 0) {
        CGPoint startCapCenter = CGPointMake(radius * cos(modifiedStart), radius * sin(modifiedStart));
        UIBezierPath* startCapPath = [UIBezierPath bezierPathWithArcCenter:startCapCenter radius:self.lineWidth/2 startAngle:0 endAngle:2 * M_PI clockwise:YES];
        [self.progressTintColor setFill];
        [startCapPath fill];
    }
    
    if (self.progress == 1) {
        CGPoint endCapCenter = CGPointMake(radius * cos(modifiedEnd), radius * sin(modifiedEnd));
        UIBezierPath* endCapPath = [UIBezierPath bezierPathWithArcCenter:endCapCenter radius:self.lineWidth/2 startAngle:0 endAngle:2 * M_PI clockwise:YES];
        [self.progressTintColor setFill];
        [endCapPath fill];
    }
}
@end

Solution

  • I think you don’t need the PieChart here, you can simply modified this Arc View

    import SwiftUI
    
    struct Arc: InsettableShape {
        let startAngle: Angle
        let endAngle: Angle
        let clockWise: Bool
        var insetAmount = 0.0
    
        func path(in rect: CGRect) -> Path {
            var path = Path()
            let rotationAdjustment = Angle.degrees(90)
            let modifiedStart = startAngle - rotationAdjustment
            let modifiedEnd = endAngle - rotationAdjustment
            path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2 - insetAmount, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockWise)
            return path
        }
    
        func inset(by amount: CGFloat) -> some InsettableShape {
            var arc = self
            arc.insetAmount += amount
            return arc
        }
    }
    
    struct ContentView: View {
        var body: some View {
            Arc(startAngle: .degrees(0), endAngle: .degrees(200), clockWise: true)
                .strokeBorder(.blue, lineWidth: 30)
        }
    }
    
    #Preview {
        ContentView()
    }