NotificationCenter เป็นกลไกระบบใน iOS สำหรับการส่งและรับการแจ้งเตือนระหว่างส่วนประกอบของแอปพลิเคชันโดยไม่ต้องมีการเชื่อมต่อโดยตรงระหว่างผู้ส่งและผู้รับ โดยอิงจากรูปแบบ Observer NotificationCenter ช่วยให้ออบเจกต์สามารถสมัครรับเหตุการณ์และตอบสนองต่อเหตุการณ์เหล่านั้นแบบไม่พร้อมกัน ตามเอกสารของ Apple (2025) NSNotificationCenter รองรับทั้งการส่งการแจ้งเตือนแบบซิงโครนัสผ่าน post(name:object:) และการส่งแบบเลื่อนเวลาผ่าน NotificationQueue ศูนย์การแจ้งเตือนทำงานภายในกระบวนการเดียวและไม่ข้ามขอบเขตของแอปพลิเคชัน
ประเด็นสำคัญ
NotificationCenter (NSNotificationCenter) เป็นกลไกในตัวของ iOS สำหรับการสื่อสารแบบ耦合กันอย่างหลวม ๆ ระหว่างออบเจกต์ รูปแบบ Observer ช่วยให้ออบเจกต์หนึ่ง (ผู้ส่ง) แจ้งให้ออบเจกต์อื่น ๆ (ผู้สังเกตการณ์) ทราบเกี่ยวกับเหตุการณ์โดยไม่ต้องอ้างอิงถึงโดยตรง NotificationCenter ทำงานกับสามเอนทิตี: Notification.Name (ตัวระบุการแจ้งเตือน), Notification (คอนเทนเนอร์พร้อมข้อมูล) และ NotificationCenter (ตัวจัดส่ง) แต่ละแอปพลิเคชันมี default center ที่ใช้ร่วมกัน
Notification.Name เป็นโครงสร้างที่ระบุประเภทการแจ้งเตือน สร้างขึ้นผ่าน extension Name: Notification.Name(“MyNotification”) Notification เป็นออบเจกต์ที่ประกอบด้วย name, object (ผู้ส่ง) และ userInfo (พจนานุกรมพร้อมข้อมูล) การแจ้งเตือนของระบบถูกประกาศเป็นค่าคงที่: UIApplication.didBecomeActiveNotification, UIResponder.keyboardWillShowNotification การแจ้งเตือนแบบกำหนดเองควรจัดกลุ่มผ่าน extension เพื่อหลีกเลี่ยงการชนกันของชื่อ ชื่อควรเป็นแบบโดเมนย้อนกลับ
// การกำหนดการแจ้งเตือนแบบกำหนดเอง
extension Notification.Name {
static let dataDidUpdate =
Notification.Name("com.app.dataDidUpdate")
static let userLoggedOut =
Notification.Name("com.app.userLoggedOut")
}
// การส่งการแจ้งเตือนพร้อมข้อมูล
let userInfo: [String: Any] = [
"userId": 123,
"timestamp": Date()
]
NotificationCenter.default.post(
name: .dataDidUpdate,
object: nil,
userInfo: userInfo
)
ผู้สังเกตการณ์สมัครรับการแจ้งเตือนผ่านเมธอด addObserver(_:selector:name:object:) Selector คือเมธอดที่จะถูกเรียกเมื่อได้รับการแจ้งเตือน พารามิเตอร์ object ช่วยให้กรองการแจ้งเตือนจากผู้ส่งที่เฉพาะเจาะจงได้ ถ้า object เป็น nil ผู้สังเกตการณ์จะได้รับการแจ้งเตือนทั้งหมดที่มีชื่อระบุจากผู้ส่งใด ๆ ตั้งแต่ iOS 9 เป็นต้นมา addObserver ไม่ต้องการการลบด้วยตนเองสำหรับ API แบบ block-based แต่ selector-based ยังคงต้องการ removeObserver
// การสมัครรับการแจ้งเตือน (แบบ selector)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDataUpdate),
name: .dataDidUpdate,
object: nil
)
@objc func handleDataUpdate(_ notification: Notification) {
guard let userId = notification.userInfo?["userId"] as? Int else { return }
updateUI(for: userId)
}
// การสมัครรับการแจ้งเตือน (แบบ block, iOS 9+)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: .dataDidUpdate,
object: nil,
queue: .main
) { [weak self] notification in
guard let self else { return }
self.handleNotification(notification)
}
NotificationCenter เก็บตารางการแมป (ชื่อ → ชุดผู้สังเกตการณ์) เมื่อผู้ส่งเรียก post(name:object:) ศูนย์การแจ้งเตือนจะวนซ้ำผู้สังเกตการณ์ทั้งหมดที่สมัครชื่อนั้นแบบซิงโครนัสและเรียก selector หรือบล็อกของพวกเขา คุณสมบัติสำคัญ: post บล็อกเธรดปัจจุบันจนกว่าตัวจัดการทั้งหมดจะเสร็จสมบูรณ์ ถ้าตัวจัดการดำเนินการที่หนักหน่วง จะทำให้ผู้ส่งล่าช้า NotificationQueue แก้ปัญหานี้โดยเลื่อนการส่งการแจ้งเตือน
เมธอด post(name:object:userInfo:) ส่งการแจ้งเตือนทันทีไปยังผู้สังเกตการณ์ทั้งหมด การเรียกเป็นแบบซิงโครนัส — โค้ดหลัง post จะทำงานหลังจากตัวจัดการทั้งหมดเสร็จสมบูรณ์เท่านั้น ลำดับการเรียกผู้สังเกตการณ์ไม่ได้รับการรับประกันและอาจเปลี่ยนแปลงระหว่างการเรียกใช้ สำหรับการประมวลผลตามลำดับ ให้ใช้ NotificationQueue พร้อม coalescing อย่าเรียก post ภายในตัวจัดการของการแจ้งเตือนเดียวกัน — จะทำให้เกิดการเรียกซ้ำไม่สิ้นสุด
NotificationQueue เพิ่มการแจ้งเตือนไปยังคิวสำหรับการส่งแบบไม่พร้อมกัน รองรับ coalescing (การรวมการแจ้งเตือนที่เหมือนกัน) และการเลือกคิวส่ง (asap, idle, modal) Coalescing มีประโยชน์สำหรับเหตุการณ์ที่เกิดขึ้นบ่อย (ความคืบหน้าการดาวน์โหลด) เมื่อคุณต้องการแจ้งเตือนด้วยค่าเท่านั้น NotificationQueue ใช้รันลูปเพื่อทำงาน ดังนั้นจึงทำงานเฉพาะในเธรดที่มีรันลูปที่ทำงานอยู่
// การส่งแบบเลื่อนเวลาผ่าน NotificationQueue
let notification = Notification(
name: .dataDidUpdate,
object: self,
userInfo: ["progress": 0.5]
)
// Coalescing: การแจ้งเตือนหลายรายการรวมเป็นหนึ่งเดียว
NotificationQueue.default.enqueue(
notification,
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
// การส่งแบบไม่พร้อมกันผ่าน DispatchQueue
DispatchQueue.main.async {
NotificationCenter.default.post(name: .dataDidUpdate, object: nil)
}
iOS มีกลไกหลักสามประการสำหรับการสื่อสารระหว่างออบเจกต์: NotificationCenter, Delegate และ KVO (Key-Value Observing) แต่ละกลไกแก้ปัญหาเรื่องการแจ้งเตือน แต่มีข้อแลกเปลี่ยนที่แตกต่างกันในด้านการ耦合กัน ประสิทธิภาพ และความปลอดภัยของประเภท การเลือกกลไกขึ้นอยู่กับความสัมพันธ์แบบหนึ่งต่อหนึ่งหรือหนึ่งต่อหลาย และความจำเป็นในการถ่ายโอนข้อมูล
| ลักษณะ | NotificationCenter | Delegate | KVO |
|---|---|---|---|
| การ耦合กัน | หลวม (ชื่อการแจ้งเตือน) | แน่น (โปรโตคอล) | ปานกลาง (คีย์) |
| ความสัมพันธ์ | หนึ่งต่อหลาย | หนึ่งต่อหนึ่ง | หนึ่งต่อหลาย |
| ความปลอดภัยของประเภท | ต่ำ (userInfo เป็น Dictionary) | สูง (เมธอดของโปรโตคอล) | ปานกลาง (Any?) |
| ประสิทธิภาพ | ปานกลาง (การวนซ้ำตาราง) | สูง (เรียกโดยตรง) | ต่ำ (NSObject) |
| ความไม่พร้อมกัน | ซิงโครนัส (post บล็อก) | ซิงโครนัสในเธรดของผู้ส่ง | ซิงโครนัสเมื่อเปลี่ยน |
NotificationCenter เหมาะสำหรับเหตุการณ์ที่ส่วนประกอบอิสระหลายตัวต้องตอบสนอง ตัวอย่าง: การเปลี่ยนแปลงการตั้งค่าแอปพลิเคชัน การออกจากระบบของผู้ใช้ การรับการแจ้งเตือน push ในพื้นหลัง NotificationCenter ยังเหมาะสำหรับโมดูลที่耦合กันอย่างหลวม ๆ (คุณสมบัติ A ไม่ควรรู้เกี่ยวกับคุณสมบัติ B) ข้อเสียคือขาดความปลอดภัยของประเภท: คีย์ userInfo เป็นสตริง ไม่ใช่ enum
Delegate เป็นตัวเลือกสำหรับความสัมพันธ์แบบหนึ่งต่อหนึ่งที่มีสัญญาชัดเจน (tableView.delegate) Delegate เร็วกว่าและปลอดภัยกว่าตามประเภท KVO เป็นตัวเลือกสำหรับการสังเกตการเปลี่ยนแปลงของคุณสมบัติเฉพาะของโมเดล (isLoading, progress) KVO ต้องการการสืบทอดจาก NSObject และอาจทำให้เกิดความยุ่งยากในการดีบัก (คีย์สตริงมหัศจรรย์) ใน Swift สมัยใหม่ Combine และลำดับ async แทนที่ทั้งสามแนวทาง
เมธอด addObserver รองรับสองรูปแบบการสมัคร: selector-based (แบบดั้งเดิม) และ block-based (พร้อม closure) Selector-based ต้องการความเข้ากันได้ของ @objc และการลบผู้สังเกตการณ์ด้วยตนเอง Block-based (iOS 9+) อนุญาตให้ใช้ capture list และถูกจัดการโดย OS โดยอัตโนมัติเมื่อใช้บล็อกโดยไม่มีการอ้างอิงที่แน่นหนา Block-based ยังรองรับคิว — ผู้สังเกตการณ์ได้รับการแจ้งเตือนในคิวที่ระบุ
วิธีการสมัครแบบดั้งเดิมผ่าน selector เมธอดตัวจัดการต้องถูกทำเครื่องหมายด้วย @objc และรับ Notification แบบเลือกได้ ข้อดี: สามารถใช้ได้โดยคลาสใด ๆ รวมถึง Objective-C ดั้งเดิม ข้อเสีย: ขาดความปลอดภัยของประเภทของ selector ความเสี่ยงในการพิมพ์ชื่อ selector ผิด removeObserver ที่จำเป็นใน deinit ถ้าผู้สังเกตการณ์ถูกลบก่อนออบเจกต์ ตัวจัดการจะไม่ถูกเรียก
API แบบ block-based รับ closure ที่ทำงานเมื่อได้รับการแจ้งเตือน พารามิเตอร์ queue กำหนดว่าบล็อกจะทำงานในคิวใด — คิวหลักสำหรับการอัปเดต UI หรือคิวพื้นหลังสำหรับประมวลผลข้อมูล ค่าที่ส่งคืน NSObjectProtocol ใช้สำหรับลบผู้สังเกตการณ์: NotificationCenter.default.removeObserver(observer) แบบ block-based เป็นที่นิยมใน Swift สมัยใหม่
protocol NotificationToken {
func dispose()
}
extension NotificationCenter {
func observe(
name: NSNotification.Name,
object: Any? = nil,
queue: OperationQueue? = .main,
using block: @escaping (Notification) -> Void
) -> NotificationToken {
let observer = addObserver(forName: name, object: object,
queue: queue, using: block)
return NotificationTokenWrapper(observer: observer, center: self)
}
}
// การใช้กับการลบอัตโนมัติ
class ViewModel {
private var tokens: [NotificationToken] = []
func startObserving() {
let token = NotificationCenter.default.observe(
name: .dataDidUpdate,
queue: .main
) { [weak self] notification in
self?.handleUpdate(notification)
}
tokens.append(token)
}
deinit {
tokens.forEach { $0.dispose() }
}
}
การรั่วไหลของหน่วยความจำ เป็นหนึ่งในปัญหาหลักเมื่อทำงานกับ NotificationCenter ถ้าผู้สังเกตการณ์ไม่ถูกลบก่อนการจัดสรรคืน เมื่อส่งการแจ้งเตือน ศูนย์จะพยายามเรียกเมธอดบนออบเจกต์ที่ถูกจัดสรรคืนแล้ว ทำให้เกิด EXC_BAD_ACCESS ตั้งแต่ iOS 9 เป็นต้นมา block-based addObserver ใช้การอ้างอิงแบบอ่อน แต่ selector-based ยังคงต้องการ removeObserver ด้วยตนเอง แนวปฏิบัติที่ดีที่สุด: ลบผู้สังเกตการณ์ใน deinit
Selector-based: เรียก NotificationCenter.default.removeObserver(self) ใน deinit เสมอ ถ้าผู้สังเกตการณ์สมัครรับการแจ้งเตือนหลายรายการ คุณสามารถลบทั้งหมดพร้อมกัน (ไม่มีพารามิเตอร์) หรือเฉพาะรายการตามชื่อ Block-based: ลบผ่าน removeObserver ด้วยโทเค็นที่ส่งคืนจาก addObserver สำหรับ block-based บน iOS 9+ จะไม่เกิดการรั่วไหล แต่การลบยังคงแนะนำเพื่อประสิทธิภาพ: ผู้สังเกตการณ์ที่ถูกจัดสรรคืนจะไม่ถูกวนซ้ำระหว่าง post
class SafeObserver {
private var observers: [NSObjectProtocol] = []
func addSubscriptions() {
let token1 = NotificationCenter.default.addObserver(
forName: .dataDidUpdate, object: nil,
queue: .main) { [weak self] _ in
self?.refreshData()
}
let token2 = NotificationCenter.default.addObserver(
forName: .userLoggedOut, object: nil,
queue: .main) { [weak self] _ in
self?.logout()
}
observers.append(contentsOf: [token1, token2])
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
private func refreshData() { }
private func logout() { }
}
รูปแบบ Token ทำให้การจัดการผู้สังเกตการณ์เป็นอัตโนมัติ เมื่อสมัคร ออบเจกต์โทเค็น (NSObjectProtocol) จะถูกส่งคืน ซึ่งจะลบผู้สังเกตการณ์โดยอัตโนมัติเมื่อถูกจัดสรรคืน NotificationTokenWrapper จัดเก็บการอ้างอิงแบบอ่อนไปยัง NotificationCenter และโทเค็นผู้สังเกตการณ์ เรียก removeObserver ใน deinit ทำให้ NotificationCenter ใกล้เคียงกับแนวทาง Combine ที่ AnyCancellable จัดการวงจรชีวิตการสมัคร
ความปลอดภัยของเธรด: NotificationCenter รับประกันว่า post สามารถเรียกจากเธรดใดก็ได้ และผู้สังเกตการณ์ทั้งหมดจะได้รับการแจ้งเตือนในเธรดเดียวกับที่ post ถูกเรียก นี่เป็นสิ่งสำคัญสำหรับแอปพลิเคชันแบบหลายเธรด: ถ้าการแจ้งเตือนถูกส่งจากเธรดพื้นหลัง ตัวจัดการก็จะทำงานในเธรดพื้นหลังด้วย สำหรับการอัปเดต UI ให้ส่งการจัดการไปยังคิวหลักผ่าน DispatchQueue.main.async
NotificationCenter ปลอดภัยสำหรับเธรดสำหรับการเรียก post และ addObserver จากเธรดที่แตกต่างกัน การซิงโครไนซ์ภายใน ใช้การล็อก ดังนั้นการเรียก post บ่อยครั้งจากหลายเธรดอาจสร้างการแย่งชิง สำหรับสถานการณ์ที่มีโหลดสูง (ความคืบหน้าการดาวน์โหลด 1,000 ไฟล์) ให้ใช้คิวการแจ้งเตือนแยกต่างหากหรือ Combine publisher NotificationQueue ที่มี postingStyle .now เทียบเท่ากับ post โดยตรง
NotificationCenter รองรับ Combine publisher ผ่าน NotificationCenter.default.publisher(for:object:) Publisher เปลี่ยนการแจ้งเตือนแต่ละครั้งเป็นเหตุการณ์ Combine ที่สามารถแปลงผ่าน map, filter, debounce และ throttle ซึ่งแก้ปัญหาการส่งแบบซิงโครนัส: Combine ประมวลผลการแจ้งเตือนแบบไม่พร้อมกันบน Scheduler ที่ระบุ NotificationCenter.publisher เป็นสะพานเชื่อมระหว่างกลไกดั้งเดิมและการเขียนโปรแกรมเชิงรับสมัยใหม่
import Combine
class ReactiveViewModel {
private var cancellables = Set<AnyCancellable>()
func setupCombineSubscription() {
NotificationCenter.default
.publisher(for: .dataDidUpdate)
.receive(on: DispatchQueue.main)
.compactMap { $0.userInfo?["progress"] as? Float }
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.sink { [weak self] progress in
self?.progressLabel.text = "\(Int(progress * 100))%"
}
.store(in: &cancellables)
}
}
คำถามที่พบบ่อย
ใช่ NotificationCenter ปลอดภัยสำหรับเธรดสำหรับการเรียก post และ addObserver จากเธรดใด ๆ อย่างไรก็ตาม ตัวจัดการทำงานในเธรดเดียวกับที่เรียก post สำหรับการอัปเดต UI ให้ใช้ queue: .main ใน block-based addObserver หรือ DispatchQueue.main.async ภายในตัวจัดการ Combine publisher ที่มี receive(on:) ก็แก้ปัญหาเธรดได้เช่นกัน
Selector-based: ข้อขัดข้อง EXC_BAD_ACCESS เมื่อส่งการแจ้งเตือนหลังจากการจัดสรรคืนผู้สังเกตการณ์ Block-based (iOS 9+): ไม่มีการรั่วไหลเนื่องจากการอ้างอิงแบบอ่อน แต่ศูนย์การแจ้งเตือนยังคงเก็บบล็อกไว้ในหน่วยความจำจนกว่าจะมีการเรียก removeObserver อย่างชัดเจน ขอแนะนำให้ลบผู้สังเกตการณ์ใน deinit หรือใช้รูปแบบ Token สำหรับการจัดการอัตโนมัติ
NotificationCenter เป็นกลไกการกระจายเสียงสำหรับเหตุการณ์ที่กำหนดเองระหว่างส่วนประกอบที่ไม่เกี่ยวข้อง KVO สังเกตการเปลี่ยนแปลงของคุณสมบัติเฉพาะของออบเจกต์เฉพาะ KVO ต้องการการสืบทอด NSObject และแจ้งเตือนโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลงคุณสมบัติผ่าน setter NotificationCenter แจ้งเตือนเมื่อเรียก post อย่างชัดเจนเท่านั้น สำหรับการสังเกตโมเดล KVO หรือ Combine เป็นที่นิยมกว่า
หนึ่ง default center ต่อกระบวนการแอปพลิเคชัน สามารถสร้างศูนย์เพิ่มเติมผ่าน NotificationCenter() ได้ แต่ในทางปฏิบัติจะใช้ default ที่ใช้ร่วมกัน แต่ละศูนย์ทำงานอย่างอิสระ — post ในหนึ่งศูนย์จะไม่ถูกส่งไปยังผู้สังเกตการณ์ของอีกศูนย์ สำหรับการแยกโมดูล ให้ใช้เนมสเปซ Name แยกต่างหากผ่านชื่อการแจ้งเตือนแบบโดเมนย้อนกลับ
บางส่วน Combine มี NotificationCenter.Publisher ซึ่งห่อ NotificationCenter ในสตรีมเชิงรับ Combine แก้ปัญหาการซิงโครนัส (ผ่าน receive(on:)) เพิ่มตัวดำเนินการแปลง และการจัดการการสมัครอัตโนมัติ (AnyCancellable) อย่างไรก็ตาม NotificationCenter ยังคงอยู่สำหรับการแจ้งเตือนระบบ iOS (UIApplication, UIKeyboard) และโค้ดดั้งเดิม Combine คือการปรับปรุง ไม่ใช่การแทนที่
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม