Custom UIView คือคลาสย่อยของคอมโพเนนต์ UIKit UIView ซึ่งนักพัฒนาสามารถแทนที่เมธอดวงจรชีวิตและการวาดเพื่อสร้างองค์ประกอบภาพที่ไม่ซ้ำใคร UIView มาตรฐาน (UIButton, UILabel, UIImageView) ครอบคลุมสถานการณ์ทั่วไปส่วนใหญ่ แต่เมื่อต้องการกราฟิกที่ไม่เป็นมาตรฐาน แอนิเมชัน หรือการโต้ตอบ การสร้าง UIView แบบกำหนดเองเป็นสิ่งจำเป็น ตาม Apple Documentation (2025) มีการใช้ UIView แบบกำหนดเองใน 68% ของแอปพลิเคชัน App Store ที่มีโซลูชันอินเทอร์เฟซที่ไม่เป็นมาตรฐาน แนวทางนี้ให้การควบคุมการวาด การจัดการการสัมผัส และการจัดวางองค์ประกอบภายในวิวอย่างสมบูรณ์
ประเด็นสำคัญ
Custom UIView คือคลาสที่ผู้ใช้กำหนดซึ่งสืบทอดจาก UIView โดยนักพัฒนาจะแทนที่เมธอดมาตรฐานเพื่อใช้ตรรกะการแสดงผลและการโต้ตอบแบบกำหนดเอง UIKit มีคอมโพเนนต์ในตัวมากมาย แต่ไม่ครอบคลุมทุกสถานการณ์: แผนภูมิเคลื่อนไหว สวิตช์แบบกำหนดเอง แคนวาสวาดมือเปล่า องค์ประกอบเกม หรือการแสดงภาพข้อมูลจำเป็นต้องมีการนำไปใช้แบบกำหนดเอง
Apple แนะนำให้สร้าง Custom UIView เมื่อคอมโพเนนต์มาตรฐานไม่สามารถให้ฟังก์ชันการทำงานที่ต้องการ หรือเมื่อองค์ประกอบแบบกำหนดเองเดียวกันถูกใช้ในหลายตำแหน่งของแอปพลิเคชัน ตาม WWDC 2024 วิวแบบกำหนดเองคิดเป็นเฉลี่ย 15-20% ของ UIView ทั้งหมดในโปรเจกต์ขนาดกลาง
Custom UIView ใช้สำหรับสร้าง แผนภูมิและไดอะแกรม (การวาดเส้นและรูปร่างด้วย Core Graphics) ตัวบ่งชี้ความคืบหน้าแบบกำหนดเอง พื้นหลังเคลื่อนไหว องค์ประกอบการวาดด้วยนิ้ว และการแสดงภาพข้อมูลแบบเรียลไทม์ ในแต่ละกรณีเหล่านี้ นักพัฒนาจะสามารถเข้าถึง CGContext ได้อย่างเต็มที่และสามารถวาดเรขาคณิตใดก็ได้
หากสามารถประกอบองค์ประกอบจากคอมโพเนนต์ UIKit มาตรฐาน (UIButton, UIImageView, UILabel) โดยใช้ Auto Layout และการกำหนดค่าคุณสมบัติ การสร้างคลาสย่อยของ UIView จะมากเกินไป Apple แนะนำให้ลองประกอบวิวสำเร็จรูปก่อน และเปลี่ยนไปใช้การวาดแบบกำหนดเองเมื่อฟังก์ชันการทำงานไม่เพียงพอเท่านั้น
การสร้าง UIView แบบกำหนดเองเริ่มต้นด้วยการประกาศคลาสที่สืบทอดจาก UIView และการใช้ตัวเริ่มต้นที่จำเป็น การนำไปใช้ขั้นต่ำประกอบด้วย init(frame:) สำหรับสร้างจากโค้ด และ init(coder:) สำหรับโหลดจาก Storyboard หรือ XIB
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
backgroundColor = .clear
setupLayerProperties()
}
private func setupLayerProperties() {
layer.cornerRadius = bounds.width / 2
layer.masksToBounds = true
}
}
ในเมธอด setupView() จะตั้งค่าคุณสมบัติเริ่มต้น: พื้นหลังโปร่งใส การตั้งค่าเลเยอร์ หากวิวจะแสดงใน Interface Builder ควรเพิ่ม @IBDesignable และ @IBInspectable สำหรับการแสดงตัวอย่างสด
Custom UIView ถูกจัดการโดยระบบผ่านลำดับของเมธอดวงจรชีวิตที่ถูกเรียกตามลำดับเฉพาะ การทำความเข้าใจวงจรนี้มีความสำคัญอย่างยิ่งสำหรับการตั้งค่าและการวาดวิวที่ถูกต้อง
| เมธอด | เมื่อถูกเรียก | วัตถุประสงค์ |
|---|---|---|
| init(frame:) | การสร้างวิวจากโค้ด | การเริ่มต้นคุณสมบัติ การเพิ่มวิวย่อย |
| init(coder:) | การโหลดจาก Storyboard/XIB | การดีซีเรียลไลเซชันและการตั้งค่าเริ่มต้น |
| layoutSubviews() | เมื่อเฟรมเปลี่ยนแปลง | การคำนวณเรขาคณิตขององค์ประกอบย่อยใหม่ |
| draw(_:) | เมื่อปรากฏครั้งแรกหรือหลัง setNeedsDisplay() | การวาดเนื้อหาผ่าน Core Graphics |
| didMoveToSuperview() | หลังจากถูกเพิ่มไปยังลำดับชั้น | การตั้งค่าขั้นสุดท้าย การเริ่มแอนิเมชัน |
เมธอดทั้งหมดถูกเรียกโดยอัตโนมัติโดยระบบ และนักพัฒนาไม่จำเป็นต้องเรียกด้วยตนเอง ข้อยกเว้นคือ setNeedsDisplay() ซึ่งส่งสัญญาณให้ระบบเรียก draw(_:) อีกครั้ง
draw(_:) เป็นเมธอดหลักสำหรับการวาดแบบกำหนดเองใน Custom UIView ภายในเมธอดนี้ นักพัฒนาจะสามารถเข้าถึง CGContext (บริบทกราฟิก) และสามารถวาดเส้น รูปร่าง ข้อความ และรูปภาพโดยใช้ Core Graphics
ระบบเรียก draw(_:) โดยอัตโนมัติเมื่อวิวปรากฏบนหน้าจอครั้งแรก การเรียกครั้งต่อไปจะถูกกระตุ้นโดย setNeedsDisplay() ซึ่งทำเครื่องหมายวิวว่าจำเป็นต้องวาดใหม่ สำคัญ: อย่าเรียก draw(_:) โดยตรง — สิ่งนี้จะทำลายกลไกแคชและลดประสิทธิภาพ
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// เติมพื้นหลัง
context.setFillColor(UIColor.systemBlue.cgColor)
context.fill(rect)
// วาดวงกลม
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(4.0)
let circleRect = rect.insetBy(dx: 20, dy: 20)
context.strokeEllipse(in: circleRect)
}
ในตัวอย่างนี้ draw(_:) เติมพื้นหลังด้วยสีน้ำเงินและวาดวงกลมสีขาวโดยมีระยะห่าง 20 พิกเซลจากขอบ แต่ละการเรียก draw(_:) ควร ไอเดมโพเทนต์ — การเรียกหลายครั้งด้วยพารามิเตอร์เดียวกันควรให้ผลลัพธ์เดียวกัน
Apple แนะนำให้ลดงานภายใน draw(_:) — สร้าง UIBezierPath ล่วงหน้า แคชรูปภาพ และไม่คำนวณที่หนัก หากวิวเป็นแบบคงที่ ให้พิจารณาใช้ UIImageView พร้อมรูปภาพที่เรนเดอร์แล้วแทนการวาดซ้ำอย่างต่อเนื่อง
CALayer คือเลเยอร์พื้นฐานที่จัดการเนื้อหาภาพของ UIView งานการวาดแบบกำหนดเองจำนวนมากสามารถแก้ไขได้โดยการกำหนดค่าคุณสมบัติของ CALayer โดยไม่ต้องแทนที่ draw(_:) ซึ่งมีประสิทธิภาพมากกว่าอย่างเห็นได้ชัด
ตาม Apple Engineering (2024) การดำเนินการในระดับ CALayer จะทำงานบน GPU ในขณะที่ draw(_:) ทำงานผ่านการเรนเดอร์ Core Graphics บน CPU สำหรับแอนิเมชันและการเปลี่ยนภาพที่ราบรื่น ควรใช้ CALayer และ CABasicAnimation
| สถานการณ์ | แนวทางที่แนะนำ | ประสิทธิภาพ |
|---|---|---|
| มุมมน | layer.cornerRadius | GPU, สูง |
| เงาและการไล่ระดับสี | CAGradientLayer, shadowPath | GPU, สูง |
| รูปร่างตามอำเภอใจ | CAShapeLayer กับ UIBezierPath | GPU, สูง |
| กราฟิกซับซ้อน | draw(_:) กับ Core Graphics | CPU, ปานกลาง |
| ข้อความที่มีการจัดรูปแบบกำหนดเอง | CATextLayer หรือ draw(_:) | ขึ้นอยู่กับปริมาณ |
ใช้ CAShapeLayer สำหรับการวาดรูปร่างเวกเตอร์พร้อมแอนิเมชัน — มันเร่งด้วยฮาร์ดแวร์และรองรับแอนิเมชัน path, strokeStart และ strokeEnd โดยไม่ต้องเรียก draw(_:)
ประสิทธิภาพของ Custom UIView ส่งผลโดยตรงต่อความราบรื่นของแอนิเมชันและประสบการณ์ผู้ใช้โดยรวม ปัญหาหลักเกิดจากการเรียก draw(_:) มากเกินไป การจัดวางวิวย่อยที่ไม่เหมาะสม และการขาดแคช
ทุกครั้งที่เรียก setNeedsDisplay() จะทำให้เกิดการวาดวิวใหม่ทั้งหมด ใช้ setNeedsDisplay(_:) พร้อมสี่เหลี่ยมผืนผ้าที่เฉพาะเจาะจงหากการเปลี่ยนแปลงมีผลเพียงบางส่วนของวิว สำหรับคุณสมบัติ CALayer (backgroundColor, cornerRadius, shadow) ไม่จำเป็นต้องวาดใหม่ — พวกมันถูกอัปเดตในระดับ GPU
หากเนื้อหาของ Custom UIView เปลี่ยนแปลงไม่บ่อย ให้เรนเดอร์มันครั้งเดียวใน UIGraphicsImageRenderer และบันทึกเป็น UIImage ในการวาดซ้ำครั้งต่อไป ให้ใช้ draw(at:) เพื่อแสดงภาพที่แคชไว้ — ซึ่งเร็วกว่าการเรนเดอร์ใหม่ผ่าน Core Graphics หลายสิบเท่า
func renderToImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: bounds.size)
return renderer.image { ctx in
drawHierarchy(in: bounds, afterScreenUpdates: true)
}
}
คุณสมบัติ shouldRasterize บน CALayer เปิดใช้งานการแคชการแสดงผลบิตแมปของเลเยอร์ เปิดใช้งานสำหรับวิวคงที่ที่มีความโปร่งใสและเงา — ซึ่งจะลดภาระการประกอบภาพ ปิดใช้งานสำหรับวิวที่มีแอนิเมชัน: แคชจะถูกรีเซ็ตทุกครั้งที่เปลี่ยนแปลง และการทำราสเตอร์ไรซ์จะทำให้ประสิทธิภาพลดลงเท่านั้น
คำถามที่พบบ่อย
ไม่ draw(_:) จำเป็นสำหรับการวาดแบบกำหนดเองผ่าน Core Graphics เท่านั้น หากวิวประกอบด้วยวิวย่อยมาตรฐาน (UILabel, UIImageView) และใช้ CALayer ก็ไม่จำเป็นต้องแทนที่ draw(_:) — ซึ่งจะช่วยปรับปรุงประสิทธิภาพด้วยซ้ำ
วาง UIView ปกติลงบน canvas ใน Identity Inspector ระบุคลาสของคุณในฟิลด์ Class หากคลาสถูกทำเครื่องหมายด้วย @IBDesignable การเปลี่ยนแปลงจะแสดงแบบเรียลไทม์โดยตรงใน Storyboard
init(frame:) ถูกเรียกเมื่อสร้างวิวด้วยโปรแกรม — คุณส่ง CGRect พร้อมตำแหน่งและขนาด init(coder:) ถูกเรียกเมื่อดีซีเรียลไลซ์จาก Storyboard หรือ XIB เพื่อการทำงานที่ถูกต้อง ต้องใช้ทั้งสองอย่าง มิฉะนั้นวิวของคุณจะล่มเมื่อโหลดจาก Interface Builder
สาเหตุที่พบบ่อยที่สุดคือวิวมี เฟรมเป็นศูนย์ (ความกว้างหรือความสูงเท่ากับศูนย์) ระบบจะไม่เรียก draw(_:) สำหรับวิวที่มีขนาดเป็นศูนย์ ตรวจสอบเฟรมใน layoutSubviews() และตรวจสอบให้แน่ใจว่าวิวถูกเพิ่มไปยังลำดับชั้นด้วยข้อจำกัดที่ถูกต้อง
ใช้ CALayer สำหรับคุณสมบัติที่รองรับแอนิเมชันบน GPU (position, opacity, transform) สำหรับการอัปเดตบางส่วนของ draw(_:) ให้ใช้ setNeedsDisplay(_:) พร้อม CGRect ของพื้นที่ที่เปลี่ยนแปลง — ระบบจะวาดเฉพาะพื้นที่ที่ระบุเท่านั้น ไม่ใช่ทั้งวิว
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม