Swift UIViewで折れ線グラフを描画 (ライブラリ不要)

# UIViewで折れ線グラフを描画
外部ライブラリ(Chartsなど)を使用せず添付のような折れ線グラフを描画する実装の解説をします。
![Simulator Screen Shot - iPhone 14 Pro - 2023-03-19 at 13.42.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1008995/c4484de8-6669-43e0-7b84-0cbb4479420f.png)

## グラフ描画の実装
グラフ描画のサンプルコードです。
折れ線の描画は、データ点を結ぶように線を引くことで行います。以下が、折れ線の描画部分のコード例です。

```
class LineChartView: UIView {
    
    let margin: CGFloat = 30.0 // グラフの余白
    let lineWidth: CGFloat = 2.0 // 折れ線の太さ
    let lineColor: UIColor = .black // 折れ線の色
    
    var xData: [CGFloat] = [1, 2, 3, 4, 5] // x軸のデータ
    var yData: [CGFloat] = [3, 7, 2, 5, 9] // y軸のデータ
    
    override func draw(_ rect: CGRect) {
        let graphWidth = rect.width - margin * 2
        let graphHeight = rect.height - margin * 2
        
        // x軸、y軸の描画
        let context = UIGraphicsGetCurrentContext()!
        context.setStrokeColor(lineColor.cgColor)
        context.setLineWidth(lineWidth)
        context.move(to: CGPoint(x: margin, y: margin))
        context.addLine(to: CGPoint(x: margin, y: margin + graphHeight))
        context.addLine(to: CGPoint(x: margin + graphWidth, y: margin + graphHeight))
        context.strokePath()
        
        // データ点の描画
        for i in 0..<xData.count {
            let x = margin + CGFloat(i) / CGFloat(xData.count - 1) * graphWidth
            let y = margin + (1 - yData[i] / 10) * graphHeight
            context.addEllipse(in: CGRect(x: x - lineWidth, y: y - lineWidth, width: lineWidth * 2, height: lineWidth * 2))
            context.fillPath()
        }
        
        // 折れ線の描画
        context.move(to: CGPoint(x: margin, y: margin + (1 - yData[0] / 10) * graphHeight))
        for i in 1..<xData.count {
            let x = margin + CGFloat(i) / CGFloat(xData.count - 1) * graphWidth
            let y = margin + (1 - yData[i] / 10) * graphHeight
            context.addLine(to: CGPoint(x: x, y: y))
        }
        context.strokePath()
    }
}
```

このコードでは、折れ線の描画には、CGContextを使用しています。また、データ点は円で描画しています。

### x軸、y軸の描画の解説
```
        // x軸、y軸の描画
        let context = UIGraphicsGetCurrentContext()!
        context.setStrokeColor(lineColor.cgColor)
        context.setLineWidth(lineWidth)
        context.move(to: CGPoint(x: margin, y: margin))
        context.addLine(to: CGPoint(x: margin, y: margin + graphHeight))
        context.addLine(to: CGPoint(x: margin + graphWidth, y: margin + graphHeight))
        context.strokePath()
```
まず、context.move(to: CGPoint(x: margin, y: margin))を使用して、描画を開始する点を指定しています。この場合は、グラフの左下の端点です。

次に、context.addLine(to: CGPoint(x: margin, y: margin + graphHeight))を使用して、y軸を描画します。ここでは、開始点から、指定された座標まで直線を引いています。

同様に、context.addLine(to: CGPoint(x: margin + graphWidth, y: margin + graphHeight))を使用して、x軸を描画しています。

最後に、context.strokePath()を使用して、描画したパスを実際に描画します。これにより、グラフの枠が描画されます。

### データ点の描画の解説
```
        // データ点の描画
        for i in 0..<xData.count {
            let x = margin + CGFloat(i) / CGFloat(xData.count - 1) * graphWidth
            let y = margin + (1 - yData[i] / 10) * graphHeight
            context.addEllipse(in: CGRect(x: x - lineWidth, y: y - lineWidth, width: lineWidth * 2, height: lineWidth * 2))
            context.fillPath()
        }
```
forループの中で、iは現在処理されているデータポイントのインデックスです。xData.countはx軸に表示されるデータポイントの数を表し、データポイントの数に基づいてx座標を計算しています。

yData[i]は現在処理されているデータポイントの値であり、1 - yData[i] / 10を使用して、y座標を計算しています。ここで10は、y軸の最大値です。例えば、yData[i]が5の場合、y座標は1 - 5 / 10 = 0.5になります。

context.addEllipse(in: CGRect(x: x - lineWidth, y: y - lineWidth, width: lineWidth * 2, height: lineWidth * 2))は、データポイントを円で描画するために使用されます。ここで、CGRectは円を描画する矩形の座標とサイズを指定します。lineWidthは、円の輪郭の太さを表します。

最後に、context.fillPath()を使用して、実際に円を描画します。

### 折れ線の描画の解説
```
        // 折れ線の描画
        context.move(to: CGPoint(x: margin, y: margin + (1 - yData[0] / 10) * graphHeight))
        for i in 1..<xData.count {
            let x = margin + CGFloat(i) / CGFloat(xData.count - 1) * graphWidth
            let y = margin + (1 - yData[i] / 10) * graphHeight
            context.addLine(to: CGPoint(x: x, y: y))
        }
        context.strokePath()
```
最初に、context.move(to:)を使用して、描画開始位置を指定します。ここでは、左下隅の座標に最初のデータ点の高さを加えたものを使用しています。次に、forループで、すべてのデータ点に対して、context.addLine(to:)を使用して、折れ線のポイントを追加します。

addLine(to:)に渡される引数は、描画するラインの終点を指定する座標です。これらの座標は、x軸の座標はグラフの左端に沿って変化し、y軸の座標はそれぞれのデータポイントに対応する高さに変化します。ここでも、marginとgraphHeightを使用して、y座標を正しく計算しています。

最後に、strokePath()メソッドを使用して、パスを描画します。strokePath()メソッドは、CGContextで現在のパスを使用して線を描画するために使用されます。これにより、折れ線グラフの折れ線が描画されます。

## LineChartViewの使用
最後に、作成したLineChartViewを使用して、折れ線グラフを描画する方法を紹介します。以下が、ViewControllerクラスでLineChartViewを使用する例です。

```
class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let lineChartView = LineChartView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
        view.addSubview(lineChartView)
    }
}
```
LineChartViewを作成し、ViewControllerのViewに追加することで、折れ線グラフが描画されます。