draw(_:) và drawRect — khái niệm, gọi và ghi đè

Tác giả: IT Sectr Đã đăng: 2026-07-20 Thời gian đọc: 7 phút

draw(_:) / drawRect là một phương thức của lớp UIView (trong Swift) và tương ứng trong Objective-C (drawRect:), chịu trách nhiệm hiển thị nội dung của view bằng Core Graphics. Hệ thống tự động gọi phương thức này khi view xuất hiện lần đầu trên màn hình và sau khi gọi setNeedsDisplay(). Theo Tài liệu Apple (2025), draw(_:) là phương thức duy nhất mà nhà phát triển có quyền truy cập vào ngữ cảnh đồ họa của màn hình hiện tại để vẽ tùy chỉnh. Ghi đè draw(_:) mang lại toàn quyền kiểm soát giao diện của thành phần — từ các hình dạng hình học đơn giản đến đồ họa hoạt ảnh phức tạp.

Những điểm chính

  • draw(_:) — phương thức UIView để vẽ tùy chỉnh qua Core Graphics, không được gọi trực tiếp bởi nhà phát triển
  • drawRect: — tương đương với draw(_:) trong Objective-C, khác về cú pháp, giống về chức năng
  • CGContext — ngữ cảnh đồ họa có sẵn bên trong draw(_:) cho tất cả các thao tác vẽ
  • setNeedsDisplay() — cách duy nhất đúng để yêu cầu vẽ lại draw(_:) từ phía nhà phát triển
  • UIGraphicsGetCurrentContext() — hàm lấy ngữ cảnh hiện tại bên trong draw(_:), bước bắt buộc trước khi vẽ

draw(_:) / drawRect là gì

draw(_:) là một phương thức instance của UIView mà UIKit gọi để hiển thị nội dung của view. Bên trong phương thức này, nhà phát triển có quyền truy cập vào ngữ cảnh đồ họa CGContext và sử dụng API Core Graphics để vẽ đường thẳng, tô màu, văn bản và hình ảnh. drawRect: trong Objective-C thực hiện cùng chức năng nhưng với cú pháp khác: tham số duy nhất là một CGRect chỉ định khu vực cần vẽ lại.

Theo Apple Engineering (2024), draw(_:) hoạt động thông qua kết xuất Bitmap Graphics Context dựa trên CPU, mang lại sự linh hoạt tối đa nhưng yêu cầu nhiều tài nguyên hơn so với CALayer. Quyết định sử dụng draw(_:) được đưa ra dựa trên độ phức tạp của đồ họa và yêu cầu hiệu suất.

Chữ ký phương thức trong Swift và Objective-C

Trong Swift, phương thức được khai báo là override func draw(_ rect: CGRect), trong đó rect là hình chữ nhật cần được vẽ lại. Trong Objective-C, chữ ký là - (void)drawRect:(CGRect)rect. Tham số rect có thể nhỏ hơn bounds của view khi vẽ lại một phần qua setNeedsDisplay(_:).

objective-c
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
    CGContextFillRect(context, rect);
}

Khi nào hệ thống gọi draw(_:)

Hệ thống tự động gọi draw(_:) trong các kịch bản được xác định chặt chẽ. Hiểu được các bộ kích hoạt này giúp tránh vẽ lại không cần thiết và tối ưu hóa hiệu suất của view. Dưới đây là tất cả các trường hợp gọi tự động của phương thức.

  • Lần hiển thị đầu tiên — khi view được thêm vào hệ thống phân cấp lần đầu và hiển thị trên màn hình
  • setNeedsDisplay() — sau khi gọi phương thức này, hệ thống vẽ lại trong chu kỳ vẽ tiếp theo
  • setNeedsDisplay(_:) — tương tự như trên, nhưng chỉ định một hình chữ nhật cụ thể để vẽ lại
  • contentMode — khi bounds thay đổi, nếu contentMode yêu cầu vẽ lại (ví dụ .redraw)
  • setNeedsLayout() — trong một số trường hợp, sau khi sắp xếp lại subview, có thể cần vẽ lại

Tài liệu Apple cảnh báo: không bao giờ gọi draw(_:) trực tiếp. Hệ thống tự quyết định khi nào thực hiện vẽ, và việc gọi trực tiếp phá vỡ cơ chế lưu cache nội bộ. Để yêu cầu vẽ lại, luôn sử dụng setNeedsDisplay() hoặc setNeedsDisplay(_:).

Cách ghi đè draw(_:) trong Swift

Ghi đè draw(_:) trong Swift bắt đầu bằng việc lấy ngữ cảnh đồ họa và các lệnh gọi Core Graphics tiếp theo. Nên tạo các phương thức riêng cho các khối vẽ logic — điều này cải thiện khả năng đọc và kiểm thử của mã.

swift
override func draw(_ rect: CGRect) {
    super.draw(rect)

    guard let context = UIGraphicsGetCurrentContext() else { return }

    // Line parameters
    context.setStrokeColor(UIColor.darkGray.cgColor)
    context.setLineWidth(2.0)

    // Drawing a triangle
    context.move(to: CGPoint(x: rect.midX, y: rect.minY + 10))
    context.addLine(to: CGPoint(x: rect.maxX - 10, y: rect.maxY - 10))
    context.addLine(to: CGPoint(x: rect.minX + 10, y: rect.maxY - 10))
    context.closePath()
    context.strokePath()
}

Trong ví dụ này, draw(_:) vẽ một hình tam giác với đường viền màu xám đậm dày 2 pixel. Gọi super.draw(rect) ở đầu phương thức được Apple khuyến nghị để bảo tồn logic vẽ của lớp cha, mặc dù triển khai mặc định của draw(_:) trong UIView là trống.

Quy tắc tính đẳng trị của draw(_:)

Mỗi lần gọi draw(_:) phải tạo ra kết quả giống hệt nhau với cùng dữ liệu đầu vào. Điều này cho phép hệ thống lưu cache kết quả và không gọi lại draw(_:) nếu nội dung của view không thay đổi. Không sử dụng giá trị ngẫu nhiên, thời gian hệ thống hoặc yêu cầu mạng bên trong draw(_:).

drawRect: trong Objective-C và sự khác biệt với draw(_:)

drawRect: là phiên bản đầu tiên của phương thức, xuất hiện trong iOS 2.0 cùng với Objective-C. Trong Swift, phương thức được đổi tên thành draw(_:) sử dụng tham số ngoài _. Về mặt chức năng, các phương thức giống hệt nhau: cả hai đều nhận CGRect của khu vực cần vẽ lại và sử dụng UIGraphicsGetCurrentContext() để truy cập ngữ cảnh đồ họa.

Đặc điểmdraw(_:) (Swift)drawRect: (Objective-C)
Chữ kýoverride func draw(_ rect: CGRect)- (void)drawRect:(CGRect)rect
Gọi supersuper.draw(rect)[super drawRect:rect]
Ngữ cảnhUIGraphicsGetCurrentContext()UIGraphicsGetCurrentContext()
Tham số rectrect: CGRectCGRect rect
Hiệu suấtGiống hệtGiống hệt

Khi di chuyển dự án từ Objective-C sang Swift, việc đổi tên phương thức là một trong những nhiệm vụ đầu tiên. Xcode cung cấp bộ chuyển đổi tự động, nhưng drawRect: yêu cầu cập nhật thủ công thành draw(_:). Theo Apple (2024), phiên bản Swift draw(_:) được ưu tiên cho các dự án mới.

Tối ưu hóa vẽ trong draw(_:)

draw(_:) chạy trên CPU, và triển khai không tối ưu có thể gây giảm khung hình và hiệu suất kém. Apple khuyến nghị một số cách tiếp cận đã được kiểm chứng để tăng tốc độ vẽ.

Giảm thiểu số lượng thao tác vẽ

Mỗi thao tác Core Graphics (move(to:), addLine(to:), strokePath) có chi phí overhead. Nhóm các thao tác và sử dụng CGPath cho các hình dạng phức tạp — đường dẫn được tạo một lần và tái sử dụng ở mỗi lần gọi draw(_:).

Sử dụng UIBezierPath cho các đối tượng vector

UIBezierPath là một wrapper Objective-C của CGPath cung cấp API đơn giản để tạo hình dạng. Tạo UIBezierPath trước (ví dụ trong bộ khởi tạo) và chỉ cần gọi fill() hoặc stroke() bên trong draw(_:).

swift
private let starPath: UIBezierPath = {
    let path = UIBezierPath()
    // Building a star shape
    path.move(to: CGPoint(x: 50, y: 0))
    for i in 1...5 {
        let angle = CGFloat(i) * 4 * CGFloat.pi / 5
        path.addLine(to: CGPoint(x: 50 + 40 * cos(angle),
                               y: 50 + 40 * sin(angle)))
    }
    path.close()
    return path
}()

override func draw(_ rect: CGRect) {
    UIColor.systemYellow.setFill()
    starPath.fill()
}

Lỗi thường gặp khi làm việc với draw(_:)

Các nhà phát triển thường mắc cùng một lỗi khi ghi đè draw(_:). Biết được các mẫu này giúp tránh lỗi và suy giảm hiệu suất. Hãy xem các vấn đề phổ biến nhất và giải pháp của chúng.

  • Gọi draw(_:) trực tiếp — không bao giờ gọi draw(_:) trực tiếp. Sử dụng setNeedsDisplay() để yêu cầu vẽ lại. Gọi trực tiếp phá vỡ cache và có thể dẫn đến hiển thị không chính xác.
  • Tính toán nặng bên trong draw(_:) — draw(_:) nên nhẹ nhất có thể. Tạo UIBezierPath, hình ảnh và các đối tượng nặng khác bên ngoài phương thức, chỉ khởi tạo một lần.
  • Tạo đối tượng bên trong draw(_:) — việc tạo UIColor, UIFont và UIGraphicsImageRenderer bên trong draw(_:) tạo ra tải bộ nhớ không cần thiết. Di chuyển việc tạo đối tượng vào thuộc tính của lớp.
  • Bỏ qua tham số rect — rect chỉ định khu vực cần vẽ lại. Vẽ bên ngoài rect bị hệ thống loại bỏ nhưng tiêu tốn tài nguyên. Kiểm tra giao điểm với rect trước khi vẽ.
  • Thiếu super.draw(rect) — mặc dù triển khai của UIView là trống, Apple khuyến nghị gọi super.draw(rect) để tương thích với các thay đổi trong tương lai của UIKit.

Tuân theo các quy tắc này, bạn sẽ đảm bảo vẽ tùy chỉnh ổn định và nhanh chóng trong bất kỳ dự án iOS nào. Hãy phân tích draw(_:) thông qua Instruments (Core Animation) để xem thời gian thực thi thực tế và các điểm nghẽn.

Câu hỏi thường gặp

Có thể gọi draw(_:) trực tiếp không?

Không, việc gọi trực tiếp draw(_:) bị cấm bởi tài liệu Apple. Hệ thống tự quản lý chu kỳ vẽ. Để yêu cầu vẽ lại, hãy sử dụng setNeedsDisplay(), đánh dấu view là cần cập nhật trong chu kỳ kết xuất gần nhất.

drawRect: khác draw(_:) như thế nào?

Về mặt chức năng, các phương thức này giống hệt nhau. drawRect: được sử dụng trong Objective-C, draw(_:) trong Swift. Cả hai đều nhận CGRect của khu vực cần vẽ lại và sử dụng cùng ngữ cảnh Core Graphics qua UIGraphicsGetCurrentContext().

Tại sao draw(_:) không được gọi cho UIView trống?

Apple tối ưu hóa kết xuất: nếu UIView không có draw(_:) được ghi đè, hệ thống sẽ không tạo ngữ cảnh bitmap cho nó. Điều này tiết kiệm bộ nhớ. Nếu ghi đè tồn tại nhưng phương thức không được gọi — hãy kiểm tra frame của view không bằng 0 và view có hiển thị hay không.

Hệ thống gọi draw(_:) bao lâu một lần?

Chỉ khi cần thiết: lần hiển thị đầu tiên, sau setNeedsDisplay(), khi bounds thay đổi với contentMode = .redraw. Ở trạng thái tĩnh, draw(_:) không được gọi lại, tiết kiệm tài nguyên CPU và pin.

Có cần gọi super.draw(rect) trong Swift không?

Apple khuyến nghị gọi super.draw(rect) ở đầu phương thức được ghi đè. Mặc dù triển khai hiện tại của UIView là trống, việc gọi super đảm bảo tương thích với các phiên bản UIKit trong tương lai và là một thực hành tốt.

Tổng kết

  • draw(_:) / drawRect — phương thức vẽ chính của UIView trong Swift và Objective-C, được hệ thống tự động gọi
  • CGContext — ngữ cảnh Core Graphics có sẵn trong draw(_:) qua UIGraphicsGetCurrentContext()
  • setNeedsDisplay() — cách đúng để yêu cầu vẽ lại; gọi trực tiếp draw(_:) bị cấm
  • Tính đẳng trị — draw(_:) phải tạo ra cùng kết quả với cùng dữ liệu đầu vào để cache đúng cách
  • UIBezierPath — tạo đường dẫn bên ngoài draw(_:) để giảm tải CPU ở mỗi lần gọi
  • Tham số rect — chứa khu vực vẽ lại; sử dụng giao điểm để tối ưu — không vẽ bên ngoài nó
  • Phân tích — kiểm tra hiệu suất draw(_:) qua Instruments Core Animation để xác định các thao tác chậm

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm