1. Giới thiệu

Chúng ta gặp rất nhiều ứng dụng có rất nhiều UI bắt mắt, như các ứng dụng thống kê chẳng hạn, rất nhiều biểu đồ với hình thù đa dạng. Đã bao giờ bạn hỏi các ứng dụng đó được làm thế nào chưa? Và một điều nữa đó là, UIKit của Apple không cung cấp cho chúng ta bất cứ một UI control sẵn nào để làm điều đó. Để làm được điều đó (ví dụ hiện thi một biểu đồ thống kê hình tròn) chúng ta cần sử dụng một framework khác: Core Graphic. Mọi thứ chúng ta nhìn được, đều được vẽ lên bằng code. Bên dưới là một ví dụ tiêu biểu cho việc sử dụng Core Graphic để vẽ đồ thì hình tròn.

Tuy nhiên ở bài này, chúng ta chưa cần phải làm những thứ đao to búa lớn gì, bài này mình sẽ giới thiệu các bạn cách vẽ các hình cơ bản trước.

  • Vẽ đường thẳng, gấp khúc
  • Vẽ hình chữ nhật
  • Vẽ hình tam giác
  • Vẽ gradient
    Tài liệu về Core Graphic iOS các bạn có thể tham khảo link sau: https://developer.apple.com/library/content/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
    Phần này có đoạn khác hay miểu tả về góc toạ độ của UI và Core Graphic. Nó có nói là gốc toạ độ của UI là góc top-left (như chúng ta đã biết), còn đối với core graphic thì gốc toạ độ là góc bottom-left. Trong bài này mình sẽ gọi code vẽ ở tầng UI do đó, mặc dù vẫn sử dụng core graphic để vẽ nhưng gốc toạ độ luôn là top-left. Trường hợp các bạn vẽ trên tầng layer (CALayer) thì gốc toạ độ sẽ phải tính ở bottom-left.
    Để thự hiện việc vẽ trên tầng View, các bạn cấn override lại hàm override func draw(_ rect: CGRect) của một UIView.
override func draw(_ rect: CGRect) {
        super.draw(rect)
        // gọi code vẽ ở đây
    }

2. Vẽ đường thẳng, gấp khúc.

private func drawLine(lineColor: UIColor, lineHeight: CGFloat, points: [CGPoint]) {
        if points.count > 1, let context = UIGraphicsGetCurrentContext() {
            context.saveGState()
            context.setShouldAntialias(true)
            context.setAllowsAntialiasing(true)
            context.setStrokeColor(lineColor.cgColor)
            context.setLineWidth(lineHeight)
            context.move(to: points[0])
            for pot in points {
                context.addLine(to: pot)
            }
            context.strokePath()
            context.restoreGState()
        }
    }

Phần trên là hàm mình dùng để vẽ đường thẳng, gấp khúc. Để vẽ đường thẳng, gấp khúc ta cần xác định tập hợp các điểm points: [CGPoint] , và nối các điểm lại với nhau thông qua hàm context.addLine(to: pot). Có một điểu mình cần lưu ý các bạn, trước khi bắt đầu vẽ bất cứ cái gì các bạn lưu lại State hiện tại của context thông qua hàm context.saveGState(). Sau khi vẽ xong xuôi, chúng ta cần trả lại State trước đó của context. Hiểu nôm na, context giống như cái bút vẽ, mỗi lần vẽ là chúng ta đã nhúng bút vào rất nhiều hộp màu, bút đã bị bẩn, vậy nên cần phải làm sạch bút trước khi bắt đầu lần vẽ tiếp theo. Tóm lại code vẽ của các bạn sẽ có cấu trúc sau:

 func ham(...) {
       context.saveGState()
       // Code vẽ ở đây ...
       context.restoreGState()
 }

3 . Vẽ hình chữ nhật

private func drawRectangle(color: UIColor, rect: CGRect) {
        if let context = UIGraphicsGetCurrentContext() {
            context.saveGState()
            context.setFillColor(color.cgColor)
            context.fill(rect)
            context.restoreGState()
        }
    }

Đơn giản chúng ta cần xác định 4 góc của hình chữ nhật thông qua CGRect. Hàm context.fill(rect) có chức năng bôi toàn bộ rect với mầu hiện tại của context.

4. Vẽ Gradient

 private func drawGradient(rect: CGRect, start: UIColor, end: UIColor, startPoint: CGPoint, endPoint: CGPoint ) {
        if let context = UIGraphicsGetCurrentContext() {
            context.saveGState()
            context.clip(to: rect)
            let colors = [start.cgColor, endColor.cgColor]
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let colorLocations: [CGFloat] = [0, 1.0]
            let gradient = CGGradient(colorsSpace: colorSpace,
                                      colors: colors as CFArray,
                                      locations: colorLocations)!
            
            context.drawLinearGradient(gradient,
                                       start: startPoint,
                                       end: endPoint,
                                       options: )
            context.restoreGState()
        }
    }

Để vẽ được một hình chữ nhật gradient, chúng ta cần xác định màu bắt đầu, màu kết thúc hàm trên được thể hiện đoạn code let colors = [start.cgColor, endColor.cgColor]. Vấn để tiếp theo chúng ta cần xác định đó là chiều thay đổi của màu xác: màu có thể thay đổi từ trái sang phải, hoặc từ trên xuống dưới hình chữ nhật. startPoint: CGPoint, endPoint: CGPoint sẽ quy định điều đó. Ví dụ:

let startPoint =  CGPoint(x: 0, y: bounds.height) // góc trái dưới hình chữ nhật
let endPoint = CGPoint(x: bounds.width, y: bounds.height) // góc phải dưới hình chữ nhật

Thì màu xác sẽ thay đổi theo chiều từ trái sang phải, trùng với chiều nối từ startPoint đến endPoint.
Vậy ta cần lấy của một điểm nằm giữa điểm đầu và điểm cuối thì làm thế nào. Như đã biết, sự thay đổi đổi màu xác của gradient tuyên theo hàm một hàm bấc nhất. Dự vào đó ta có thể xác định được màu của điểm nằm giữa.

private func getGradientColorAtPoint(point: CGPoint) -> UIColor {
        let percent = point.x / bounds.width
        let resultRed = startColor.components.red + percent * (endColor.components.red - startColor.components.red)
        let resultGreen = startColor.components.green + percent * (endColor.components.green - startColor.components.green)
        let resultBlue = startColor.components.blue + percent * (endColor.components.blue - startColor.components.blue)
        return UIColor(red: resultRed, green: resultGreen, blue: resultBlue, alpha: 1.0)
    }

5. Vẽ hình tam giác

private func drawTriangle(color: UIColor) {
        if let context = UIGraphicsGetCurrentContext() {
            context.saveGState()
            let  startX = selectedPositionX - transparentHeight/2
            let startY = bounds.height - transparentHeight
            context.move(to: CGPoint(x: startX, y: startY))
            let secondY = transparentHeight / sqrt(2) + selectedPositionY
            context.addLine(to: CGPoint(x: selectedPositionX, y: secondY))
            let thirdX = startX + transparentHeight
            context.addLine(to: CGPoint(x: thirdX, y: selectedPositionY))
            context.closePath()
            context.setFillColor(color.cgColor)
            context.fillPath()
            context.restoreGState()
        }
    }

Xác định 3 điểm của tam giác, nối lại thông qua hàm context.movecontext.addLine và kết thúc bằng hàm closePath(). Ta đã có một path đóng kín, khí đó gọi hàm context.fillPath() để bôi màu hiện tại vào path vừa tạo được. Chúng ta có được hình tam giác mình cần.
Full source code mình để ở https://github.com/tungvt01/drawIOS. Các bạn lấy về chạy thành công sẽ thu được như bên dưới

Phần tới chúng ta sẽ vẽ nhưng hình phức tạp hơn và vẽ trên tầng CALayer.