I have creating a custom UIKit view that is a line chart view and I want to have the choice of adding a gradient color below the lines. I have this so far so I am very close but I don't want the path to close at the bottom of the chart I want the line to just stop at the last data point. This is the code so far:
class LineChartView: UIView {
var dataPoints: [CGFloat] = [] // Data points for your chart
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Set up the bezier path
let path = UIBezierPath()
// Calculate the gap between each point
let horizontalGap = rect.width / CGFloat(dataPoints.count - 1)
// Start from the first data point
var startPoint = CGPoint(x: 0, y: dataPoints[0])
path.move(to: startPoint)
// Connect the points with lines
for i in 0..<dataPoints.count {
let nextPoint = CGPoint(x: CGFloat(i) * horizontalGap, y: dataPoints[i])
path.addLine(to: nextPoint)
startPoint = nextPoint
}
// Add extra points to close the path and fill the area under the line
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
let customBlueColor = UIColor.blue.withAlphaComponent(0.5)
// Get the cgColor of the custom blue color
let customBlueCGColor = customBlueColor.cgColor
// Create a gradient layer
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
gradientLayer.colors = [customBlueCGColor, UIColor.clear.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0) // Start from the top
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0) // End at the bottom
// Create a shape layer for the gradient mask
let gradientMaskLayer = CAShapeLayer()
gradientMaskLayer.path = path.cgPath
// Add the gradient mask to the gradient layer
gradientLayer.mask = gradientMaskLayer
// Add the gradient layer to the view's layer
layer.addSublayer(gradientLayer)
// Create a shape layer for the line
let lineLayer = CAShapeLayer()
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.blue.cgColor // Set color
lineLayer.lineWidth = 3.0
lineLayer.lineCap = .round
lineLayer.lineJoin = .round
lineLayer.path = path.cgPath
// Add the line layer to the view's layer
layer.addSublayer(lineLayer)
// Draw X-axis
let xAxisLayer = CAShapeLayer()
xAxisLayer.strokeColor = UIColor.black.cgColor
xAxisLayer.lineWidth = 3.0
let xAxisPath = UIBezierPath()
xAxisPath.move(to: CGPoint(x: 0, y: bounds.height))
xAxisPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
xAxisLayer.path = xAxisPath.cgPath
layer.addSublayer(xAxisLayer)
// Draw Y-axis
let yAxisLayer = CAShapeLayer()
yAxisLayer.strokeColor = UIColor.black.cgColor
yAxisLayer.lineWidth = 3.0
let yAxisPath = UIBezierPath()
yAxisPath.move(to: CGPoint(x: 0, y: 0))
yAxisPath.addLine(to: CGPoint(x: 0, y: bounds.height))
yAxisLayer.path = yAxisPath.cgPath
layer.addSublayer(yAxisLayer)
}
}
Then if you want to put it in your view controller do the following:
class ViewController: UIViewController {
let randomNumbers: [CGFloat] = Array(repeating: 0.0, count: 20).map { _ in CGFloat.random(in: 1..<200) }
private lazy var lineChartView: LineChartView = {
let view = LineChartView()
view.dataPoints = randomNumbers
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(lineChartView)
configure()
}
private func configure() {
NSLayoutConstraint.activate([
lineChartView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
lineChartView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
lineChartView.heightAnchor.constraint(equalToConstant: 300),
lineChartView.widthAnchor.constraint(equalToConstant: 400)
])
}
}
First, don't do this in override func draw(_ rect: CGRect)
... that will be called many times, and you'll be adding many multiples of your shape layers.
Add the layers - and set their non-changing properties - when the view is initialized. Then define and set the paths and mask in layoutSubviews()
.
// for blue line
let linePath = UIBezierPath()
// for gradient filled area
let gradPath = UIBezierPath()
Build both paths with the same moveTo / lineTo code, but only add the last point and close the gradient path.
Actually, you don't need two paths... I was looking at it thinking you were drawing the shapes in draw()
, but you're using layers, so...
Define the line path and assign it to the line layer, then close it and assign it to the mask layer.
Here's your class with the above changes:
class LineChartView: UIView {
var dataPoints: [CGFloat] = [] // Data points for your chart
// Create a gradient layer
let gradientLayer = CAGradientLayer()
// Create a shape layer for the line
let lineLayer = CAShapeLayer()
let xAxisLayer = CAShapeLayer()
let yAxisLayer = CAShapeLayer()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
let customBlueColor = UIColor.blue.withAlphaComponent(0.5)
// Get the cgColor of the custom blue color
let customBlueCGColor = customBlueColor.cgColor
layer.addSublayer(gradientLayer)
layer.addSublayer(lineLayer)
layer.addSublayer(xAxisLayer)
layer.addSublayer(yAxisLayer)
// gradient layer properties
gradientLayer.colors = [customBlueCGColor, UIColor.clear.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0) // Start from the top
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0) // End at the bottom
// line layer properties
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = UIColor.blue.cgColor // Set color
lineLayer.lineWidth = 3.0
lineLayer.lineCap = .round
lineLayer.lineJoin = .round
yAxisLayer.strokeColor = UIColor.black.cgColor
yAxisLayer.lineWidth = 3.0
}
override func layoutSubviews() {
super.layoutSubviews()
let rect = bounds
// for both blue line and gradient mask
let linePath = UIBezierPath()
// Calculate the gap between each point
let horizontalGap = rect.width / CGFloat(dataPoints.count - 1)
// Start from the first data point
var startPoint = CGPoint(x: 0, y: dataPoints[0])
linePath.move(to: startPoint)
// Connect the points with lines
for i in 0..<dataPoints.count {
let nextPoint = CGPoint(x: CGFloat(i) * horizontalGap, y: dataPoints[i])
linePath.addLine(to: nextPoint)
startPoint = nextPoint
}
// set the line layer path
lineLayer.path = linePath.cgPath
// Now, add extra points to close the path and fill the area under the line
// but don't close the blue-line path
linePath.addLine(to: CGPoint(x: rect.width, y: rect.height))
linePath.addLine(to: CGPoint(x: 0, y: rect.height))
// Create a shape layer for the gradient mask
let gradientMaskLayer = CAShapeLayer()
gradientMaskLayer.path = linePath.cgPath
gradientLayer.frame = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
// Add the gradient mask to the gradient layer
gradientLayer.mask = gradientMaskLayer
// Draw X-axis
xAxisLayer.strokeColor = UIColor.black.cgColor
xAxisLayer.lineWidth = 3.0
let xAxisPath = UIBezierPath()
xAxisPath.move(to: CGPoint(x: 0, y: bounds.height))
xAxisPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
xAxisLayer.path = xAxisPath.cgPath
// Draw Y-axis
let yAxisPath = UIBezierPath()
yAxisPath.move(to: CGPoint(x: 0, y: 0))
yAxisPath.addLine(to: CGPoint(x: 0, y: bounds.height))
yAxisLayer.path = yAxisPath.cgPath
}
}
Now looks like this: