setNeedsDisplay() เป็นเมธอดอินสแตนซ์ของ UIView ที่ทำเครื่องหมายวิวว่าต้องการให้วาดใหม่ แตกต่างจากการเรียก draw(_:) โดยตรง setNeedsDisplay() จะตั้งค่า dirty flag สำหรับวิวและคืนการควบคุมทันที ระบบประมวลผลคำขอนี้ในรอบการเรนเดอร์ที่ใกล้ที่สุด ทำให้สามารถรวมหลายการเรียกเป็นการดำเนินการวาดเดียว ตามเอกสารของ Apple (2025) การวาดใหม่แบบเลื่อนเวลาผ่าน setNeedsDisplay() เป็นวิธีที่ถูกต้องเพียงวิธีเดียวในการอัปเดตเนื้อหาของ UIView ที่กำหนดเอง
ประเด็นสำคัญ
setNeedsDisplay() เป็นเมธอดของ UIView ที่ส่งสัญญาณให้ระบบทราบว่าเนื้อหาของวิวล้าสมัยและต้องการการเรนเดอร์ใหม่ เมธอดไม่รับพารามิเตอร์และคืนค่า void หลังจากการเรียก วิวจะถูกทำเครื่องหมายเป็น dirty และในรอบการวาดที่ใกล้ที่สุด ระบบจะเรียก draw(_:) สำหรับวิวนี้
คุณสมบัติหลักคือ อะซิงโครนัส การเรียก setNeedsDisplay() หลายครั้งติดต่อกันสำหรับวิวเดียวกันจะไม่ส่งผลให้มีการเรียก draw(_:) หลายครั้ง — ระบบรวมพวกมันเป็นการดำเนินการเดียว สิ่งนี้สำคัญอย่างยิ่งต่อประสิทธิภาพ: หากคุณสมบัติของวิวเปลี่ยนแปลงหลายครั้งต่อเฟรม draw(_:) จะถูกเรียกเพียงครั้งเดียว
กลไกการวาดใหม่แบบเลื่อนเวลาขึ้นอยู่กับรอบการเรนเดอร์ของ UIKit เมื่อเรียก setNeedsDisplay() ระบบจะตั้งค่าแฟลกภายใน layer.needsDisplay เป็น true เมื่อสิ้นสุด run loop ปัจจุบัน ระบบจะวนซ้ำเลเยอร์ dirty ทั้งหมดและเรียก draw(_:) สำหรับแต่ละเลเยอร์
run loop ของ UIKit รวมถึงเฟส อัปเดต ในระหว่างที่ประมวลผลวิว dirty ในเฟสนี้ layoutSubviews() จะถูกเรียกหากจำเป็น จากนั้น draw(_:) สำหรับวิวที่ถูกทำเครื่องหมายทั้งหมด หลังจากวาดเสร็จ ผลลัพธ์จะถูกส่งต่อไปยัง GPU ผ่าน Core Animation สำหรับการประกอบ
class ProgressView: UIView {
var progress: CGFloat = 0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// พื้นหลัง
ctx.setFillColor(UIColor.lightGray.cgColor)
ctx.fill(rect)
// แถบความคืบหน้า
let fillRect = CGRect(x: rect.minX, y: rect.minY,
width: rect.width * progress, height: rect.height)
ctx.setFillColor(UIColor.systemGreen.cgColor)
ctx.fill(fillRect)
}
}
ในตัวอย่างนี้ progressView อัปเดต progress และแต่ละ setter จะเรียก setNeedsDisplay() โดยอัตโนมัติ ระบบรอจนกระทั่งสิ้นสุด run loop และเรียก draw(_:) ด้วยค่า progress จริง หาก progress เปลี่ยนแปลงสามครั้งต่อเฟรม draw(_:) จะทำงานเพียงครั้งเดียว — ด้วยค่าสุดท้าย
เมธอด setNeedsDisplay(_:) รับพารามิเตอร์ rect ชนิด CGRect ซึ่งจำกัดพื้นที่วาดใหม่ ทำให้สามารถวาดใหม่เฉพาะส่วนที่เปลี่ยนแปลงของวิว แทนที่จะวาดทั้งวิว ความแตกต่างด้านประสิทธิภาพจะเห็นได้ชัดเมื่อทำงานกับวิวขนาดใหญ่ที่การเปลี่ยนแปลงส่งผลกระทบต่อพื้นที่เล็ก
| ลักษณะ | setNeedsDisplay() | setNeedsDisplay(_:) |
|---|---|---|
| พารามิเตอร์ | ไม่มี | CGRect — พื้นที่วาดใหม่ |
| พื้นที่วาดใหม่ | bounds ทั้งหมดของวิว | เฉพาะสี่เหลี่ยมที่ระบุ |
| ประสิทธิภาพ | ต่ำกว่าสำหรับวิวขนาดใหญ่ | สูงกว่าสำหรับการเปลี่ยนแปลงบางส่วน |
| พารามิเตอร์ rect ใน draw(_:) | bounds ทั้งหมด | สี่เหลี่ยมที่ระบุ |
| เมื่อใดควรใช้ | เปลี่ยนเนื้อหาทั้งหมด | การเปลี่ยนแปลงเฉพาะจุด (เคอร์เซอร์, ไฮไลต์) |
สำหรับกราฟิกที่ซับซ้อน แนะนำให้วาดทั้งฉากในบริบทนอกหน้าจอก่อน จากนั้นเรียก setNeedsDisplay(_:) เฉพาะพื้นที่ที่เปลี่ยนแปลง นี่เป็นเทคนิคมาตรฐานในแอปพลิเคชันวาดภาพและโปรแกรมแก้ไขกราฟิกบน iOS
displayIfNeeded() เป็นเมธอดซิงโครนัสที่เริ่มการวาดใหม่ของวิว dirty ทั้งหมดในลำดับชั้นทันที โดยไม่รอให้สิ้นสุด run loop แตกต่างจาก setNeedsDisplay() ที่ตั้งค่าแฟลกเท่านั้น displayIfNeeded() บังคับให้ดำเนินการ draw(_:) ทันที
Apple แนะนำให้ใช้ displayIfNeeded() เฉพาะในกรณีที่รุนแรงเมื่อต้องการผลลัพธ์การวาดทันที — ตัวอย่างเช่น ก่อนถ่ายภาพหน้าจอ หรือระหว่างการสร้างภาพแบบซิงโครนัสสำหรับ UIGraphicsImageRenderer ในสถานการณ์ปกติ โมเดลอะซิงโครนัสผ่าน setNeedsDisplay() จะดีกว่าเนื่องจากไม่บล็อกเธรดหลัก
// แอสังโครนัส — แนะนำ
someView.setNeedsDisplay()
// ซังโครนัส — เมื่อจำเป็นเท่านั้น
someView.setNeedsDisplay()
someView.displayIfNeeded() // draw(_:) ถูกเรียกทันที
การเรียก displayIfNeeded() โดยไม่มี setNeedsDisplay() ก่อนหน้าจะไม่มีผล — เมธอดประมวลผลเฉพาะวิวที่ทำเครื่องหมายเป็น dirty แล้วเท่านั้น หากไม่มีวิวใดต้องการวาดใหม่ displayIfNeeded() จะเสร็จสมบูรณ์โดยไม่เรียก draw(_:)
การเรียก setNeedsDisplay() บ่อยครั้งสามารถลดประสิทธิภาพ โดยเฉพาะระหว่างแอนิเมชัน มาดูกลยุทธ์การเพิ่มประสิทธิภาพหลักกัน
แทนที่จะเรียก setNeedsDisplay() ทุกครั้งที่คุณสมบัติแต่ละรายการเปลี่ยนแปลง ให้สะสมการเปลี่ยนแปลงและเรียกเมธอดครั้งเดียวหลังจากใช้การอัปเดตทั้งหมด ตัวอย่างเช่น เมื่อเปลี่ยนสี ขนาด และตำแหน่งขององค์ประกอบ — เรียก setNeedsDisplay() ครั้งเดียวหลังจากตั้งค่าคุณสมบัติทั้งหมด
หากคุณสมบัติสามารถทำแอนิเมชันผ่าน CALayer (backgroundColor, opacity, position, transform) ให้ใช้ Core Animation แทน draw(_:) คุณสมบัติของ CALayer จะอัปเดตบน GPU โดยไม่เรียก draw(_:) ให้ 60 FPS โดยไม่มีภาระบน CPU
เพื่อรวมการเปลี่ยนแปลงหลายรายการเป็นรอบการเรนเดอร์เดียว ให้ใช้ CATransaction สิ่งนี้ช่วยให้รวมการเรียก setNeedsDisplay() หลายครั้งเป็นการดำเนินการ draw(_:) เดียวเมื่อสิ้นสุดธุรกรรม
CATransaction.begin()
CATransaction.setDisableActions(true)
view1.setNeedsDisplay()
view2.setNeedsDisplay()
view3.setNeedsDisplay()
CATransaction.commit()
อย่าใช้ displayIfNeeded() มากเกินไป — การวาดใหม่แบบซิงโครนัสบล็อกเธรดหลักและอาจทำให้เฟรมดรอปได้ ใช้เฉพาะสำหรับการดำเนินการที่ผลลัพธ์การวาดมีความสำคัญก่อนที่ run loop ปัจจุบันจะสิ้นสุด
คำถามที่พบบ่อย
ไม่ การเรียก draw(_:) โดยตรงเป็นสิ่งต้องห้าม setNeedsDisplay() ทำเครื่องหมายวิวเป็น dirty อย่างถูกต้อง และระบบเรียก draw(_:) ในเวลาที่เหมาะสมของรอบการเรนเดอร์ การเรียก draw(_:) โดยตรงจะละเว้นกลไกการแคชและอาจนำไปสู่สถานะที่ไม่สอดคล้องกัน
การเรียก setNeedsDisplay() หลายครั้งสำหรับวิวเดียวกันจะถูกรวมโดยระบบเป็นการเรียก draw(_:) ครั้งเดียว สิ่งนี้เกิดขึ้นเพราะเมธอดตั้งค่าเฉพาะ dirty flag ซึ่งจะถูกล้างหลังจากการดำเนินการจริงของ draw(_:) เมื่อสิ้นสุด run loop
setNeedsDisplay() ขอให้วาดเนื้อหาของวิวใหม่ (เรียก draw(_:)) setNeedsLayout() ขอให้จัดเรียงวิวย่อยใหม่ (เรียก layoutSubviews()) layoutSubviews() สามารถนำไปสู่การเปลี่ยนแปลงขนาด ซึ่งอาจทำให้เกิดการวาดใหม่
setNeedsDisplay(_:) ควรใช้สำหรับการเปลี่ยนแปลงเฉพาะจุด: การเคลื่อนที่ของเคอร์เซอร์ การเปลี่ยนสีของพื้นที่ที่เลือก การอัปเดตส่วนหนึ่งของกราฟ สิ่งนี้จำกัดพื้นที่วาดใหม่และปรับปรุงประสิทธิภาพเมื่อเทียบกับการวาดวิวทั้งใหม่
โดยค่าเริ่มต้น การเปลี่ยน frame หรือ bounds ไม่เรียก setNeedsDisplay() ระบบเพียงแค่ย้ายวิว หากต้องการวาดใหม่เมื่อขนาดเปลี่ยน ให้ตั้งค่าคุณสมบัติ contentMode เป็น .redraw — จากนั้น UIKit จะเรียก setNeedsDisplay() โดยอัตโนมัติทุกครั้งที่ bounds เปลี่ยน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม