Bài viết này sẽ giới thiệu cho các bạn cách tạo một biểu đồ đơn giản mà không cần sử dụng bất kỳ thư viện nào.

Bài viết sử dụng Swift 4.2 và XCode 10.1 cho dự án mẫu.

Tất nhiên, có khá nhiều thư viện để triển khai biểu đồ nhanh chóng và bạn có thể cân nhắc sử dụng một trong số chúng trước khi làm một cái biểu đồ theo ý thích.

Tuy nhiên, tự làm cũng không khó và chắc chắn sẽ giúp bạn học được nhiều hơn và nó cũng có một số ưu điểm như:

Thiết kế linh hoạt: Bạn có thể làm nó trông giống như bất kì thứ gì bạn muốn. Thư viện của bên thứ ba có thể không có biểu đồ trông giống như thiết kế mà công ty bạn muốn.

Không có mã dự phòng: Bởi vì bạn chỉ cần xây dựng những gì bạn cần.

Mục tiêu

Bài viết sẽ hướng dẫn bạn cách xây dựng một biểu đồ thanh đẹp như thế này:

Những gì bạn cần biết và các thành phần được sử dụng để xây dựng biểu đồ trên:

UIScrollView: để cho phép cuộn sang trái và phải
UIBezierPath: để tạo đường dẫn cho hình dạng
CALayer: để xây dựng các thanh hình chữ nhật
CAShapeLayer: để xây dựng các hình dạng giống như các thanh trong Biểu đồ 1: Biểu đồ thanh đẹp. Đây là một lớp rất tuyệt vời giúp bạn xây dựng tất cả các loại hình dạng 2D. Nó cũng có các chức năng bổ sung như màu tô và nét, mũ dòng, hoa văn và nhiều hơn nữa.
CATextLayer: để hiển thị văn bản

Bắt đầu nào!

Tôi sẽ chỉ cho bạn cách xây dựng một biểu đồ thanh với các thanh hình chữ nhật như hình dưới đây trước tiên. Sau đó, chúng ta sẽ thay đổi các thanh hình chữ nhật thành các thanh cong. Hãy để Lát gọi biểu đồ bên dưới Basic BarChart

BasicBarChart Class

Về cơ bản, BasicBarChart là một lớp con của UIView. Nó chứa một scrollView để hỗ trợ cuộn và scrollView chứa một thể hiện CALayer gọi là mainLayer. mainLayer chứa tất cả các lớp khác được tạo bởi biểu đồ và kích thước của mainLayer bằng với sizeSize của scrollView. Hình ảnh dưới đây thể hiện cách biểu đồ được hiển thị:

Chúng ta hãy xem định nghĩa của lớp BasicBarChart.

class BasicBarChart: UIView {
    
    /// the width of each bar
    let barWidth: CGFloat = 40.0
    
    /// space between each bar
    let space: CGFloat = 20.0
    
    /// space at the bottom of the bar to show the title
    private let bottomSpace: CGFloat = 40.0
    
    /// space at the top of each bar to show the value
    private let topSpace: CGFloat = 40.0
    
    /// contain all layers of the chart
    private let mainLayer: CALayer = CALayer()
    
    /// contain mainLayer to support scrolling
    private let scrollView: UIScrollView = UIScrollView()
    
    ...
}

Chúng ta có thể sử dụng CAScrollLayer để hỗ trợ cuộn, tuy nhiên ở đây tôi sử dụng UISCrollView vì nó dễ dàng hơn nhiều.

dataEntries property

BasicBarChart có một thuộc tính được gọi là dataEntries là tập hợp của BarEntry. Giao diện người dùng của biểu đồ sẽ được cập nhật khi dataEntries được đặt:

class BasicBarChart: UIView {

    ...
    
    var dataEntries: [BarEntry]? = nil {
        didSet {
            mainLayer.sublayers?.forEach({$0.removeFromSuperlayer()})
            
            if let dataEntries = dataEntries {
                scrollView.contentSize = CGSize(width: (barWidth + space)*CGFloat(dataEntries.count), height: self.frame.size.height)
                mainLayer.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
                
                drawHorizontalLines()
                
                for i in 0..<dataEntries.count {
                    showEntry(index: i, entry: dataEntries[i])
                }
            }
        }
    }
    
    ...
 }

BarEntry là một struct đơn giản đại diện cho dữ liệu của mỗi thanh:


struct BarEntry {
  let color: UIColor
    
  /// Ranged from 0.0 to 1.0
  let height: Float
    
  /// To be shown on top of the bar
  let textValue: String
    
  /// To be shown at the bottom of the bar
  let title: String
}

Bất cứ khi nào thuộc tính dataEntries được thiết lập, mainLayer sẽ clean mọi thứ và tạo lại bằng phương thức showEntry.

showEntry method

Phương thức showEntry chỉ cần tính toán vị trí x và y của thanh và sau đó gọi phương thức drawBar.

    private func showEntry(index: Int, entry: BarEntry) {
        /// Starting x postion of the bar
        let xPos: CGFloat = space + CGFloat(index) * (barWidth + space)
        
        /// Starting y postion of the bar
        let yPos: CGFloat = translateHeightValueToYPosition(value: entry.height)
        
        drawBar(xPos: xPos, yPos: yPos, color: entry.color)
        
        /// Draw text above the bar
        drawTextValue(xPos: xPos - space/2, yPos: yPos - 30, textValue: entry.textValue, color: entry.color)
        
        /// Draw text below the bar
        drawTitle(xPos: xPos - space/2, yPos: mainLayer.frame.height - bottomSpace + 10, title: entry.title, color: entry.color)
    }

drawBar method

Phương thức drawBar rất đơn giản, nó chỉ tính toán khung của barLayer, đặt màu nền và sau đó, thêm barLayer vào mainLayer.

private func drawBar(xPos: CGFloat, yPos: CGFloat, color: UIColor) {
 let barLayer = CALayer()
  barLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth, height: mainLayer.frame.height - bottomSpace - yPos)
  barLayer.backgroundColor = color.cgColor
  mainLayer.addSublayer(barLayer)
}

Các phương thức drawTextValue và drawTitle cũng khá đơn giản. 

    private func drawTextValue(xPos: CGFloat, yPos: CGFloat, textValue: String, color: UIColor) {
        let textLayer = CATextLayer()
        textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth+space, height: 22)
        textLayer.foregroundColor = color.cgColor
        textLayer.backgroundColor = UIColor.clear.cgColor
        textLayer.alignmentMode = kCAAlignmentCenter
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
        textLayer.fontSize = 14
        textLayer.string = textValue
        mainLayer.addSublayer(textLayer)
    }
    
    private func drawTitle(xPos: CGFloat, yPos: CGFloat, title: String, color: UIColor) {
        let textLayer = CATextLayer()
        textLayer.frame = CGRect(x: xPos, y: yPos, width: barWidth + space, height: 22)
        textLayer.foregroundColor = color.cgColor
        textLayer.backgroundColor = UIColor.clear.cgColor
        textLayer.alignmentMode = kCAAlignmentCenter
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil)
        textLayer.fontSize = 14
        textLayer.string = title
        mainLayer.addSublayer(textLayer)
    }

Horizontal lines

Trong biểu đồ thanh cơ bản, bạn có thể thấy rằng tôi có 3 đường ngang, một ở dưới cùng của biểu đồ, một ở đầu biểu đồ và một đường đứt nét ở giữa. Những dòng này có thể được tạo bằng CAShapeLayer:

let path = UIBezierPath()
path.move(to: CGPoint(x: xPos, y: yPos))
path.addLine(to: CGPoint(x: scrollView.frame.size.width, y: yPos))

let lineLayer = CAShapeLayer()
lineLayer.path = path.cgPath
lineLayer.lineWidth = 0.5
if lineInfo["dashed"] as! Bool {
  lineLayer.lineDashPattern = [4, 4]
}
lineLayer.strokeColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1).cgColor
self.layer.insertSublayer(lineLayer, at: 0)

Chỉ cần vẽ một đường dẫn thẳng bằng UIBezierPath, sau đó, gán đường dẫn cho thuộc tính đường dẫn của một cá thể CAShapeLayer. Có một thuộc tính rất hay của CAShapeLayer đó là lineDashPotype cho phép bạn tạo một đường đứt nét giống như đường kẻ mà tôi có ở giữa biểu đồ.

Bởi vì tôi muốn các dòng đó là tĩnh khi người dùng cuộn biểu đồ, vì vậy, tôi thêm chúng trực tiếp vào self.layer thay vì mainLayer.

Như vậy là chúng ta đã vẽ xong biểu đồ thanh cơ bản, các bạn có thể xem chi tiết hơn trong mã code ở cuối bài nhé!

Giờ hãy cùng xây dựng biểu đồ thanh cong

Class BasicBarChart là để vẽ ra biểu đồ thanh cơ bản, tôi sẽ tạo một class có tên là BeautifulBarChart để vẽ ra biểu đồ thanh cong. Sự khác biệt chính giữa 2 biểu đồ này là phương thức drawBar và drawTextValue. Các phương pháp và tính chất chỉ hơi khác nhau chút.

Đây là mã cho phương thức drawBar:

private func drawBar(xPos: CGFloat, yPos: CGFloat, height: CGFloat, color: UIColor) {
        let leftPath: UIBezierPath = UIBezierPath()
        leftPath.move(to: CGPoint(x: xPos, y: yPos))
        leftPath.addCurve(to: CGPoint(x: xPos+barWidth/2, y: yPos - height), controlPoint1: CGPoint(x: (xPos+barWidth/2), y: yPos), controlPoint2: CGPoint(x: xPos + barWidth*3/10, y: yPos - height))
        leftPath.addLine(to: CGPoint(x: xPos + barWidth/2, y: yPos))
        
        let leftLine = CAShapeLayer()
        leftLine.path = leftPath.cgPath
        leftLine.lineWidth = 0.0
        leftLine.fillColor = color.cgColor
        leftLine.strokeColor = color.cgColor
        
        let rightPath: UIBezierPath = UIBezierPath()
        rightPath.move(to: CGPoint(x: xPos+barWidth, y: yPos))
        rightPath.addCurve(to: CGPoint(x: xPos + barWidth/2, y: yPos-height), controlPoint1: CGPoint(x: xPos+barWidth/2, y: yPos), controlPoint2: CGPoint(x: xPos + barWidth*7/10, y: yPos-height))
        rightPath.addLine(to: CGPoint(x: xPos + barWidth/2, y: yPos))
        
        let rightLine = CAShapeLayer()
        rightLine.path = rightPath.cgPath
        rightLine.lineWidth = 0.0
        rightLine.fillColor = color.cgColor
        rightLine.strokeColor = color.cgColor
        
        mainLayer.addSublayer(leftLine)
        mainLayer.addSublayer(rightLine)
    }

Mỗi thanh được hình thành bởi 2 hình dạng. Hình ảnh dưới đây minh họa cách tạo hình bên trái bằng UIBezierPath để vẽ đường cong bezier:

Việc tạo ra đường bên phải tương tự như đường bên trái. Khi bạn có đường dẫn, một hình có thể được tạo bằng CAShapeLayer. Sau đó, chỉ cần thêm 2 hình dạng đó vào mainLayer. 

Và hiển thị 2 biểu đồ để chúng ta so sánh:

Các bạn có thể xem toàn bộ project ở đây.

Cảm ơn đã theo dõi bài viết này, các bạn có thể tìm hiểu nhiều hơn các bài viết về iOS tại blog của Techmaster, và các khóa học được xây dựng đáp ứng với xu thế công nghệ năm 2019

Techmaster có khóa học iOS Swift, React Native, Flutter ngắn hạn và dài hạn đảm bảo chất lượng đầu ra của học viên

Bài viết được sưu tầm