# สัมภาษณ์ iOS Push Notifications 2026: APNs, โทเคน และ troubleshooting > คู่มือเตรียมสัมภาษณ์ iOS อย่างครบถ้วนเกี่ยวกับ Push Notifications, APNs, การจัดการโทเคน และ troubleshooting พร้อมคำถามยอดนิยมและคำตอบโดยละเอียด - Published: 2026-02-28 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, push-notifications, apns, swift, interview - Reading time: 12 min --- Push Notifications ยังเป็นหัวข้อสำคัญในสัมภาษณ์ iOS การเข้าใจการทำงานของ APNs การจัดการ device token และการแก้ปัญหาทั่วไปแสดงถึงความเข้าใจอย่างลึกซึ้งในระบบนิเวศของ Apple คู่มือนี้รวบรวมคำถามสัมภาษณ์ที่พบบ่อยที่สุด > **จุดสำคัญในการสัมภาษณ์** > > ผู้สัมภาษณ์มองหาผู้สมัครที่เข้าใจวงจรชีวิตทั้งหมด: ตั้งแต่การลงทะเบียนอุปกรณ์จนถึงการส่งการแจ้งเตือน รวมถึงการจัดการข้อผิดพลาดอย่างเหมาะสมในแต่ละขั้นตอน ## สถาปัตยกรรม APNs: รากฐานของการแจ้งเตือนบน iOS Apple Push Notification service (APNs) คือบริการกลางที่จัดการการส่ง push notification ไปยังอุปกรณ์ของ Apple การเข้าใจสถาปัตยกรรมของมันจำเป็นต่อการตอบคำถามสัมภาษณ์ได้อย่างมีประสิทธิภาพ ### APNs ทำงานอย่างไร? การสื่อสารเกี่ยวข้องกับสามผู้เล่นหลัก: แอป iOS, APNs และเซิร์ฟเวอร์ backend ขั้นตอนทั้งหมดเป็นดังนี้: ```swift // AppDelegate.swift import UIKit import UserNotifications @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Request notification authorization UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .badge, .sound] ) { granted, error in guard granted else { return } // Register with APNs DispatchQueue.main.async { application.registerForRemoteNotifications() } } return true } } ``` การลงทะเบียน APNs เกิดขึ้นในสองขั้น: ขออนุญาตจากผู้ใช้ จากนั้นเรียก `registerForRemoteNotifications()` ### การจัดการ device token Device token เป็นตัวระบุเฉพาะที่ APNs สร้างขึ้นเพื่อใช้กับอุปกรณ์เครื่องใดเครื่องหนึ่ง โทเคนนี้สามารถเปลี่ยนแปลงได้ และควรถูกส่งไปยังเซิร์ฟเวอร์ backend ทุกครั้งที่เปิดแอป ```swift // AppDelegate.swift extension AppDelegate { // Callback when APNs provides the token func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { // Convert token to hexadecimal string let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() print("Device Token: \(tokenString)") // Send to backend server sendTokenToServer(tokenString) } // Callback on registration failure func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { print("Failed to register: \(error.localizedDescription)") } private func sendTokenToServer(_ token: String) { // Implement HTTP request to backend } } ``` โทเคนถูกส่งกลับมาในรูปของ `Data` และต้องถูกแปลงเป็นสตริงเลขฐานสิบหกก่อนส่งไปยังเซิร์ฟเวอร์ ## คำถามสัมภาษณ์ APNs ที่พบบ่อย สัมภาษณ์ iOS มักมีทั้งคำถามทฤษฎีและปฏิบัติเกี่ยวกับ APNs ต่อไปนี้คือคำถามที่พบบ่อยที่สุดพร้อมคำตอบโดยละเอียด ### คำถามที่ 1: ความแตกต่างระหว่าง APNs sandbox กับ production คืออะไร? > **สภาพแวดล้อมของ APNs** > > APNs มีสองสภาพแวดล้อมแยกกันโดยมี endpoint ต่างกัน โทเคนที่สร้างในสภาพแวดล้อมหนึ่งจะใช้ในอีกสภาพแวดล้อมไม่ได้ | สภาพแวดล้อม | Endpoint | การใช้งาน | |--------------|----------|-----------| | Sandbox | api.sandbox.push.apple.com | Debug, TestFlight | | Production | api.push.apple.com | App Store | ### คำถามที่ 2: จัดการกับโทเคนหมดอายุอย่างไร? Device token อาจเปลี่ยนได้ด้วยหลายเหตุผล: การคืนค่าระบบ การติดตั้งบนอุปกรณ์ใหม่ หรือการรีเฟรชเป็นระยะโดย APNs เซิร์ฟเวอร์ต้องจัดการกับการตอบกลับข้อผิดพลาดของ APNs อย่างเหมาะสม ```swift // NotificationService.swift enum APNsError: Int { case badDeviceToken = 400 case unregistered = 410 case payloadTooLarge = 413 case tooManyRequests = 429 case internalServerError = 500 var shouldRemoveToken: Bool { // Remove token only if device is no longer registered return self == .unregistered || self == .badDeviceToken } } struct APNsResponse { let statusCode: Int let deviceToken: String func handleError() { guard let error = APNsError(rawValue: statusCode) else { return } if error.shouldRemoveToken { // Remove token from database TokenRepository.shared.remove(deviceToken) } } } ``` การจัดการข้อผิดพลาด APNs ฝั่งเซิร์ฟเวอร์มีความสำคัญอย่างยิ่งต่อการรักษาฐานข้อมูลโทเคนให้สะอาดและหลีกเลี่ยงคำขอที่ไม่จำเป็น ### คำถามที่ 3: ใช้งาน silent notification อย่างไร? Silent notification ช่วยปลุกแอปให้ทำงานพื้นหลังเพื่อดำเนินงานโดยไม่แสดงการแจ้งเตือนใด ๆ ต่อผู้ใช้ ```swift // AppDelegate.swift func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { // Check if this is a silent notification guard let aps = userInfo["aps"] as? [String: Any], aps["content-available"] as? Int == 1 else { completionHandler(.noData) return } // Perform background task performBackgroundTask { result in switch result { case .success: completionHandler(.newData) case .failure: completionHandler(.failed) } } } ``` Payload JSON ของ silent notification ต้องมี `"content-available": 1` ภายในออบเจ็กต์ `aps` ## Notification Service Extension: การปรับแต่งขั้นสูง Notification Service Extension อนุญาตให้แก้ไขเนื้อหาของการแจ้งเตือนก่อนแสดงผล ฟีเจอร์นี้มักถูกถามในการสัมภาษณ์ ### สร้าง Service Extension ```swift // NotificationService.swift (in Extension target) import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { self.contentHandler = contentHandler bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent guard let bestAttemptContent = bestAttemptContent else { contentHandler(request.content) return } // Modify content if let imageURLString = bestAttemptContent.userInfo["image-url"] as? String, let imageURL = URL(string: imageURLString) { downloadImage(from: imageURL) { attachment in if let attachment = attachment { bestAttemptContent.attachments = [attachment] } contentHandler(bestAttemptContent) } } else { contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called when time limit (30 seconds) is exceeded if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } private func downloadImage( from url: URL, completion: @escaping (UNNotificationAttachment?) -> Void ) { URLSession.shared.downloadTask(with: url) { localURL, _, error in guard let localURL = localURL, error == nil else { completion(nil) return } let tempDirectory = FileManager.default.temporaryDirectory let fileName = url.lastPathComponent let destinationURL = tempDirectory.appendingPathComponent(fileName) try? FileManager.default.moveItem(at: localURL, to: destinationURL) let attachment = try? UNNotificationAttachment( identifier: fileName, url: destinationURL, options: nil ) completion(attachment) }.resume() } } ``` Extension มีเวลา 30 วินาทีในการแก้ไขเนื้อหา หากเกินขีดจำกัดนี้จะมีการเรียกเมธอด `serviceExtensionTimeWillExpire()` ## Troubleshooting Push Notifications การดีบัก push notification เป็นหัวข้อที่พบซ้ำในการสัมภาษณ์ ผู้สมัครควรรู้จักเครื่องมือและเทคนิคในการวินิจฉัย ### ตรวจสอบสถานะการลงทะเบียน ```swift // DebugHelper.swift struct PushNotificationDebugger { static func checkNotificationStatus() async { let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() print("=== Push Notification Status ===") print("Authorization: \(settings.authorizationStatus.description)") print("Alert: \(settings.alertSetting.description)") print("Badge: \(settings.badgeSetting.description)") print("Sound: \(settings.soundSetting.description)") print("Notification Center: \(settings.notificationCenterSetting.description)") } } extension UNAuthorizationStatus { var description: String { switch self { case .notDetermined: return "Not Determined" case .denied: return "Denied" case .authorized: return "Authorized" case .provisional: return "Provisional" case .ephemeral: return "Ephemeral" @unknown default: return "Unknown" } } } ``` ฟังก์ชัน debug นี้ช่วยตรวจสอบสถานะสิทธิ์ของการแจ้งเตือนได้อย่างรวดเร็ว ### ข้อผิดพลาดที่พบบ่อยและวิธีแก้ > **ข้อผิดพลาดที่ควรรู้** > > ผู้สัมภาษณ์มักถามเกี่ยวกับข้อผิดพลาดที่พบบ่อยและแนวทางแก้ไข ต่อไปนี้คือสิ่งสำคัญที่สุดที่ควรจดจำ | ข้อผิดพลาด | สาเหตุ | วิธีแก้ | |-------------|-------|--------| | โทเคนไม่ถูกต้อง | สภาพแวดล้อมผิด (sandbox/prod) | ตรวจสอบ provisioning profile | | ไม่ได้รับการแจ้งเตือน | เปิดโหมดประหยัดพลังงาน | ทดสอบขณะแบตเต็ม | | Extension ไม่ถูกเรียก | Payload ไม่มี `mutable-content` | เพิ่ม `"mutable-content": 1` | | Background fetch ล้มเหลว | ผู้ใช้ปิดแอปเอง | แจ้งผู้ใช้เกี่ยวกับข้อจำกัด | ### ทดสอบ APNs โดยตรง สำหรับการทดสอบการแจ้งเตือนระหว่างพัฒนา เครื่องมือ `curl` ช่วยให้ส่งคำขอไปที่ APNs ได้โดยตรง: ```swift // TestPayload.swift struct APNsTestPayload { static let silentNotification = """ { "aps": { "content-available": 1 }, "custom-data": "test" } """ static let richNotification = """ { "aps": { "alert": { "title": "New message", "body": "Message content" }, "mutable-content": 1, "sound": "default" }, "image-url": "https://example.com/image.jpg" } """ } ``` Payload ทดสอบเหล่านี้ช่วยตรวจสอบพฤติกรรมของแอปกับการแจ้งเตือนหลายประเภท ## แนวปฏิบัติที่ดีในระดับ production คำถามเกี่ยวกับแนวปฏิบัติที่ดีช่วยประเมินประสบการณ์ของผู้สมัครกับแอป production ### การจัดการข้อผิดพลาดของเครือข่าย ```swift // PushTokenManager.swift actor PushTokenManager { private var pendingToken: String? private var retryCount = 0 private let maxRetries = 3 func registerToken(_ token: String) async { pendingToken = token await sendTokenWithRetry() } private func sendTokenWithRetry() async { guard let token = pendingToken else { return } do { try await APIClient.shared.registerPushToken(token) pendingToken = nil retryCount = 0 } catch { retryCount += 1 if retryCount < maxRetries { // Retry with exponential backoff let delay = UInt64(pow(2.0, Double(retryCount))) * 1_000_000_000 try? await Task.sleep(nanoseconds: delay) await sendTokenWithRetry() } } } } ``` การใช้ `actor` ช่วยให้การจัดการโทเคนปลอดภัยต่อการทำงานพร้อมกันในสภาพแวดล้อมแบบขนาน ### การเก็บโทเคนแบบโลคัล ```swift // TokenStorage.swift struct TokenStorage { private static let tokenKey = "com.app.pushToken" static func save(_ token: String) { UserDefaults.standard.set(token, forKey: tokenKey) } static func retrieve() -> String? { UserDefaults.standard.string(forKey: tokenKey) } static func hasTokenChanged(_ newToken: String) -> Bool { guard let savedToken = retrieve() else { return true } return savedToken != newToken } } ``` การบันทึกโทเคนแบบโลคัลช่วยลดการเรียกเครือข่ายที่ไม่จำเป็นเมื่อโทเคนยังไม่เปลี่ยน ## สรุป ความเชี่ยวชาญใน iOS Push Notifications แสดงถึงความเข้าใจอย่างลึกซึ้งในระบบนิเวศของ Apple และการสื่อสารระหว่างไคลเอนต์-เซิร์ฟเวอร์ จุดสำคัญที่ควรจำสำหรับการสัมภาษณ์: ✅ สถาปัตยกรรม APNs: เข้าใจกระบวนการตั้งแต่ลงทะเบียนจนถึงส่งการแจ้งเตือน ✅ Device token: วงจรชีวิตและการจัดการเมื่อโทเคนเปลี่ยน ✅ Notification Service Extension: ปรับแต่งเนื้อหาภายในกรอบ 30 วินาที ✅ Troubleshooting: รู้จักข้อผิดพลาดทั่วไปและวิธีแก้ ✅ Silent notifications: `content-available` สำหรับ background fetch ✅ แนวปฏิบัติที่ดี: ตรรกะ retry การเก็บโทเคนแบบโลคัล การจัดการข้อผิดพลาด --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/ios-push-notifications-apns-interview