Delegate เป็นรูปแบบการออกแบบที่ออบเจกต์หนึ่งมอบหมายการทำงานให้กับอีกออบเจกต์หนึ่ง ใน iOS รูปแบบนี้ถูกนำไปใช้ผ่านโปรโตคอล Swift และ @protocol Objective-C การมอบหมายเป็นหนึ่งในรูปแบบพื้นฐานของ Cocoa Touch ที่ใช้ใน UITableViewDelegate, UITextFieldDelegate และ API อื่นๆ ของ Apple อีกนับร้อย ตามเอกสารสำหรับนักพัฒนา Apple (2025) ประมาณ 70% ของคลาสระบบ UIKit ใช้ดีลิเกตเพื่อปรับแต่งพฤติกรรมโดยไม่ต้องสืบทอด
ประเด็นสำคัญ
Delegate เป็นรูปแบบการออกแบบเชิงพฤติกรรมที่ช่วยให้ออบเจกต์สามารถมอบหมายความรับผิดชอบบางส่วนให้กับออบเจกต์อื่น ไม่เหมือนกับการสืบทอดที่คลาสลูกเขียนทับเมธอดของคลาสพ่อแม่ ดีลิเกตใช้การประกอบ (composition): ออบเจกต์เจ้าของเก็บการอ้างอิงไปยังดีลิเกตและเรียกเมธอดของมันในจุดที่กำหนด
ดีลิเกตกำหนด โปรโตคอล — ชุดของเมธอดที่ดีลิเกตสามารถนำไปใช้ เมธอดแบ่งออกเป็นแบบบังคับ (required) และแบบเลือกได้ (optional) ใน Swift เมธอดโปรโตคอลแบบเลือกได้จะถูกทำเครื่องหมายด้วยคีย์เวิร์ด @objc optional
ออบเจกต์ A (เจ้าของ) มีคุณสมบัติ delegate — การอ้างอิงแบบอ่อนไปยังออบเจกต์ B (ดีลิเกต) เมื่อเกิดเหตุการณ์ขึ้น A จะตรวจสอบว่า B นำเมธอดโปรโตคอลที่เกี่ยวข้องไปใช้หรือไม่และเรียกมัน การอ้างอิงแบบอ่อน จำเป็น: หากไม่มีมัน ดีลิเกตจะไม่สามารถถูกปลดปล่อยจากหน่วยความจำได้เพราะเจ้าของเก็บมันไว้ด้วยการอ้างอิงแบบแข็ง
protocol LoaderDelegate: AnyObject {
func loaderDidStart(_ loader: DataLoader)
func loader(_ loader: DataLoader, didLoad data: Data)
func loader(_ loader: DataLoader, didFailWith error: Error)
}
class DataLoader {
weak var delegate: LoaderDelegate?
func start() {
delegate?.loaderDidStart(self)
// การโหลดแบบอะซิงโครนัส
}
}คลาส DataLoader กำหนดโปรโตคอล LoaderDelegate และเรียกเมธอดดีลิเกตในจุดสำคัญของวงจรการโหลด AnyObject รับประกันว่าโปรโตคอลสามารถนำไปใช้ได้โดยคลาสเท่านั้น — ซึ่งจำเป็นสำหรับการอ้างอิงแบบอ่อน
การนำดีลิเกตไปใช้ใน Swift ประกอบด้วยสามขั้นตอน: การประกาศโปรโตคอล การสร้างคุณสมบัติ delegate แบบอ่อนในเจ้าของ และการนำโปรโตคอลไปใช้ในคลาสดีลิเกต มาดูตัวอย่าง UITextField แบบกำหนดเองพร้อมการตรวจสอบความถูกต้อง
protocol ValidatorDelegate: AnyObject {
func validate(_ input: String) -> Bool
func validatorDidFail(_ input: String)
}
class ValidatedTextField: UITextField {
weak var validator: ValidatorDelegate?
override func textDidChange() {
guard let text = self.text else { return }
if validator?.validate(text) == false {
validator?.validatorDidFail(text)
self.layer.borderColor = UIColor.red.cgColor
}
}
}
class LoginViewController: UIViewController, ValidatorDelegate {
let textField = ValidatedTextField()
override func viewDidLoad() {
super.viewDidLoad()
textField.validator = self
}
func validate(_ input: String) -> Bool {
return input.count >= 6
}
func validatorDidFail(_ input: String) {
print("การตรวจสอบล้มเหลว: อินพุตสั้นเกินไป")
}
}LoginViewController นำโปรโตคอล ValidatorDelegate ไปใช้และกำหนดตัวเองเป็นดีลิเกตของ textField ทุกครั้งที่มีการเปลี่ยนแปลงข้อความ ValidatedTextField จะเรียก validate(_:) และหากการตรวจสอบล้มเหลว — validatorDidFail(_:) ตัวควบคุมทำหน้าที่เป็นตัวกลางระหว่าง View และตรรกะการตรวจสอบ
การเลือกระหว่างดีลิเกต การแจ้งเตือน และ closure ขึ้นอยู่กับจำนวนผู้รับและการเชื่อมต่อของส่วนประกอบ แต่ละกลไกแก้ปัญหาการสื่อสารระหว่างออบเจกต์ แต่มีข้อแลกเปลี่ยนที่แตกต่างกัน
| คุณลักษณะ | Delegate | NotificationCenter | Closure |
|---|---|---|---|
| ประเภทการสื่อสาร | 1:1 | 1:N | 1:1 |
| การเชื่อมต่อ | อ่อน (ผ่านโปรโตคอล) | อ่อนมาก (คีย์สตริง) | ปานกลาง (การจับบริบท) |
| ความปลอดภัยของชนิด | สมบูรณ์ | ไม่มี (Any?) | สมบูรณ์ |
| ความเสี่ยง retain cycle | ไม่มี (weak) | ไม่มี | มี (การจับ self) |
| เมื่อใดควรใช้ | callback ที่ซับซ้อนพร้อมหลายเมธอด | เหตุการณ์ที่หลายคนสนใจ | closure ง่ายๆ ที่มี 1-2 callback |
Delegate เหมาะสมที่สุดเมื่อคุณต้องการส่งชุดเหตุการณ์ที่เกี่ยวข้องไปยังผู้รับรายเดียว NotificationCenter เหมาะสำหรับการแจ้งเตือนแบบกระจายเสียง Closure สำหรับการดำเนินการแบบอะซิงโครนัสอย่างง่าย เช่น completion handler ใน URLSession
Objective-C ใช้ @protocol และ @optional สำหรับการประกาศดีลิเกต ต่างจาก Swift เมธอดโปรโตคอลทั้งหมดเป็นแบบเลือกได้โดยค่าเริ่มต้น ความแตกต่างหลักคือการเรียก respondsToSelector: ก่อนส่งข้อความไปยังดีลิเกต เนื่องจากเมธอดอาจไม่ได้ถูกนำไปใช้
@protocol ImageCacheDelegate
@optional
- (void)cacheDidStartDownload: (ImageCache *)cache;
- (void)cache: (ImageCache *)cache didCacheImage: (UIImage *)image;
@required
- (void)cache: (ImageCache *)cache didFailWithError: (NSError *)error;
@end
@interface ImageCache : NSObject
@property (nonatomic, weak) id<ImageCacheDelegate> delegate;
- (void)downloadImageAtURL: (NSURL *)url;
@end
@implementation ImageCache
- (void)downloadImageAtURL: (NSURL *)url {
if ([self.delegate respondsToSelector:@selector(cacheDidStartDownload:)]) {
[self.delegate cacheDidStartDownload:self];
}
// การโหลดรูปภาพแบบอะซิงโครนัส
}
@endความแตกต่างหลักใน Objective-C: ก่อนเรียกเมธอดแบบเลือกได้ จำเป็นต้องมีการตรวจสอบ respondsToSelector: ใน Swift เมธอดโปรโตคอลแบบเลือกได้จะขจัดการตรวจสอบนี้ — optional chaining (?.) จัดการกรณีที่ไม่มีการนำไปใช้โดยอัตโนมัติ
ข้อผิดพลาดในการใช้ดีลิเกต นำไปสู่หน่วยความจำรั่ว แอปพลิเคชันดับ และบักที่ซ่อนเร้น มาดูปัญหาที่พบบ่อยที่สุดห้าประการ
Retain cycle เป็นข้อผิดพลาดที่พบบ่อยที่สุด หากคุณสมบัติ delegate ถูกประกาศเป็น strong และดีลิเกตก็เป็นเจ้าของออบเจกต์เจ้าของด้วย จะเกิดวงจรการคงอยู่ขึ้น ออบเจกต์ทั้งสองจะไม่มีวันถูกปลดปล่อยจากหน่วยความจำ วิธีแก้: ประกาศดีลิเกตเป็น weak var ใน Swift หรือ @property (weak) ใน Objective-C เสมอ
หากออบเจกต์เจ้าของมีอายุยืนกว่าดีลิเกตและการอ้างอิงยังคงอยู่ การเรียกเมธอดดีลิเกตจะส่งผลให้เกิด EXC_BAD_ACCESS การอ้างอิงแบบอ่อนแก้ปัญหานี้โดยอัตโนมัติ: หลังจากดีลิเกตถูกปลดปล่อย คุณสมบัติจะกลายเป็น nil อย่างไรก็ตาม ในสถานการณ์แบบหลายเธรด คุณควรตรวจสอบดีลิเกตเพิ่มเติมในเธรดหลัก
โปรโตคอลที่มีเมธอดมากกว่า 20 รายการละเมิดหลักการแยกอินเทอร์เฟซ (ISP) UITableViewDelegate มีเมธอดแบบเลือกได้ประมาณ 30 รายการ — นี่เป็นข้อยกเว้นทางประวัติศาสตร์ ในโปรโตคอลของคุณเอง ควรแบ่งความรับผิดชอบออกเป็นโปรโตคอลย่อยหลายๆ โปรโตคอล แต่ละโปรโตคอลมีบทบาทของตัวเอง
API ระบบของ Apple ใช้รูปแบบ Delegate อย่างจริงจัง มาดูตัวอย่างสำคัญสามตัวอย่างจาก UIKit ที่ปรากฏในทุกแอปพลิเคชัน iOS
| API | โปรโตคอล | เมธอดหลัก |
|---|---|---|
| UITableView | UITableViewDelegate | didSelectRowAt, heightForRowAt, willDisplay |
| UITextField | UITextFieldDelegate | shouldChangeCharactersIn, didBeginEditing, shouldReturn |
| URLSession | URLSessionDelegate | didReceiveChallenge, didCompleteWithError, didBecomeInvalidWithError |
แต่ละโปรโตคอลเหล่านี้นำพฤติกรรมในแง่มุมต่างๆ ไปใช้: UITableViewDelegate จัดการรูปลักษณ์และการตอบสนองต่อการสัมผัส UITextFieldDelegate ควบคุมการป้อนข้อความ URLSessionDelegate จัดการเหตุการณ์เครือข่าย สิ่งนี้แสดงให้เห็นถึงความยืดหยุ่นของรูปแบบ: ดีลิเกตสามารถปรับให้เข้ากับพื้นที่ความรับผิดชอบใดๆ
คำถามที่พบบ่อย
Delegate จัดการพฤติกรรมและรูปลักษณ์ (ความสูงของเซลล์ การตอบสนองต่อการสัมผัส) DataSource ให้ข้อมูล (จำนวนแถว เนื้อหาของเซลล์) ใน UITableViewDelegate และ UITableViewDataSource — เป็นโปรโตคอลสองรายการที่แยกจากกันซึ่งแบ่งความรับผิดชอบระหว่างการนำเสนอและข้อมูล
การอ้างอิงแบบอ่อน ป้องกัน retain cycle เจ้าของ (เช่น UITableView) เก็บการอ้างอิงไปยังดีลิเกตเป็น weak เท่านั้น หากดีลิเกต (UIViewController) เป็นเจ้าของตาราง การอ้างอิงแบบแข็งไปยังดีลิเกตจะสร้างวงจร: ViewController → UITableView → Delegate (ViewController) Weak ทำลายวงจรนี้
ใน SwiftUI รูปแบบ Delegate ถูกใช้น้อยลง — มันถูกแทนที่ด้วย @Binding, @State และ closure อย่างไรก็ตาม ดีลิเกตยังคงใช้สำหรับการรวม UIKit ผ่าน UIViewRepresentable ตัวอย่างเช่น MKMapViewDelegate และ WKUIDelegate ยังคงมีความเกี่ยวข้องเมื่อห่อหุ้มส่วนประกอบ UIKit ใน SwiftUI
@objc optional อนุญาตให้ประกาศเมธอดแบบเลือกได้ในโปรโตคอล Swift นี่คือกลไกความเข้ากันได้กับรันไทม์ Objective-C หากไม่มี @objc เมธอดโปรโตคอล Swift ทั้งหมดจะบังคับโดยค่าเริ่มต้น Optional ใช้ในโปรโตคอล UIKit ที่ดีลิเกตสามารถนำไปใช้เฉพาะเมธอดที่ต้องการเท่านั้น
หนึ่งออบเจกต์สามารถมี ดีลิเกตเพียงตัวเดียว สำหรับแต่ละคุณสมบัติ delegate หากคุณต้องแจ้งให้หลายออบเจกต์ทราบ ให้ใช้ multicast delegate, อาร์เรย์ของดีลิเกต หรือ NotificationCenter รูปแบบ Delegate ถูกออกแบบมาให้เป็นความสัมพันธ์แบบ 1:1
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ